ADR-0020 Gráficos
Gráficos padrão da empresa — TanStack Charts como SVG renderizado no servidor, por trás de wrappers finos nossos e não dos gráficos do shadcn/ui assentes em Recharts, com uma tabela equivalente para cada gráfico.
- Decisão
- TanStack ChartsConstruir a app
- Estado
- Aceite
- Data
- Âmbito
- Predefinição da empresa para todas as novas apps
Ligações
Contexto
Seção intitulada “Contexto”Todos os produtos mostram números: tendências, comparações, dashboards. As decisões à volta estão tomadas e nem todas apontam no mesmo sentido:
- Componentes: shadcn/ui sobre Base UI, com design tokens como variáveis CSS (ADR-0011). O shadcn/ui traz um componente Chart, mas assenta em Recharts (“We use Recharts under the hood”): a CLI copia
ChartContainer,ChartTooltip,ChartLegende umChartConfigpara o repositório, e os componentes do próprio Recharts compõem-se lá dentro. Ou seja, o caminho mais fácil é o Recharts. Este ADR existe porque escolhemos outro caminho, e temos de construir o que o shadcn/ui nos daria - SSR: o TanStack Start faz streaming de React renderizado no servidor (ADR-0002). Um gráfico deve chegar como markup, não como uma caixa vazia que se preenche depois de o JavaScript medir o contentor
- Dados: TanStack Query por trás dos loaders do Router, lido com
useSuspenseQuerye alimentado por server functionsGET(ADR-0012); a fonte de verdade é o Postgres (ADR-0007) - Tabelas: o TanStack Table (ADR-0018) é o padrão para dados tabulares, que é também o de que um gráfico precisa como alternativa em texto
- Testes: testes de componentes em browser mode e axe em todas as páginas que um percurso E2E visita (ADR-0014)
O que está em aberto: a biblioteca de gráficos, como ela recebe os nossos tokens e quem é dono da base de acessibilidade que nenhuma biblioteca de gráficos dá por si só: uma alternativa em texto com os valores exatos.
Requisitos:
- SVG renderizado no servidor que hidrata no sítio, sem novo fetch e sem saltos de layout depois da hidratação
- Tematizado pelas nossas variáveis CSS. O modo claro e o modo escuro trocam variáveis (ADR-0011); o código dos gráficos não tem valores hexadecimais
- Uma base acessível: um gráfico com nome, acesso por teclado aos pontos, valores exatos acessíveis sem passar o cursor por cima
- Custo de bundle proporcional ao gráfico, não à biblioteca
- Margem para crescer: anotações, small multiples e codificações pouco comuns sem trocar de biblioteca a meio de um produto
Opções consideradas
Seção intitulada “Opções consideradas”Biblioteca de gráficos
Seção intitulada “Biblioteca de gráficos”A. TanStack Charts
Seção intitulada “A. TanStack Charts”O @tanstack/charts é uma gramática de gráficos em TypeScript. Os marks (lineY, barY, dot, text, …) consomem os dados tal como estão, os channels ligam campos a codificações visuais e as escalas são as compactas incluídas ou módulos D3. O motor compila uma cena neutra quanto ao renderer, desenhada por omissão em SVG, ou em Canvas a pedido. O adaptador para React é @tanstack/charts/react. O lema: “A chart grammar you don’t have to outgrow.”
Prós
- Mesmo mantenedor e mesma organização do Router, do Query, do Start e do Form; MIT; 1.1.0 (2026-10-09). Aplica-se um contrato de API estável desde a 1.0: as versões patch corrigem defeitos, as minor acrescentam capacidades compatíveis, e remover ou renomear seja o que for exige uma major
- Renderiza no servidor o SVG completo com
initialWidth, e o cliente adota-o. Verificámos: orenderToStringde umChartdevolve um<svg role="img" aria-roledescription="chart" aria-label="…">com grelha, eixos e marks já lá dentro - O tema é herança: o texto e a grelha usam
currentColor, o fundo é transparente, as cores das séries são variáveis CSS--ts-chart-1…6e o aspeto do tooltip lê variáveis--ts-chart-tooltip-* - As primitivas de acessibilidade estão incluídas:
ariaLabelé obrigatório,ariaDescriptiontorna-se um<desc>do SVG, o SVG recebe foco (tabIndex0), funcionam as setas,Home/End,Enter/SpaceeEscape, e a redução de movimento é respeitada por predefinição - Uma só definição cobre os gráficos habituais e o que vem depois: linhas, áreas, barras, pontos, facets, anotações, polar, treemap, Sankey, geo. Os módulos D3 encaixam onde uma escala precisar
- Tree-shakable (
sideEffects: false, imports por subcaminho exato); o Canvas, o movimento e a exportação ficam fora do bundle até serem importados. O benchmark do próprio TanStack coloca gráficos de linhas, barras, áreas e dispersão em 43–50 kB gzip, do básico ao avançado (um número do fornecedor, não uma medição independente)
Contras
- Jovem: primeira publicação no npm a 2026-07-29, 1.0.0 a 2026-10-03, 1.1.0 a 2026-10-09 — menos de três meses de história, 18 dias depois da 1.0. Cerca de 353 mil downloads por semana (2026-10-12 a 2026-10-18) contra cerca de 33,3 M do Recharts; 847 estrelas no GitHub
- O seu próprio
ACKNOWLEDGEMENTS.mddiz que “was implemented almost entirely with AI coding agents under Tanner Linsley’s direct supervision”. A evidência de qualidade são as suas suites de testes, conformidade e bundle e a nossa própria utilização, não anos em produção - A documentação em tanstack.com acompanha o
mainainda por publicar; a versão publicada é a 1.1.0 - Não faz parte do shadcn/ui: sem componentes de gráficos, sem blocks, sem entradas de registry. O estilo do frame, do tooltip e da legenda é nosso
- Um modelo diferente dos componentes JSX do Recharts (marks, channels, objetos de escala) e muito menos exemplos online
- As alternativas em texto são nossas: a legenda é
aria-hidden, o tooltip é “supplemental” e a documentação manda a aplicação fornecer a alternativa com os valores exatos - As escalas compactas cobrem linear, band, point e ordinal. Escalas de tempo, logarítmicas e de cor sequencial precisam do
d3-scalecomo dependência direta
Verificação de nomes. Circulam três nomes de pacote:
@tanstack/chartsé a biblioteca@tanstack/react-charts1.1.0 é um pacote fino de compatibilidade que depende dela; o seu README diz “New applications use the React adapter from@tanstack/charts/react”react-charts(sem scope) é uma biblioteca anterior do mesmo autor, não esta: a taglatesté a 2.0.0-beta.7, a última publicação foi a 3.0.0-beta.57 a 2023-11-02, e o seu repositório,TanStack/react-charts, está arquivado com “No Longer Maintained” no README
B. Recharts 3.x através dos Charts do shadcn/ui
Seção intitulada “B. Recharts 3.x através dos Charts do shadcn/ui”recharts 3.10.1 (MIT, 2026-07-25), usado como o shadcn/ui o documenta: shadcn add chart copia os wrappers para src/components/ui/, e os gráficos constroem-se com BarChart, XAxis, Bar e companhia. shadcn/ui: “We do not wrap Recharts”; o seu componente de gráficos usa agora o Recharts v3.
Prós
- O caminho que o ADR-0011 já sugere: código copiado e nosso, colorido por tokens
--chart-1…5(color: "var(--chart-1)"noChartConfig), modo escuro através de variáveis e uma biblioteca de blocos de gráficos para copiar - O maior ecossistema: cerca de 33,3 M de downloads por semana; a maioria das respostas, exemplos e sugestões de IA sobre gráficos em React assume-o
- SVG, e o
accessibilityLayertemtruepor predefinição no Recharts 3, pelo que o suporte de teclado e de leitores de ecrã vem incluído - MIT e com versões publicadas ativamente
Contras
- Só React. O dimensionamento responsivo passa pelo
ResponsiveContainer, que observa o elemento pai comResizeObserver(initialDimensiontem por predefinição{ width: -1, height: -1 }); a própria nota v3 do shadcn/ui manda manter uma altura noChartContainer“so ResponsiveContainer can measure on first render”. O guia do Recharts não tem capítulo sobre SSR, pelo que o que uma renderização em streaming no servidor emite fica para nós descobrirmos - Mais pesado: o benchmark do próprio TanStack indica 97–113 kB gzip com o React externalizado, contra 43–50 kB gzip do TanStack Charts (números do fornecedor; apenas indicativos)
- Não nos poupa trabalho: a tabela equivalente, as alternativas em texto e as verificações da paleta abaixo são nossas seja qual for a biblioteca
C. Apache ECharts
Seção intitulada “C. Apache ECharts”echarts 6.1.0 (Apache-2.0, 2026-05-19).
Prós: canvas por predefinição com uma opção de renderer SVG, e o handbook recomenda canvas para dados volumosos (“>1k is an experience value”); renderização SVG em string no servidor desde a 5.3.0 e um runtime leve no cliente para hidratação desde a 5.5.0; o aria.show gera uma descrição do gráfico (o TanStack Charts deixa isso connosco) e os padrões decal distinguem séries sem cor.
Contras: API imperativa echarts.init, com o React servido por um wrapper de terceiros como o echarts-for-react (3.0.6); tematizado com paletas em JavaScript e temas registados, não com variáveis CSS; o aria vem desligado por predefinição e exige importar o AriaComponent; a renderização no servidor exige width e height explícitos; o bundle mais pesado dos candidatos (156,77–177,34 kB gzip no benchmark do TanStack).
D. visx
Seção intitulada “D. visx”@visx/xychart 4.0.0 (MIT, 2026-06-11). “A collection of reusable low-level visualization components” que combinam o D3 com o React.
Prós: controlo total sobre cada gráfico; React 18 e 19; D3 por baixo.
Contras: primitivas, não gráficos — eixos, legendas, tooltips, dimensionamento e acessibilidade são montados por nós em cada gráfico; só React; o @visx/xychart precisa também do @react-spring/web. Cerca de 232 mil downloads por semana.
E. Observable Plot
Seção intitulada “E. Observable Plot”@observablehq/plot 0.6.17 (ISC, 2025-02-14). “Focused on accelerating exploratory data analysis”; a gramática de marks e channels que o TanStack Charts nomeia como a sua maior inspiração de API.
Prós: conciso e expressivo; saída em SVG; um desenho maduro.
Contras: pré-1.0, e a última versão é de 2025-02-14; sem componente React — a documentação dá uma receita (opção document e um DOM virtual para SSR, JSDOM em Node) que seria nossa.
F. Chart.js
Seção intitulada “F. Chart.js”chart.js 4.5.1 (MIT, 2025-10-13), com react-chartjs-2 5.3.1.
Prós: cerca de 7,1 M de downloads por semana; canvas em primeiro lugar, o que serve dados volumosos; MIT.
Contras: desenha para um <canvas>, pelo que o conteúdo do gráfico só existe depois de o cliente o pintar; a sua documentação diz “the canvas content will not be accessible to screen readers” e deixa os nomes e o conteúdo alternativo a cargo da aplicação.
G. Nivo
Seção intitulada “G. Nivo”@nivo/* 0.99.0 (MIT, 2025-05-23).
Prós: componentes React com saída SVG, HTML e canvas; uma API de renderização no servidor, legendas, padrões SVG e gradientes; cerca de 846 mil downloads por semana para o @nivo/core.
Contras: pré-1.0; a última versão no npm é de 2025-05-23, embora o repositório continue a receber commits (último push a 2026-07-21); só React.
Superfície de renderização
Seção intitulada “Superfície de renderização”- SVG por predefinição: renderizado no servidor, estilizado por variáveis CSS, presente na árvore de acessibilidade, o texto continua selecionável. Custo: um nó DOM por mark
- Canvas por mark, só quando medido: para um mark pesado de pintar, como uma dispersão densa. Não pinta nada no servidor — os píxeis aparecem depois da montagem — e a única saída do servidor é a casca
Decisão
Seção intitulada “Decisão”O TanStack Charts é o padrão da empresa para gráficos em todas as novas apps. Não usamos o componente Chart do shadcn/ui: mantemos o seu aspeto (tokens, Card) e trocamos o seu motor, com wrappers finos nossos.
Em concreto:
- Pacotes:
@tanstack/charts1.x, importado por subcaminhos exatos (@tanstack/charts,@tanstack/charts/scales/linear,@tanstack/charts/react,@tanstack/charts/tooltip). Nuncareact-charts, e não@tanstack/react-chartsem novas apps. Acrescentard3-scalee@types/d3-scaleapenas para um gráfico que precise de uma escala de tempo, logarítmica ou de cor sequencial (por exemploscaleUtc); nunca o pacoted3completo - Ler a documentação que vem com a versão instalada. O pacote contém
docs/ellms.txt; o site acompanha omainainda por publicar. Tratar cada atualização minor como qualquer atualização de dependência: ler as notas de lançamento e correr os testes de componentes dos gráficos - Uma tarefa por camada (SOLID: cada uma tem uma só razão para mudar):
- Hook de dados: uma factory de query options mais
useSuspenseQuery, junto da funcionalidade (ADR-0012). Obtém e molda as linhas, e não importa nada da biblioteca de gráficos - Componente de gráfico: renderiza. Recebe
rows(apenas os campos que desenha) e umlabelcomo props, constrói a definição emuseMemoe devolve<Chart>. Nunca faz fetch e nunca toca noQueryClient - Frame:
ChartFrameemsrc/components/chart/, construído sobre oCarddo shadcn/ui. Renderiza um título, uma descrição visível,childrenpara o gráfico e slots para a legenda e para a tabela equivalente. ReencaminhaclassName. As variantes vêm de composição, não de flags booleanas - O contentor passa as mesmas
rowsao gráfico e à sua tabela equivalente, pelo que não podem divergir
- Hook de dados: uma factory de query options mais
- Os dados vêm do Query e são agregados antes de viajar. A server function devolve as linhas que o gráfico desenha, agrupadas ou somadas na base de dados, nunca milhares de linhas em bruto. As linhas são JSON simples (números e strings ISO), porque o structural sharing do Query precisa de dados compatíveis com JSON; o componente de gráfico cria os valores
Datedentro do seuuseMemo. O structural sharing mantém entãodataestável por referência numa atualização que não mudou nada, pelo que a definição só é reconstruída quando os dados mudam de facto - Regras de SSR: dar sempre ao servidor um tamanho (
initialWidthmaisheight, ouaspectRatio). Nada numa definição lêwindow, o layout, o relógio nem valores aleatórios. Passar os formatadores sensíveis ao idioma de forma explícita, com o mesmo idioma no servidor e no cliente. Nunca alternar o gráfico comtypeof window, e nunca o envolver num componente só de cliente a não ser que um gráfico não consiga mesmo renderizar no servidor (nesse caso, reservar-lhe a altura). A primeira app acrescenta um teste de fumo de SSR do Start: a resposta do servidor contém o<svg role="img">do gráfico e o seuaria-label - Tematização pelos nossos tokens: o
ChartFramedefine--ts-chart-1…6comovar(--chart-1)… e--ts-chart-tooltip-background,-color,-bordere-border-radiuscomovar(--popover),var(--popover-foreground),1px solid var(--border)evar(--radius)na sua própria raiz. Definidos aí, resolvem-se contra o tema mais próximo (claro, escuro ou de marca de um produto); em:root, congelariam os valores do tema da raiz para qualquer contentor.darkaninhado. O texto e a grelha mantêm ocurrentColorpredefinido. Sem valores hexadecimais no código dos gráficos - A cor da série segue a entidade, não a sua posição. Dar a todos os gráficos com várias séries uma escala ordinal explícita com domínio fixo (
scaleOrdinal().domain([…]).range(["var(--chart-1)", …])), para que filtrar nunca volte a pintar as séries que restam. Verificámos que as stringsvar(--chart-N)chegam ao SVG comostrokeefill. Uma nona série dobra-se em “Outros” ou passa a small multiple - Base de acessibilidade:
ariaLabelem todos os gráficos, a dizer o que mostram (“Receita mensal, 2026”), com a conclusão no título visível e não no nome acessível.ariaDescriptionquando as unidades ou o período não estiverem visíveis ali perto. Manterkeyboardligado nos gráficos interativos, manter estilos de foco visíveis e manterrespectReducedMotionna predefinição. Um tooltip é complementar: nada de essencial vive só nele. Os testes de componentes renderizam cada gráfico com linhas fixas e verificam o seu nome acessível e que a tabela equivalente corresponde às linhas; o axe já corre em todas as páginas E2E (ADR-0014) - Regras da casa para gráficos (o método de dataviz que o nosso próprio site de documentação segue):
- Validar as paletas categóricas para deficiência na perceção das cores (daltonismo), em claro e escuro, contra a superfície real: séries vizinhas com pelo menos ΔE 8 de distância (OKLab ×100) sob simulação de protanopia e deuteranopia, e marks com pelo menos 3:1 contra a superfície, a não ser que os valores sejam legíveis de outra forma (etiquetas ou a tabela). Não assumimos que os
--chart-1…5predefinidos do shadcn/ui passam; correr a verificação - Uma série não tem legenda — o título dá-lhe o nome. Duas ou mais séries têm legenda mais etiquetas diretas (
colorLegend, e um marktextnas pontas das linhas ou das barras), nunca um número em cada ponto - O texto nunca usa a cor da série. Um mark
textcom série (z) apanha a cor da série por predefinição (verificámos:fill="var(--chart-1)"), pelo que todos os markstextdefinemfill: "currentColor". As etiquetas da legenda mantêm a tinta predefinida; nunca usar o padrãolabel.fill: (_series, { color }) => colorda documentação das legendas. A cor vive na amostra e no mark - Todos os gráficos têm uma tabela equivalente ou uma alternativa em texto equivalente. O padrão é uma
<table>semântica com legenda, alcançável por um controlo visível junto ao gráfico, nunca escondida de toda a gente. Para poucas linhas basta uma<table>simples; usar o TanStack Table (ADR-0018) quando a tabela precisar de ordenação ou paginação. Um sparkline ao lado de um número pode usar esse número como alternativa - Nunca eixos y duplos. Duas medidas em escalas diferentes passam a dois gráficos alinhados ou são indexadas a uma base comum. O guia do próprio TanStack diz que eixos quantitativos duplos “often make unrelated movement look related”
- A cor nunca é o único sinal: associá-la a etiquetas diretas, posição ou ordem
- Validar as paletas categóricas para deficiência na perceção das cores (daltonismo), em claro e escuro, contra a superfície real: séries vizinhas com pelo menos ΔE 8 de distância (OKLab ×100) sob simulação de protanopia e deuteranopia, e marks com pelo menos 3:1 contra a superfície, a não ser que os valores sejam legíveis de outra forma (etiquetas ou a tabela). Não assumimos que os
- SVG por predefinição; Canvas por mark apenas para um mark cujo custo medimos, importado de
@tanstack/charts/canvas. Os píxeis do Canvas não existem no servidor, pelo que um mark Canvas denso aparece depois da hidratação — manter marks SVG e eixos à volta dele - O que não instalamos:
recharts,shadcn add chart,react-charts,@tanstack/react-charts. Um produto que precise do que esta gramática não consegue exprimir, ou de dados demasiado densos para SVG e Canvas aqui, escreve o seu próprio ADR de exceção (Como decidimos)
Os fatores decisivos: os mesmos mantenedores e o mesmo ciclo de lançamentos do Router, do Query, do Start e do Form; SVG renderizado no servidor que recebe os nossos tokens por herança; primitivas de acessibilidade incluídas; e uma só gramática que cresce com o produto em vez de forçar a troca de biblioteca. Aceitamos uma biblioteca com menos de três meses, a perda dos componentes e dos blocos de gráficos do shadcn/ui e a posse dos wrappers.
Consequências
Seção intitulada “Consequências”Mais fácil:
- Os gráficos chegam como SVG completo e acessível na resposta do servidor e hidratam no sítio, sem moldura vazia e sem o salto de medir-e-depois-desenhar
- As mudanças de tema são edições de variáveis: a paleta e o aspeto do tooltip seguem os tokens do ADR-0011, e o modo escuro não exige reconstruir gráficos
- Os dados fluem num só sentido: Query → linhas → gráfico e tabela equivalente (ADR-0012, ADR-0018). Um componente de gráfico pode ser renderizado num teste com linhas fixas e sem rede
- O custo de bundle acompanha os marks que um gráfico usa; o Canvas, o movimento e a exportação não custam nada até serem importados
- Uma só biblioteca para o gráfico habitual e para o invulgar, e a mesma equipa do resto da nossa stack TanStack
Mais difícil:
- Os componentes, blocos e exemplos de gráficos do shadcn/ui assumem o Recharts. Adaptá-los à mão; o mesmo vale para muitas respostas do Stack Overflow e sugestões de IA. O pacote traz
llms.txte a sua documentação para ajudar - O
src/components/chart/é nosso para construir e manter: frame, tabela equivalente, mapeamento de tokens. Tal como os campos de formulário do ADR-0013, é o primeiro candidato para o registry partilhado do shadcn/ui (ADR-0011) quando dois produtos o duplicarem - A biblioteca é jovem: 1.1.0, menos de três meses no npm, uma comunidade pequena e, segundo os seus próprios agradecimentos, uma base de código escrita quase toda por agentes de IA sob a supervisão do mantenedor. Rever cada versão minor, ler as notas de lançamento, reportar problemas upstream e deixar os testes de componentes apanhar regressões
- A base de acessibilidade é nossa para impor em revisão e em testes: nomes, descrições, a tabela equivalente e a verificação de cores não vêm da biblioteca
- Servidor e cliente têm de produzir markup idêntico: uma definição que leia o relógio, o idioma ou o
windowcausa uma discrepância de hidratação. Rever isso, e memorizar as definições em função dos seus dados - Os eixos de tempo trazem o
d3-scalecomo dependência direta, com os seus tipos - O site acompanha o
main, não a versão publicada; a documentação dentro do pacote instalado é a referência
Do que abdicamos: dos componentes e dos blocos de gráficos do shadcn/ui, do ecossistema do Recharts (cerca de 33,3 M de downloads semanais contra cerca de 353 mil) e do historial de uma biblioteca com anos em produção. Aceite — SVG renderizado no servidor e tematizado pelos nossos tokens, primitivas de acessibilidade incluídas e uma só gramática que cresce pesam mais numa predefinição da empresa. Escrever um ADR que o substitua se a biblioteca estagnar, se uma versão 1.x quebrar o seu contrato de compatibilidade ou se os gráficos do shadcn/ui deixarem o Recharts.

