Charts dashboard — gráficos em Canvas 🚀¶
Neste exemplo você vai montar um mini dashboard de analytics: dois gráficos
(barras e linhas multi-série) desenhados em <canvas>, uma fileira de cartões de
métrica, e um botão Next week que reescreve os dados. Tudo controlado por
estado Python tipado — você não escreve uma linha de JavaScript.
O que você vai construir¶
- 📊 Um BarChart com a receita diária da semana.
- 📈 Um LineChart com duas séries (visitas e cadastros).
- 🧮 Uma fileira de MetricCard / StatCard com totais e variação percentual.
- 🔁 Um botão Next week que avança a janela de dados e repinta os gráficos.
Nota — dados determinísticos
O exemplo guarda apenas um índice de semana (week: int) no estado. Todos os
números — totais, deltas, séries — são derivados dentro de view() a cada
render. Sem estado redundante, sem dessincronização.
Pré-requisitos¶
Dica
Se você ainda não conhece o ciclo estado → view → patches, leia primeiro o tutorial de introdução.
Passo 1 — Os dados de domínio¶
Começamos com duas semanas de números sintéticos. O dashboard mostra uma semana por vez; o botão alterna entre elas.
from __future__ import annotations
from dataclasses import dataclass, field
# Duas semanas de figuras diárias sintéticas.
WEEKLY_REVENUE: list[list[float]] = [
[1200.0, 1500.0, 900.0, 1800.0, 2100.0, 2400.0, 1700.0],
[1600.0, 1400.0, 2000.0, 2300.0, 1900.0, 2600.0, 2200.0],
]
WEEKLY_VISITS: list[list[float]] = [
[320.0, 410.0, 280.0, 500.0, 640.0, 720.0, 480.0],
[450.0, 390.0, 560.0, 680.0, 600.0, 810.0, 700.0],
]
WEEKLY_SIGNUPS: list[list[float]] = [
[12.0, 18.0, 9.0, 22.0, 31.0, 40.0, 25.0],
[20.0, 16.0, 28.0, 34.0, 30.0, 45.0, 38.0],
]
DAY_LABELS: list[str] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
Os dados ficam como constantes de módulo — nunca são copiados para o estado.
Passo 2 — O estado¶
O estado é minúsculo: só o índice da semana e os rótulos do eixo X.
@dataclass
class DashboardState:
"""State for the analytics dashboard.
Attributes:
week: Index of the currently displayed week (0 or 1).
labels: The x-axis day labels shared by every chart.
"""
week: int = 0
labels: list[str] = field(default_factory=lambda: list(DAY_LABELS))
def make_state() -> DashboardState:
"""Build the initial dashboard state.
Returns:
A fresh :class:`DashboardState` showing the first week.
"""
return DashboardState()
Passo 3 — Helpers de formatação¶
Dois pequenos helpers puros mantêm a view limpa: um formata dinheiro, o outro
calcula a variação percentual entre dois totais.
def _money(value: float) -> str:
"""Format a number as a compact USD string.
Args:
value: The raw monetary amount.
Returns:
The amount formatted with a dollar sign and thousands separators.
"""
return f"${value:,.0f}"
def _delta_pct(current: float, previous: float) -> tuple[str, bool]:
"""Compute a percentage delta and its direction.
Args:
current: The current period's total.
previous: The prior period's total to compare against.
Returns:
A tuple of the formatted percentage string and whether it went up.
"""
if previous == 0.0:
return "+0%", True
change: float = (current - previous) / previous * 100.0
return f"{change:+.1f}%", change >= 0.0
Dica — funções puras são testáveis
Como _money e _delta_pct não tocam em app.state, você pode testá-las
diretamente com pytest, sem montar nenhum runtime.
Passo 4 — Os cartões de métrica¶
MetricCard e StatCard recebem label, value, delta, delta_up e um
color_scheme. O delta_up controla a cor da seta (verde para cima, vermelho
para baixo).
from tempest_core import Row, Style
from tempestweb.components import MetricCard, StatCard
metrics: Row = Row(
style=Style(gap=12.0),
children=[
MetricCard(
key="m-revenue",
label="Revenue",
value=_money(revenue_total),
delta=revenue_delta,
delta_up=revenue_up,
color_scheme="primary",
),
MetricCard(
key="m-visits",
label="Visits",
value=f"{visits_total:,.0f}",
delta=visits_delta,
delta_up=visits_up,
color_scheme="secondary",
),
StatCard(
key="m-signups",
label="Sign-ups",
value=f"{signups_total:,.0f}",
delta=signups_delta,
delta_up=signups_up,
color_scheme="tertiary",
),
],
)
Passo 5 — Os gráficos em Canvas¶
O BarChart recebe values + labels. O LineChart recebe uma lista de
ChartSeries, cada uma com seus points, um label e um color_scheme. Ambos
desenham no <canvas> — width/height definem o tamanho do bitmap.
from tempest_core import Card, Text
from tempestweb.components import BarChart, ChartSeries, LineChart
revenue_card: Card = Card(
key="card-revenue",
children=[
Text(content="Daily revenue", key="title-revenue"),
BarChart(
key="chart-revenue",
width=520.0,
height=220.0,
color_scheme="primary",
values=revenue,
labels=app.state.labels,
),
],
)
trends_card: Card = Card(
key="card-trends",
children=[
Text(content="Engagement trends", key="title-trends"),
LineChart(
key="chart-trends",
width=520.0,
height=220.0,
series=[
ChartSeries(points=visits, label="Visits", color_scheme="primary"),
ChartSeries(
points=signups,
label="Sign-ups",
color_scheme="tertiary",
),
],
),
],
)
Info — o Canvas é só mais um nó da árvore
O renderizador DOM emite um <canvas> e reexecuta o desenho quando os
values/series mudam. Para você, autor do app, o gráfico é apenas um widget
como qualquer outro — nada de manipular contexto 2D na mão.
Passo 6 — O handler "Next week"¶
Um único handler avança a janela de dados, fazendo wrap com módulo:
def next_week() -> None:
app.set_state(lambda s: setattr(s, "week", (s.week + 1) % len(WEEKLY_REVENUE)))
Como tudo é derivado de state.week, mudar esse índice repinta todos os
gráficos e cartões de uma só vez.
O app completo¶
"""Charts dashboard — a tempestweb example showcasing Canvas-backed charts.
This small analytics dashboard renders two charts (a bar chart and a multi-series
line chart) plus a row of metric cards, all driven entirely by typed Python state.
Like every tempestweb example, the same ``view`` runs unchanged in both modes::
tempestweb dev --mode wasm # Python in the browser (Pyodide)
tempestweb dev --mode server # Python on the server (FastAPI + WebSocket)
A "Next week" button mutates the state to advance the data window, demonstrating
that the charts re-render reactively from the same source of truth.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from tempest_core import App, Button, Column, Row, Style, Text, Widget
from tempest_core.style import Edge
from tempestweb.components import (
BarChart,
Card,
ChartSeries,
LineChart,
MetricCard,
StatCard,
)
# Two weeks of synthetic daily figures. The dashboard shows one week at a time and
# the "Next week" button toggles between them.
WEEKLY_REVENUE: list[list[float]] = [
[1200.0, 1500.0, 900.0, 1800.0, 2100.0, 2400.0, 1700.0],
[1600.0, 1400.0, 2000.0, 2300.0, 1900.0, 2600.0, 2200.0],
]
WEEKLY_VISITS: list[list[float]] = [
[320.0, 410.0, 280.0, 500.0, 640.0, 720.0, 480.0],
[450.0, 390.0, 560.0, 680.0, 600.0, 810.0, 700.0],
]
WEEKLY_SIGNUPS: list[list[float]] = [
[12.0, 18.0, 9.0, 22.0, 31.0, 40.0, 25.0],
[20.0, 16.0, 28.0, 34.0, 30.0, 45.0, 38.0],
]
DAY_LABELS: list[str] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
@dataclass
class DashboardState:
"""State for the analytics dashboard.
Attributes:
week: Index of the currently displayed week (0 or 1).
labels: The x-axis day labels shared by every chart.
"""
week: int = 0
labels: list[str] = field(default_factory=lambda: list(DAY_LABELS))
def make_state() -> DashboardState:
"""Build the initial dashboard state.
Returns:
A fresh :class:`DashboardState` showing the first week.
"""
return DashboardState()
def _money(value: float) -> str:
"""Format a number as a compact USD string.
Args:
value: The raw monetary amount.
Returns:
The amount formatted with a dollar sign and thousands separators.
"""
return f"${value:,.0f}"
def _delta_pct(current: float, previous: float) -> tuple[str, bool]:
"""Compute a percentage delta and its direction.
Args:
current: The current period's total.
previous: The prior period's total to compare against.
Returns:
A tuple of the formatted percentage string and whether it went up.
"""
if previous == 0.0:
return "+0%", True
change: float = (current - previous) / previous * 100.0
return f"{change:+.1f}%", change >= 0.0
def view(app: App[DashboardState]) -> Widget:
"""Render the dashboard UI from the current state.
Args:
app: The application handle exposing ``state`` and ``set_state``.
Returns:
The widget tree for the current state.
"""
def next_week() -> None:
app.set_state(lambda s: setattr(s, "week", (s.week + 1) % len(WEEKLY_REVENUE)))
week: int = app.state.week
prev: int = (week - 1) % len(WEEKLY_REVENUE)
revenue: list[float] = WEEKLY_REVENUE[week]
visits: list[float] = WEEKLY_VISITS[week]
signups: list[float] = WEEKLY_SIGNUPS[week]
revenue_total: float = sum(revenue)
visits_total: float = sum(visits)
signups_total: float = sum(signups)
revenue_delta, revenue_up = _delta_pct(revenue_total, sum(WEEKLY_REVENUE[prev]))
visits_delta, visits_up = _delta_pct(visits_total, sum(WEEKLY_VISITS[prev]))
signups_delta, signups_up = _delta_pct(signups_total, sum(WEEKLY_SIGNUPS[prev]))
metrics: Row = Row(
style=Style(gap=12.0),
children=[
MetricCard(
key="m-revenue",
label="Revenue",
value=_money(revenue_total),
delta=revenue_delta,
delta_up=revenue_up,
color_scheme="primary",
),
MetricCard(
key="m-visits",
label="Visits",
value=f"{visits_total:,.0f}",
delta=visits_delta,
delta_up=visits_up,
color_scheme="secondary",
),
StatCard(
key="m-signups",
label="Sign-ups",
value=f"{signups_total:,.0f}",
delta=signups_delta,
delta_up=signups_up,
color_scheme="tertiary",
),
],
)
revenue_card: Card = Card(
key="card-revenue",
children=[
Text(content="Daily revenue", key="title-revenue"),
BarChart(
key="chart-revenue",
width=520.0,
height=220.0,
color_scheme="primary",
values=revenue,
labels=app.state.labels,
),
],
)
trends_card: Card = Card(
key="card-trends",
children=[
Text(content="Engagement trends", key="title-trends"),
LineChart(
key="chart-trends",
width=520.0,
height=220.0,
series=[
ChartSeries(points=visits, label="Visits", color_scheme="primary"),
ChartSeries(
points=signups,
label="Sign-ups",
color_scheme="tertiary",
),
],
),
],
)
return Column(
style=Style(gap=16.0, padding=Edge.all(24)),
children=[
Row(
style=Style(gap=12.0),
children=[
Text(content=f"Analytics — Week {week + 1}", key="heading"),
Button(label="Next week", on_click=next_week, key="next-week"),
],
),
metrics,
Row(
style=Style(gap=16.0),
children=[revenue_card, trends_card],
),
],
)
Rodando o exemplo ▶¶
Verificação
Você deve ver dois gráficos e três cartões de métrica. Clique em Next week → o título muda para "Week 2", as barras e linhas se reorganizam, e os deltas recalculam. ✅
Recapitulando¶
- ✅ Guardar só o mínimo no estado (
week) e derivar o resto naview. - ✅ Renderizar gráficos em Canvas com
BarCharteLineChart+ChartSeries. - ✅ Exibir métricas com
MetricCard/StatCard(delta+delta_up). - ✅ Repintar tudo com uma única mutação de estado.
- ✅ Rodar o mesmo
app.pynos dois modos sem alterar uma linha.
Próximos passos
- Adicione um seletor de intervalo (dia/semana/mês) com
SegmentedControl. - Veja o Dashboard app shell para um layout completo.