Pular para o conteúdo

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

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, ChartLegend e um ChartConfig para 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 useSuspenseQuery e alimentado por server functions GET (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

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: o renderToString de um Chart devolve 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…6 e o aspeto do tooltip lê variáveis --ts-chart-tooltip-*
  • As primitivas de acessibilidade estão incluídas: ariaLabel é obrigatório, ariaDescription torna-se um <desc> do SVG, o SVG recebe foco (tabIndex 0), funcionam as setas, Home/End, Enter/Space e Escape, 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.md diz 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 main ainda 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-scale como dependência direta

Verificação de nomes. Circulam três nomes de pacote:

  • @tanstack/charts é a biblioteca
  • @tanstack/react-charts 1.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 tag latest é 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

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)" no ChartConfig), 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 accessibilityLayer tem true por 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 com ResizeObserver (initialDimension tem por predefinição { width: -1, height: -1 }); a própria nota v3 do shadcn/ui manda manter uma altura no ChartContainer “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

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).

@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.

@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.

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.

@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.

  • 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

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/charts 1.x, importado por subcaminhos exatos (@tanstack/charts, @tanstack/charts/scales/linear, @tanstack/charts/react, @tanstack/charts/tooltip). Nunca react-charts, e não @tanstack/react-charts em novas apps. Acrescentar d3-scale e @types/d3-scale apenas para um gráfico que precise de uma escala de tempo, logarítmica ou de cor sequencial (por exemplo scaleUtc); nunca o pacote d3 completo
  • Ler a documentação que vem com a versão instalada. O pacote contém docs/ e llms.txt; o site acompanha o main ainda 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 um label como props, constrói a definição em useMemo e devolve <Chart>. Nunca faz fetch e nunca toca no QueryClient
    • Frame: ChartFrame em src/components/chart/, construído sobre o Card do shadcn/ui. Renderiza um título, uma descrição visível, children para o gráfico e slots para a legenda e para a tabela equivalente. Reencaminha className. As variantes vêm de composição, não de flags booleanas
    • O contentor passa as mesmas rows ao gráfico e à sua tabela equivalente, pelo que não podem divergir
  • 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 Date dentro do seu useMemo. O structural sharing mantém então data está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 (initialWidth mais height, ou aspectRatio). 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 com typeof 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 seu aria-label
  • Tematização pelos nossos tokens: o ChartFrame define --ts-chart-1…6 como var(--chart-1)… e --ts-chart-tooltip-background, -color, -border e -border-radius como var(--popover), var(--popover-foreground), 1px solid var(--border) e var(--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 .dark aninhado. O texto e a grelha mantêm o currentColor predefinido. 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 strings var(--chart-N) chegam ao SVG como stroke e fill. Uma nona série dobra-se em “Outros” ou passa a small multiple
  • Base de acessibilidade: ariaLabel em 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. ariaDescription quando as unidades ou o período não estiverem visíveis ali perto. Manter keyboard ligado nos gráficos interativos, manter estilos de foco visíveis e manter respectReducedMotion na 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…5 predefinidos 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 mark text nas pontas das linhas ou das barras), nunca um número em cada ponto
    • O texto nunca usa a cor da série. Um mark text com série (z) apanha a cor da série por predefinição (verificámos: fill="var(--chart-1)"), pelo que todos os marks text definem fill: "currentColor". As etiquetas da legenda mantêm a tinta predefinida; nunca usar o padrão label.fill: (_series, { color }) => color da 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
  • 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.

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.txt e 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 window causa 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-scale como 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.