Material 7 · Design para páginas e campanhas

Dashboards: painéis densos que um gestor lê em uma tela

Um painel bom cabe numa tela e responde três perguntas em cinco segundos: o que mudou, onde e o que fazer. Este guia reúne a ciência de dados que escolhe o gráfico certo para cada pergunta, as escalas que não mentem, o estilo que deixa dezenas de números legíveis quando empilhados, sobrepostos ou em matriz, o que o Power BI ensina sobre dashboards e relatórios, a stack de 2026 para construir o mesmo na web (Apache ECharts 6.1, Observable Plot, Vega-Lite, number-flow, Motion, carrossel nativo com scroll-snap e Embla quando crescer) e quatro oficinas para praticar. Comece pelo painel abaixo: ele é real, roda nesta página e cada decisão dele está explicada nas seções seguintes.

Referências consultadas em 3 de novembro de 2026. Os exemplos acompanham um caso fictício, a agência Mirante e seus 12 clientes; nenhum número representa dados reais da Leadlovers ou de clientes reais, e nada aqui descreve telas ou funcionalidades oficiais da plataforma.

Painel de operação da MiranteOutubro de 2026 contra setembro e contra a meta · atualizado em 1 de novembro de 2026 · fonte: planilha de operação (exercício) · dona do número: Helena, operações

A operação em outubro: cancelamentos (churn) ficou 0,6 p.p. acima da meta; leads captados superou a meta de outubro de 2026 em 4,8%.

Os gráficos abaixo são SVG estático com tabela equivalente. O botão carrega o Apache ECharts 6.1.0 do vendor do portal (1,1 MB) só se você pedir; os filtros e o período ficam na URL desta página.

Leads captados4.612+10,3% sobre setMeta 4.400 (+4,8%)
Custo por leadR$ 35,10-8,6% sobre setMeta R$ 36,00 (-2,5%)
Conversão lead → reunião10,9%-0,9 p.p. sobre setMeta 12,0% (-1,1 p.p.)
Reuniões realizadas503+2,0% sobre setMeta 528 (-4,7%)
Receita recorrente (MRR)R$ 428.900+4,0% sobre setMeta R$ 435.000 (-1,4%)
Cancelamentos (churn)3,1%+0,5 p.p. sobre setMeta 2,5% (+0,6 p.p.)
NPS47-4 pts sobre setMeta 55 (-8 pts)
Tempo de resposta ao lead1,9 h-20,8% sobre setMeta 2,0 h (-5,0%)
Campanhas ativas91+5,8% sobre setSem meta definida

Outubro passou da meta semanal a partir da segunda semana

Leads por semana · outubro contra setembro · meta de 880 por semana

Leads por semana: outubro contra setembro e a meta de 880Outubro fica acima de setembro nas cinco semanas e cruza a meta de 880 leads a partir da semana 2, com pico de 1.004 na semana 3. Setembro só passa da meta na semana 4. Valores na tabela abaixo.02755508251.100Meta 8808787801.004S1S2S3S4S5OutubroSetembro
Ver como tabela
Leads por semana (unidade: leads; semanas de outubro e setembro de 2026)
SemanaSetembroOutubroMeta
S1790860880
S2835912880
S38701.004880
S4905958880
S5780878880

Da visita ao fechamento: a conversão de lead para reunião é a etapa que caiu

Funil de outubro · escala logarítmica para manter as etapas pequenas legíveis

Funil de outubro em escala logarítmica, de 48.200 visitas a 61 fechamentosCada etapa mostra o total e a taxa sobre a etapa anterior: leads 9,6% das visitas, reuniões 10,9% dos leads, propostas 42,1% das reuniões, fechamentos 28,8% das propostas. A escala logarítmica mantém as etapas pequenas visíveis; a tabela traz os valores.Visitas48.200Leads4.612 (9,6% da anterior)Reuniões503 (10,9% da anterior)Propostas212 (42,1% da anterior)Fechamentos61 (28,8% da anterior)Escala logarítmica (base 10): cada passo à direita multiplica por 10.
Ver como tabela
Funil de outubro de 2026 (unidade: pessoas ou registros por etapa)
EtapaTotalSobre a anterior
Visitas48.200100%
Leads4.6129,6%
Reuniões50310,9%
Propostas21242,1%
Fechamentos6128,8%

Três clientes caíram sobre setembro; Studio Prisma pede a primeira conversa da semana

Leads de outubro por cliente, do maior para o menor · cor de alerta só em quem caiu · o carrossel na seção Laboratório destaca um cliente aqui

Leads de outubro por cliente, do maior para o menorPadaria Horizonte lidera com 612 leads e Café Ladeira fecha com 255. Três clientes caíram sobre setembro e aparecem em cor de alerta: Loja Marés (menos 7%), Imobiliária Lume (menos 3%) e Studio Fotográfico Prisma (menos 11%). Valores na tabela abaixo.Padaria Horizonte612 (+9%)Clínica Vale Verde540 (+4%)Loja Marés498 (-7%)Escola Compasso431 (+12%)Academia Pulso402 (+2%)Imobiliária Lume377 (-3%)Oficina Trilho341 (+6%)Pet Serena318 (+1%)Cursos Bússola296 (+15%)Studio Fotográfico Prisma274 (-11%)Dental Aurora268 (+5%)Café Ladeira255 (0%)
Ver como tabela
Leads de outubro de 2026 por cliente e variação sobre setembro (unidade: leads e %)
ClienteLeadsSobre set
Padaria Horizonte612+9%
Clínica Vale Verde540+4%
Loja Marés498-7%
Escola Compasso431+12%
Academia Pulso402+2%
Imobiliária Lume377-3%
Oficina Trilho341+6%
Pet Serena318+1%
Cursos Bússola296+15%
Studio Fotográfico Prisma274-11%
Dental Aurora268+5%
Café Ladeira2550%

Fonte: planilha de operação da Mirante, exercício didático; definição de lead: formulário enviado e validado por telefone; meta aprovada em 29 de setembro de 2026. Dados fictícios.

Escreva a decisão que a tela alimenta antes de escolher o primeiro gráfico

Um painel existe para que alguém decida alguma coisa em pouco tempo. Quando essa frase falta, a tela cresce por acúmulo: cada reunião pede mais um gráfico, ninguém retira o anterior, e o resultado é uma página que mostra tudo e não responde nada. Na agência Mirante (nome fictício, exemplo didático que atravessa este guia), o painel antigo de operação tinha 23 gráficos distribuídos em quatro telas de rolagem, com velocímetros para o NPS, uma pizza de nove fatias para os canais de captação, um gráfico de eixo duplo que cruzava leads e receita, e cores por cliente que mudavam a cada filtro aplicado. Helena, diretora de operações, levava cerca de 40 minutos toda segunda-feira para extrair dali três decisões: em qual cliente intervir, qual canal reforçar e o que levar para a reunião de diretoria.

O painel novo, construído por Otávio (desenvolvedor que trabalha com agente de código) e por Camila (designer), cabe numa tela de 1.280 px e responde em cinco segundos a uma pergunta de três partes: o que mudou, onde mudou e o que fazer a respeito. O detalhe por cliente ficou num carrossel abaixo da primeira tela, e o restante virou páginas de consulta ligadas por um clique. A diferença entre as duas versões não está na quantidade de dados, que é a mesma, e sim na pergunta que organizou cada uma. Esta seção ensina a formular essa pergunta e a classificar o painel antes de abrir o editor.

A frase que todo painel precisa ter escrita no topo do arquivo

O guia de Gráficos desta coleção já pede uma frase antes de qualquer biblioteca: qual decisão a tela alimenta e quem decide. Em dashboards a exigência dobra de peso, porque uma tela com dez figuras multiplica por dez o custo de uma pergunta mal feita. A frase tem três partes fixas. Quem decide: um cargo, uma pessoa, com o ritmo em que ela olha a tela (toda segunda, a cada hora, só no fechamento do mês). O que decide: um verbo de ação, como realocar verba, acionar um cliente, aprovar um plano. Com que informação: os poucos números sem os quais a decisão não acontece.

Na Mirante, a frase ficou assim: "Helena, toda segunda-feira às 9h, decide em quais dos 12 clientes a equipe vai intervir nesta semana e qual canal recebe verba extra, a partir de leads, custo por lead, conversão em reunião, receita recorrente, cancelamentos e NPS, comparados com setembro e com a meta de outubro." Com a frase escrita, 14 dos 23 gráficos antigos perderam o lugar na primeira tela: não alimentavam nenhuma das duas decisões. Nenhum foi apagado; foram para a página de consulta, acessível por um clique, onde continuam úteis para outra pessoa com outra pergunta.

Stephen Few definiu o objeto em 2004, e a definição continua a mais exigente

Em "Dashboard Confusion", publicado na Intelligent Enterprise em 20 de março de 2004, Stephen Few definiu o dashboard como uma apresentação visual da informação mais importante para atingir um ou mais objetivos, consolidada e organizada numa única tela, de modo que possa ser monitorada de relance (a citação chega por fontes secundárias; o artigo original saiu de circulação). Cada trecho da frase corta uma prática comum. "Mais importante" exclui o gráfico que está ali por via das dúvidas. "Um ou mais objetivos" exige a frase da decisão. "Única tela" proíbe a rolagem como estrutura principal. "De relance" fixa o tempo de leitura em segundos em vez de minutos.

Few desenvolveu o critério em Information Dashboard Design (O'Reilly, 2006), onde também propôs o gráfico de marcador (bullet graph) para substituir os velocímetros que ocupavam espaço sem transmitir mais do que um número. O velocímetro do NPS da Mirante, que mostrava 47 pontos numa meia-lua colorida de 220 px de altura, virou uma linha de 24 px com o valor, a meta de 55 e o resultado de setembro, 51. A seção Estilos e notação detalha a construção; aqui basta reter o princípio: a tela única obriga a trocar ornamento por informação.

Três famílias de tela, e a que a Mirante precisava

O nome "dashboard" cobre três objetos diferentes, e misturá-los numa tela só é a origem da maior parte dos painéis ruins. O relatório de leitura tem poucas figuras, uma conclusão escrita, comparação com meta e uma recomendação; quase não tem interação e circula por e-mail ou PDF. O painel de monitoramento é a tela única de Few: indicadores que a pessoa olha num ritmo fixo para detectar desvio e agir. O painel de exploração tem filtros, detalhamento (drill, a passagem do agregado para o detalhe) e múltiplas visões; serve a quem investiga uma causa, enquanto quem decide em cinco segundos precisa de outra tela.

Cada família atende a uma pergunta e a um ritmo de leitura; a Mirante precisava de um painel de monitoramento com um carrossel de exploração logo abaixo
FamíliaPara quem e em que ritmoInteraçãoNa MiranteEvite
Relatório de leituraDiretoria, uma vez por mês.Quase nenhuma; a página se lê de cima para baixo.O fechamento de outubro: cinco figuras, uma conclusão por figura, meta e recomendação.Filtros que mudam a conclusão escrita sem reescrevê-la.
Painel de monitoramentoHelena, toda segunda às 9h.Período e comparação na URL; um clique para o detalhe.A primeira tela: manchete, nove indicadores, variação contra meta e contra setembro.Rolagem como estrutura; gráfico sem meta nem referência.
Painel de exploraçãoGestores de conta, durante a semana.Filtros, detalhamento, troca de visão, exportação.O carrossel de 12 clientes e a página de consulta com os 14 gráficos retirados.Colocar os filtros de exploração na primeira tela de quem só decide.

O erro do painel antigo da Mirante foi tentar ser as três coisas ao mesmo tempo: a primeira tela trazia seletores de cliente, canal e período que Helena nunca usava, e cada troca de filtro recolorindo as séries fazia com que o azul que ela tinha aprendido como "Padaria Horizonte" virasse "Clínica Vale Verde" na tela seguinte. O guia de Navegação trata do lugar dos filtros na URL; a regra que importa aqui é separar as famílias por tela em vez de por abas dentro da mesma tela.

O que o Microsoft Learn chama de dashboard e de relatório

O Power BI, referência para boa parte das equipes que chegam a este guia, separa os dois objetos de forma parecida. Na página "Dashboards for designers" do Microsoft Learn, atualizada em 23 de julho de 2026, a definição é direta: um dashboard do Power BI é uma página única, chamada de canvas, que conta uma história por meio de visualizações; por estar limitado a uma página, um dashboard bem projetado contém só os destaques dessa história. As visualizações se chamam blocos (tiles) e são fixadas (pin) a partir de relatórios. A mesma página traz a tabela de diferenças, reproduzida abaixo com as linhas que interessam a quem desenha para a web.

Dashboard e relatório no Power BI, segundo a página "Dashboards for designers" do Microsoft Learn (23 de julho de 2026)
CapacidadeDashboardRelatório
PáginasUma página.Uma ou mais páginas.
Fontes de dadosUm ou mais relatórios e um ou mais modelos semânticos.Um único modelo semântico por relatório.
Detalhamento nos visuaisSó quando a página inteira do relatório é fixada.Sim.
Filtrar e fatiarNão; a página diz que não se filtra nem se fatia um dashboard.Sim.
AlertasSim, em blocos de dashboard, em certas condições.Não.
Disponível no Power BI DesktopNão; só no serviço.Sim.

A tabela ensina duas coisas a quem constrói em HTML. A primeira é que a tela de monitoramento não precisa de filtro: o Power BI chegou a proibi-lo no objeto que chama de dashboard, e a Mirante retirou os seletores da primeira tela sem perder nada. A segunda é que o alerta pertence ao monitoramento, e a exploração pertence ao relatório; o detalhe por cliente, com filtros e detalhamento, fica na camada seguinte. A seção O que o Power BI ensina volta a esse ponto com os recursos de 2026.

O teste dos cinco segundos, feito antes de desenhar

O teste é simples e cabe numa reunião de 15 minutos. Mostre a primeira tela a uma pessoa do público real por cinco segundos, esconda e peça que responda três perguntas: o que mudou, onde e o que ela faria. A resposta certa vem da manchete e da linha de indicadores; se a pessoa precisou de um gráfico específico ou de um filtro, a tela ainda está na família errada. Na Mirante, o painel antigo passou por cinco pessoas: nenhuma respondeu a terceira pergunta, e duas responderam "o que mudou" com um número que estava fora da tela, lembrado da reunião anterior. O painel novo, na mesma rodada, teve quatro respostas completas em cinco.

A justificativa do limite de tempo vem da psicologia da percepção. Franconeri, Padilla, Shah, Zacks e Hullman, em "The Science of Visual Data Communication: What Works" (Psychological Science in the Public Interest, dezembro de 2021), mostram que o sistema visual extrai estatísticas globais de uma tela, como médias e medianas, em fração de segundo, mas que comparar subconjuntos de valores é lento e exige atenção dirigida. A primeira tela deve, portanto, entregar o global em um relance e deixar a comparação de subconjuntos para a camada seguinte, onde a pessoa já sabe o que procura.

O inventário em texto que precede o código

Com a frase da decisão e a família escolhidas, o passo seguinte é uma lista em texto: cada pergunta que a pessoa faz, o número que a responde, a comparação que dá sentido ao número e o gráfico (ou tabela, ou cartão) que a carrega. O inventário é o contrato que o agente de código recebe; sem ele, o pedido "faça um dashboard de marketing" devolve a tela de 23 gráficos de novo, agora gerada em dois minutos. O guia de React usa a mesma disciplina para o portal denso: estrutura aprovada em texto, visual depois.

Inventário da primeira tela da Mirante: a pergunta de Helena, o número, a comparação e o elemento que o carrega (dados fictícios de outubro de 2026)
Pergunta de HelenaNúmeroComparaçãoElemento
A operação cresceu?4.612 leads em outubro.+10,3% sobre setembro (4.180); 4,8% acima da meta (4.400).Manchete em texto e cartão com variação.
Estamos pagando mais caro por lead?CPL de R$ 35,10.R$ 3,30 abaixo de setembro; R$ 0,90 abaixo da meta.Cartão com variação e linha de cinco semanas.
Os leads viram reunião?Conversão de 10,9%.0,9 p.p. abaixo de setembro; 1,1 p.p. abaixo da meta.Cartão com a variação em p.p. e o denominador (4.612 leads).
A receita acompanha?MRR de R$ 428.900.Meta de R$ 435.000: faltam R$ 6.100.Gráfico de marcador (bullet) contra a meta.
Estamos perdendo clientes?Churn de 3,1% da base.Maior valor desde abril; meta de 2,5%.Linha de sete meses com a meta tracejada.
Em qual cliente intervir?Studio Prisma (-11%) e Loja Marés (-7%).Variação de leads sobre setembro, 12 clientes.Barra ordenada com os dois piores em destaque; carrossel para o detalhe.
Qual canal reforçar?Instagram com 1.845 leads (40%).Participação de cada canal no mês.Barra horizontal ordenada, cinco canais.
A equipe responde rápido?Mediana de 1,9 hora.Meta de 2,0 horas; setembro 2,4.Cartão com variação.
Os clientes recomendam?NPS 47.Setembro 51; meta 55.Gráfico de marcador, e não velocímetro.

Nove perguntas, nove elementos, e cada um com a comparação que lhe dá sentido. A coluna "Comparação" é a que mais trabalho dá e a que mais vale: um número sem referência obriga Helena a lembrar o valor anterior, e a memória de trabalho, segundo Franconeri e colegas em 2021, é o recurso mais escasso de quem lê uma tela densa. As seções seguintes constroem cada elemento: a anatomia da tela dá a posição e o tamanho; pergunta e gráfico escolhe a forma; escalas e eixos garante que a comparação seja lida sem distorção.

Monte a primeira tela para ser lida de cima para baixo e da esquerda para a direita, em cinco segundos

A primeira tela de um painel de monitoramento tem quatro camadas fixas, sempre na mesma ordem: a manchete do dado, a linha de indicadores com variação, a figura que explica o desvio principal e a entrada para o detalhe. Na agência Mirante (fictícia, exemplo didático), Camila desenhou essas camadas em papel antes de abrir o editor, com os números de outubro de 2026 escritos à mão em cada caixa. O desenho a lápis já reprovou duas ideias: o velocímetro do NPS, que ocupava a área de três cartões, e a pizza de canais, que não cabia em nenhuma camada sem empurrar o resto para baixo da dobra.

Esta seção fixa a posição, o tamanho e a hierarquia de cada camada, com os números que a sustentam. A escolha do gráfico em cada caixa fica para a seção pergunta e gráfico; aqui a pergunta é onde cada coisa fica e quanto espaço recebe.

A manchete diz o que mudou; o número grande diz quanto

A manchete é uma frase em texto corrido, escrita a partir do dado, diferente de um título de seção. "Outubro de 2026 fechou com 4.612 leads, 10,3% acima de setembro e 4,8% acima da meta; a conversão em reunião caiu para 10,9% e os cancelamentos subiram para 3,1%, o maior valor desde abril." Quem lê só essa frase já responde às duas primeiras perguntas do teste dos cinco segundos. Num painel que atualiza sozinho, a manchete é calculada a partir dos valores: o guia de Gráficos explica a regra do título que afirma a conclusão; aqui ela vira um parágrafo gerado, com o número que mais se afastou da meta em primeiro lugar.

Abaixo da manchete, um único número grande (o "número herói", a figura que a tela lidera) com 48 px ou mais, na mesma fonte sem serifa de todo o resto. A skill dataviz, usada na revisão deste guia, fixa exatamente um número herói por tela e proíbe fonte de exibição ou serifa nele, porque o número vira decoração. Na Mirante, o número herói é a receita recorrente, R$ 428.900, porque é a medida que a diretoria cobra; leads, apesar de maiores em volume, ficam na linha de cartões. Um número herói usa algarismos proporcionais (a largura natural de cada dígito); algarismos tabulares, em que todo dígito tem a largura do zero, só entram em colunas de tabela e em eixos, onde os valores precisam alinhar na vertical.

Nove a onze indicadores, cada um com denominador e duas comparações

A linha de indicadores reúne de nove a onze cartões, o limite que o guia de Gráficos desta coleção fixou para um painel de uma página. Menos do que isso costuma esconder uma dimensão da operação; mais do que isso derruba a leitura de relance, porque a pessoa passa a varrer em vez de reconhecer. Cada cartão tem quatro partes: o rótulo em frase ("Conversão lead para reunião", sem dois-pontos e sem caixa alta), o valor, a variação contra o período anterior e a variação contra a meta, com a direção marcada por sinal e por palavra, além da cor. Percentual sem denominador não entra: "10,9% de 4.612 leads em outubro de 2026" é a forma completa, porque uma taxa alta sobre uma base pequena engana.

O cartão de conversão da Mirante mostra como a regra evita um erro comum. A queda de 11,8% para 10,9%, de setembro para outubro de 2026, é de 0,9 ponto percentual (p.p., a diferença entre dois percentuais), e não de 0,9%; escrita como "-7,6%" (a variação relativa) pareceria uma queda maior do que a de setembro para outubro nos cancelamentos, de 2,6% para 3,1%, que em termos relativos é de 19,2%. O cartão mostra a diferença em p.p. e deixa a variação relativa para a dica ao passar o ponteiro. A seção Escalas e eixos volta a essa distinção.

Leads captados em outubro4.612+10,3% sobre setembro (4.180); meta 4.400 atingida
Conversão lead para reunião10,9%-0,9 p.p. sobre setembro; 1,1 p.p. abaixo da meta de 12,0%; base de 4.612 leads
Cancelamentos (churn) da base3,1%+0,5 p.p. sobre setembro; meta 2,5%; maior valor desde abril

O canto superior esquerdo recebe o que decide; o resto recebe o que explica

A página "Tips and tricks for creating reports" do Microsoft Learn resume em duas frases o que a pesquisa de leitura de telas repete há décadas: "Put the most important information in the upper corner" e "Make the most important information biggest". Em interfaces lidas da esquerda para a direita, a varredura começa no canto superior esquerdo e perde atenção à medida que desce; o Nielsen Norman Group documenta esse padrão desde o rastreamento ocular de 2006, repetido em 2017, com leitores que percorrem a tela em forma de F e deixam o canto inferior direito por último. A conclusão para o painel é mecânica: a manchete e o número herói ocupam o canto superior esquerdo; a linha de cartões segue logo abaixo; a figura do desvio principal vem à direita ou abaixo; a entrada para o detalhe fecha a tela.

O erro oposto é mais raro e também custa: espalhar os indicadores em ordem alfabética ou na ordem em que o banco os devolve. A Mirante ordenou os nove cartões pela distância da meta, com os dois que mais se afastaram (cancelamentos e conversão) nas primeiras posições da linha, e manteve essa ordem fixa entre semanas, porque a posição aprendida também é informação: Helena sabe que o terceiro cartão é a conversão sem precisar ler o rótulo.

Grade de doze colunas e as proporções que se repetem

A primeira tela usa uma grade de 12 colunas, a mesma que o guia de Tailwind recomenda para contêineres de 1.200 px, com calha de 16 px e espaçamento base de 4 px. A manchete ocupa 12 colunas; o número herói ocupa 4 e divide a linha com a figura do desvio principal, de 8; os cartões ocupam 4 colunas cada em três linhas de três (nove cartões) ou 3 colunas em linhas de quatro (até 12). A 390 px a grade colapsa em uma coluna, os cartões viram uma lista rolável e a figura perde a legenda em favor do rótulo direto; a seção Celular detalha o colapso.

O mesmo desenho, com as alturas em pixels, aparece na figura abaixo. Os retângulos vazios representam as áreas, e o texto dentro de cada um diz o que a área carrega e quantas colunas ocupa.

Esqueleto da primeira tela do painel da Mirante em uma grade de 12 colunas Uma manchete de 12 colunas no topo; abaixo, à esquerda, o número herói de 4 colunas com a receita recorrente, e à direita a figura do desvio principal de 8 colunas com a linha de cancelamentos de abril a outubro contra a meta; na terceira faixa, nove cartões de indicador em três linhas de três, cada um com 4 colunas; na base, uma faixa de 12 colunas que abre o carrossel de 12 clientes. A tabela abaixo lista cada área, a largura em colunas e a altura em pixels. Manchete: outubro fechou com 4.612 leads, +10,3% sobre setembro; conversão caiu para 10,9% 12 col Receita recorrente (MRR) R$ 428.900 faltam R$ 6.100 para a meta · 4 col Cancelamentos, abril a outubro, % da base · 8 col 2 3 3,1% meta 2,5% Cancelamentos 3,1%+0,5 p.p. · meta 2,5% · 4 col Conversão 10,9%-0,9 p.p. · meta 12,0% NPS 47-4 pontos · meta 55 Leads 4.612+10,3% · meta 4.400 Custo por lead R$ 35,10-R$ 3,30 · meta R$ 36,00 Reuniões 503+10 · meta 528 Resposta ao lead 1,9 h-0,5 h · meta 2,0 h Campanhas ativas 91+5 · sem meta Fechamentos 61de 212 propostas Detalhe por cliente: carrossel com 12 cartões, Studio Prisma (-11%) e Loja Marés (-7%) primeiro · 12 col
Esqueleto da primeira tela em 12 colunas: manchete, número herói à esquerda, figura do desvio principal à direita, nove cartões em três linhas e a entrada do carrossel. Dados fictícios de outubro de 2026.
As áreas da figura, com largura na grade de 12 colunas e altura aproximada em pixels a 1.280 px de largura
ÁreaColunasAlturaConteúdo
Manchete1256 pxFrase calculada a partir do dado, com o maior desvio primeiro.
Número herói4160 pxMRR R$ 428.900, meta R$ 435.000, diferença escrita.
Figura do desvio principal8160 pxLinha de cancelamentos de abril a outubro com a meta tracejada e o último valor rotulado.
Nove cartões4 cada, três por linha72 px por linhaRótulo, valor, variação contra setembro e contra a meta, denominador quando é taxa.
Entrada do detalhe1248 pxCarrossel de 12 clientes, piores variações primeiro.

Hierarquia tipográfica com números fixos

O painel usa uma única família sem serifa, a do sistema, em quatro tamanhos que não mudam de tela para tela: número herói de 48 px ou mais; valor de cartão de 28 a 32 px; rótulo e variação de 14 a 16 px; eixos e legendas de 14 px, que é o piso do guia de Gráficos para rótulo e eixo. Nenhum texto do painel fica abaixo de 14 px a 1.280 px nem abaixo de 12 px a 390 px. O peso faz o resto da hierarquia: valor em negrito, rótulo em peso normal, variação em peso normal com o sinal e a palavra de direção ("acima", "abaixo") antes da cor.

O número do cartão não ganha a cor da série nem a cor do estado; o texto usa os tokens de texto e a cor fica numa marca ao lado (um ponto, uma seta desenhada, uma barra fina), como manda a skill dataviz. O motivo é contraste: um verde ou um âmbar que funcionam como preenchimento de barra raramente passam de 4,5:1 como texto sobre a superfície, e o painel precisa passar nos dois temas. O guia de Ícones explica a marcação de estado sem depender da cor; o cartão de cancelamentos da Mirante escreve "+0,5 p.p." e "acima da meta" antes de pintar qualquer coisa.

A regra dos dois cliques e a procedência de cada número

Todo número da primeira tela leva a pessoa, em até dois cliques, a quatro informações: a fonte (de onde o dado veio), a definição (o que conta e o que não conta), a data da última atualização e o dono (quem responde pelo número). O guia de Gráficos chama isso de regra dos dois cliques. No painel da Mirante, o primeiro clique no cartão abre o detalhe no carrossel, e o segundo abre uma gaveta com a ficha do indicador: "Conversão lead para reunião: reuniões realizadas no mês dividido por leads captados no mesmo mês; fonte: CRM da agência, exportação diária às 6h; atualizado em 3 de novembro de 2026 às 6h12; dono: coordenação de atendimento." Sem a ficha, a discussão da segunda-feira vira uma disputa sobre a definição em vez de uma decisão sobre o número.

A data de atualização aparece também na própria tela, num rodapé fixo em 14 px, com a data do dado mais recente e a hora da última carga. Dado atrasado (carga de ontem numa tela que deveria ter a de hoje) muda o rodapé para um aviso com causa e próxima ação, no lugar de um ponto vermelho.

Os estados da tela que ninguém desenha e todo mundo vê

Um painel passa mais tempo em estados intermediários do que o desenho no editor sugere. Carregando: os cartões mostram o rótulo e um espaço reservado com a altura final, com aria-busy="true" na região, e nada que pisque por mais de três pulsos. Atualizando: a figura anterior continua visível com opacidade reduzida até a nova chegar, sem esqueleto e sem salto de layout, como a skill dataviz exige para recarregamento. Sem dado: o cartão escreve "Sem informação para outubro" e a causa provável, em vez de mostrar zero, porque ausência e zero são estados diferentes (o guia de Gráficos traz os sinais do IBGE para isso). Atrasado: a data no rodapé muda de cor e de texto, com o que fazer. Sem credencial e fonte fora do ar são estados próprios, com mensagem que nomeia o serviço e o contato.

Esses estados ficam no HTML servido, como classes ou atributos de dados em cada cartão, de modo que o teste de aceitação consiga provocá-los sem derrubar o banco. A Mirante gravou um conjunto de dados de exercício com cada estado, e a oficina Primeira tela em cinco segundos os usa.

Densidade medida, e não sentida

"Denso demais" e "vazio demais" são opiniões até que alguém conte. Três contagens resolvem a conversa: palavras visíveis na primeira tela (a Mirante ficou entre 180 e 220, contadas no texto renderizado, contra 640 do painel antigo); marcas de dado na tela (barras, pontos, segmentos: o painel novo tem 7 pontos de linha, 9 cartões, 1 bullet e 12 barras no carrossel aberto, 29 marcas; o antigo passava de 300); e número de cores com significado (quatro: a cor da série principal, o cinza de contexto, o âmbar da meta e a cor de estado). O limite não é uma regra universal; é o número que a equipe mede no teste dos cinco segundos e fixa no contrato visual, descrito no guia de Tailwind como DESIGN.md.

A conta de marcas segue a orientação de Franconeri e colegas em 2021 sobre memória de trabalho: cada marca que precisa ser comparada com outra consome atenção, e a primeira tela só deve conter as comparações que a decisão exige. O carrossel e a página de consulta carregam as demais. A seção seguinte escolhe o gráfico de cada caixa pela tarefa de leitura.

Escolha o gráfico pela tarefa de leitura que a pergunta exige

A pergunta de quem lê determina a tarefa perceptiva, e a tarefa perceptiva determina o gráfico. Essa cadeia tem base experimental desde 1984, quando William Cleveland e Robert McGill publicaram "Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods" no Journal of the American Statistical Association (vol. 79, n. 387). Eles mediram a precisão com que pessoas estimam valores em dez tarefas elementares e obtiveram um ranking: posição numa escala comum em primeiro, depois posição em escalas não alinhadas, depois comprimento, direção e ângulo, depois área, depois volume e curvatura, e por último sombreado e saturação de cor. Jeffrey Heer e Michael Bostock replicaram o experimento em 2010 com centenas de participantes do Mechanical Turk e chegaram a resultados muito próximos.

Na agência Mirante (fictícia, exemplo didático), a pizza de nove fatias dos canais pedia ao leitor uma comparação de ângulos e áreas, as tarefas do fim do ranking; a barra horizontal ordenada que a substituiu pede uma comparação de posição numa escala comum, a tarefa do topo. Helena passou a ver que, em outubro de 2026, o Instagram (1.845 leads, 40%) tem quase o triplo do WhatsApp (738, 16%) sem precisar ler os números. Esta seção organiza as perguntas de um painel por tarefa e dá, para cada uma, o gráfico recomendado, a alternativa e o que evitar, sempre com um exemplo dos dados de exercício.

A tarefa de leitura vem antes do tipo de gráfico

O catálogo de 40 tipos do guia de Gráficos desta coleção já organiza os gráficos por pergunta; esta seção aplica a mesma lógica ao conjunto de um painel, onde oito a doze figuras disputam a atenção e onde a mesma tarefa deve receber sempre a mesma forma. A tabela lista as famílias de pergunta que aparecem na operação de uma agência, a tarefa perceptiva envolvida e a forma que a serve. A coluna "Evite" reúne os erros que a Mirante cometeu no painel antigo ou que apareceram em revisões de painéis gerados por agente de código.

Da pergunta ao gráfico: a tarefa perceptiva (Cleveland e McGill, 1984; Franconeri e colegas, 2021) decide a forma; exemplos com os dados fictícios de outubro de 2026
PerguntaTarefa perceptivaGráfico recomendadoAlternativaEviteNa Mirante
Quem está na frente? (ranking)Posição numa escala comum.Barra horizontal ordenada, maior primeiro, uma cor.Lollipop quando há mais de 15 itens; tabela acima de 20.Ordem alfabética; cor por item; pizza.12 clientes por leads, Padaria Horizonte (612) no topo, Café Ladeira (255) na base.
Quanto é? (magnitude)Posição e comprimento.Cartão com valor, variação e denominador.Barra única com meta ao lado.Velocímetro; rosca de uma fatia.4.612 leads, +10,3% sobre setembro.
Para onde vai? (tendência)Direção e posição ao longo do tempo.Linha de 2 px, rótulo direto na ponta, meta tracejada.Área só para uma série que parte do zero.Barras para 12 ou mais períodos; eixo de tempo irregular.Churn de abril a outubro: 2,2 → 3,1%, meta 2,5%.
Repete todo ano? (sazonalidade)Comparação de posição entre ciclos.Linhas sobrepostas, uma por ciclo, mesma escala de meses.Mapa de calor mês × ano.Ler um pico anual como crescimento.Leads de outubro contra outubro do ano anterior, e não contra setembro, quando houver histórico.
Chegamos à meta? (meta contra realizado)Posição contra uma marca.Gráfico de marcador (bullet): barra do realizado, traço da meta, faixas em um matiz.Linha com meta tracejada quando há tempo.Velocímetro; semáforo sem número.MRR R$ 428.900 contra meta de R$ 435.000.
De onde veio a variação? (decomposição)Comprimento acumulado.Cascata (waterfall) com até 8 degraus e total nas pontas.Tabela de variação com sinal.Mais de 8 degraus; cascata sem total.De 4.180 a 4.612 leads: Instagram +198, Google +141, WhatsApp +62, E-mail +21, Outros +10.
Como a mistura mudou? (composição no tempo)Comprimento empilhado.Barras 100% empilhadas com o total absoluto escrito no eixo.Área empilhada para muitos períodos.Mais de 5 segmentos; rosca por período.Canais por mês: Instagram 40%, Google 28%, WhatsApp 16%, E-mail 10%, Outros 6%.
Quanto cada parte pesa? (parte do todo)Comprimento sobre uma linha de base.Barra horizontal ordenada por participação, ou barra segmentada única.Rosca só até 3 fatias, com o total no centro.Pizza com mais de 3 fatias; o Microsoft Learn aceita até 8, o guia da casa fixa 3.Cinco canais em barras; a pizza de nove fatias saiu.
Como os valores se espalham? (distribuição)Posição e densidade.Histograma; boxplot (caixa dos quartis e bigodes); enxame (beeswarm) do ECharts 6 para até algumas centenas de pontos.Violino para comparar formas entre grupos.Mostrar só a média; esconder os extremos.Tempo de resposta ao lead: mediana de 1,9 h, mas a cauda passa de 8 h em dois clientes.
Uma coisa acompanha a outra? (relação)Posição em duas escalas.Dispersão com 2 ou 3 grupos; jitter (deslocamento aleatório) quando os pontos se sobrepõem.Linha de tendência só com o método escrito.Ler correlação como causa; mais de 3 cores.CPL por cliente contra conversão: Studio Prisma com CPL alto e conversão baixa.
Onde o funil perde? (conversão)Comprimento entre etapas.Barras horizontais por etapa com a taxa entre etapas escrita.Tabela de etapas com denominador.Funil em trapézio (a área engana); etapas sem taxa.48.200 visitas → 4.612 leads (9,6%) → 503 reuniões (10,9%) → 212 propostas (42,1%) → 61 fechamentos (28,8%).
Quem fica com o tempo? (coorte)Posição ao longo do tempo por grupo.Linhas por coorte na mesma escala, ou tabela de coorte com cor sequencial.Mapa de calor coorte × mês.Misturar coortes de tamanhos diferentes num total.Clientes de maio: 100, 94, 89, 86, 84, 83% ativos de M0 a M5.
Por onde passa? (fluxo)Largura de ligação.Sankey com até 3 colunas e 10 nós.Tabela origem × destino.Sankey com dezenas de nós; cores por fluxo.Canal → tipo de lead → resultado, três colunas.
O mesmo padrão vale em todo grupo? (comparação entre grupos)Posição em painéis alinhados.Múltiplos pequenos na mesma escala, grade de até 3 colunas no computador.Linhas sobrepostas até 4 séries.Escala livre por painel sem aviso.Leads por semana em 12 painéis, um por cliente, escala 0 a 160.
Quanto tempo leva? (tempo entre eventos)Comprimento.Barra de duração por etapa ou gráfico de Gantt (barras no tempo) para poucas tarefas.Histograma das durações.Média sem a mediana; Gantt com dezenas de linhas na primeira tela.Da captação à reunião: mediana de 4 dias.
Poucos respondem por quase tudo? (concentração)Comprimento e posição acumulada.Pareto: barras ordenadas e linha acumulada de 0 a 100% no eixo da direita (única exceção ao eixo duplo).Barra ordenada com corte em 80%.Eixo duplo em qualquer outro caso.Os 5 maiores clientes somam 2.483 leads, 54% do total.

Comparar: posição numa escala comum

Toda comparação entre categorias começa com uma barra horizontal ordenada, uma cor e a mesma linha de base. A barra horizontal aceita rótulos longos sem rotação, e a ordem decrescente coloca a pergunta "quem está na frente" na primeira leitura. O guia de Gráficos limita a barra ordenada a 15 categorias; acima disso, os dez maiores mais "outros". Para destacar, a skill dataviz recomenda a forma de ênfase: um item na cor de acento e o resto em cinza. Na Mirante, em outubro de 2026, as 12 barras de leads por cliente ficam em cinza e as duas com queda maior (Studio Prisma, -11%, e Loja Marés, -7%) recebem a cor de acento, porque a pergunta de Helena é "em quem intervir", e não "quem é maior".

A comparação de magnitude isolada, por sua vez, dispensa gráfico: um cartão com o número, a variação e o denominador é a forma mais exata. Uma barra sozinha, uma rosca de uma fatia ou um velocímetro só acrescentam tinta. O Microsoft Learn, nas dicas de relatório, pede números com no máximo três ou quatro algarismos significativos ("3.4 million" em vez de 3.400.000); em português, "R$ 428,9 mil" no cartão e o valor completo na dica e na tabela.

Acompanhar: tempo

Tempo pede linha, com dois pixels de espessura, marcador só nos pontos que importam e o último valor escrito na ponta. Até quatro séries convivem com legenda; até cinco com rótulo direto; a partir daí, múltiplos pequenos. Barras para série temporal só valem com poucos períodos (até uns 12) e quando a pergunta é a magnitude de cada período em vez da direção. O eixo de tempo precisa ser regular: Jae Rho e colegas mediram em 2024 (CogSci) 15,4 vezes mais erro de leitura com intervalos irregulares, número que o guia de Gráficos já registra.

Sazonalidade é a armadilha mais cara desta família. Um pico de leads em outubro comparado com setembro parece crescimento; comparado com outubro do ano anterior pode ser a repetição do ciclo. O painel que tem histórico de mais de um ano compara contra o mesmo mês do ano anterior e mostra os ciclos sobrepostos, um por ano, na mesma escala de meses. A Mirante, com dados só desde abril de 2026, escreveu isso no rodapé da figura: "comparação sazonal disponível a partir de abril de 2027".

Decompor: variação e composição

"De onde veio a diferença" é uma pergunta de cascata: a barra de setembro (4.180 leads) à esquerda, um degrau por causa e a barra de outubro (4.612) à direita, com o total nas duas pontas. Os degraus da Mirante são Instagram +198, Google +141, WhatsApp +62, E-mail +21 e Outros +10, cinco degraus, dentro do limite de oito do guia de Gráficos. Acima de oito, a cascata vira tabela de variação com sinal, alinhada à direita com algarismos tabulares.

Composição no tempo, como a dos canais ao longo de 2026, pede barras 100% empilhadas com o total absoluto escrito no eixo ou sobre cada barra, porque a proporção sem o total engana quando a base muda. Até cinco segmentos por barra, com um vão de 2 px na cor da superfície entre eles, como a skill dataviz especifica, em vez de contorno. A ordem dos segmentos é fixa entre períodos; o Instagram fica sempre na base, para que o olho compare o mesmo segmento de barra em barra.

Distribuir: a forma dos dados

Uma média esconde a forma. O tempo de resposta ao lead da Mirante tem mediana de 1,9 hora, abaixo da meta de 2,0, mas a distribuição tem cauda longa: dois clientes têm leads esperando mais de 8 horas, e a média, puxada por eles, passa de 2,6 horas. O histograma mostra a cauda; o boxplot (caixa que vai do primeiro ao terceiro quartil, com a mediana no meio e bigodes até os extremos não atípicos) compara a forma entre clientes em pouco espaço; o enxame (beeswarm, introduzido no ECharts 6.0 em julho de 2025) desenha cada lead como um ponto deslocado para não sobrepor, bom para até algumas centenas de pontos. O violino, também disponível como série customizada no ECharts 6, acrescenta a densidade ao boxplot.

Franconeri e colegas insistem em 2021 numa regra de método que vale para quem monta painel: visualizar a distribuição antes de confiar num resumo estatístico. Um painel que mostra só a mediana do tempo de resposta não revela os dois clientes da cauda; a figura de distribuição, no detalhe do carrossel, revela, e é ela que gera a intervenção.

Relacionar: duas medidas

Quando a pergunta é "uma coisa acompanha a outra", a forma é a dispersão: cada cliente um ponto, CPL no eixo horizontal, conversão no vertical, no máximo três grupos por cor. Com pontos sobrepostos, o jitter (deslocamento aleatório pequeno, recurso do ECharts 6.0) separa o que estava empilhado. A linha de tendência entra só com o método escrito na legenda ("regressão linear simples, n = 12") e nunca sozinha, porque doze pontos sustentam uma tendência fraca.

A leitura mais perigosa da dispersão é causal. CPL alto e conversão baixa no Studio Prisma coincidem; o painel não sabe se um causa o outro, se um terceiro fator (um criativo gasto, uma mudança de oferta) causa os dois, ou se é ruído de um mês. A dispersão responde "esses dois andam juntos"; a causa é trabalho de investigação, fora do painel. O rodapé da figura diz isso em uma linha.

Converter: funil e coorte

O funil da Mirante vai de 48.200 visitas a 61 fechamentos em cinco etapas. A forma certa, no funil de outubro de 2026, é uma barra horizontal por etapa, com a taxa entre etapas escrita ao lado: 9,6% de visita para lead, 10,9% de lead para reunião, 42,1% de reunião para proposta e 28,8% de proposta para fechamento. O funil em trapézio, comum em ferramentas de marketing, faz a área de cada faixa variar com a largura e com a altura ao mesmo tempo, e a área está no fim do ranking de Cleveland e McGill. O guia de Gráficos reforça: conversão é o funil inteiro, e cada etapa tem nome.

A coorte responde "quem fica com o tempo". Os clientes que entraram em maio de 2026 estavam 94% ativos no mês seguinte e 83% no quinto mês. Com várias coortes, as linhas na mesma escala mostram se as entradas recentes retêm melhor ou pior que as antigas; a tabela de coorte, com cor sequencial de um matiz só, mostra o mesmo em menos espaço e serve a quem quer o número exato. Coortes de tamanhos muito diferentes não se somam num total sem aviso.

Quando a tabela vence

A tabela vence o gráfico em três situações: quando a pessoa precisa do valor exato (plano contra realizado centavo a centavo), quando compara razões com bases diferentes (taxa de dois clientes com 600 e 250 leads) e quando há mais de sete classes com significado, limite a partir do qual, segundo a skill dataviz, as cores deixam de se distinguir. A tabela de variação da Mirante, com indicador, setembro, outubro, meta, diferença contra setembro e diferença contra a meta, alinhada à direita com algarismos tabulares, é a figura mais consultada da página de consulta, e nenhum gráfico a substitui.

Três formas lado a lado com os dados de exercício

Três formas para três perguntas: barra ordenada dos 12 clientes, linha de leads por semana em setembro e outubro, e gráfico de marcador do MRR contra a meta À esquerda, doze barras horizontais ordenadas de Padaria Horizonte (612 leads) a Café Ladeira (255), com Loja Marés e Studio Prisma em destaque por queda. Ao centro, duas linhas de cinco semanas: outubro de 860 a 1.004 leads, setembro de 780 a 905. À direita, um gráfico de marcador com a receita recorrente de R$ 428.900, a meta de R$ 435.000 como traço e setembro, R$ 412.300, como marca. Os valores estão na tabela abaixo. Ranking: leads por cliente Padaria Horizonte Clínica Vale Verde Loja Marés Escola Compasso Academia Pulso Imobiliária Lume Oficina Trilho Pet Serena Cursos Bússola Studio Prisma Dental Aurora Café Ladeira Destaque: queda sobre setembro Tendência: leads por semana 700 900 1.100 1.004 (S3) setembro outubro S1S2S3S4S5 Eixo de 700 a 1.100, início escrito Meta: MRR contra R$ 435 mil R$ 428.900 realizado traço preto: meta 435.000 traço âmbar: setembro 412.300 300 400 450 Faixas em um matiz; sem velocímetro
Três perguntas, três formas: ranking em barra ordenada com ênfase, tendência em linha com início do eixo escrito, meta em gráfico de marcador. Dados fictícios de outubro de 2026.
Valores das três figuras (dados fictícios): leads por cliente em outubro, leads por semana em setembro e outubro, e MRR contra meta
FiguraItemValorReferência
RankingPadaria Horizonte612+9% sobre setembro
Loja Marés (destaque)498-7%
Studio Prisma (destaque)274-11%
Café Ladeira2550%
TendênciaOutubro, S1 a S5860, 912, 1.004, 958, 878total 4.612
Setembro, S1 a S5790, 835, 870, 905, 780total 4.180
MetaMRR outubroR$ 428.900faltam R$ 6.100
MetaR$ 435.000traço
MRR setembroR$ 412.300marca comparativa

Dez armadilhas estatísticas que o painel precisa expor, e não esconder

Um gráfico certo sobre um número errado continua errado. A tabela reúne as dez leituras enganosas mais frequentes em painéis de operação, como detectar cada uma e o que a tela deve mostrar para evitá-la. Várias apareceram no painel antigo da Mirante; a de dias úteis diferentes custou uma reunião inteira em agosto de 2026, quando um "crescimento" de 9% em leads era, em leads por dia útil, uma queda de 1%.

Dez armadilhas de leitura em painéis, com a detecção e a correção na tela; exemplos com os dados fictícios da Mirante
ArmadilhaComo detectarO que mostrar
Média puxada por um extremoMédia e mediana distantes (tempo de resposta: média 2,6 h, mediana 1,9 h).Mediana como valor principal, média e distribuição no detalhe.
Base pequenaTaxa alta ou baixa sobre denominador abaixo de algumas dezenas (Café Ladeira: 255 leads, 3 reuniões a mais parecem +25%).Denominador escrito e intervalo ou aviso "base pequena" abaixo de 30 casos.
Taxa sem denominadorPercentual sozinho no cartão."10,9% de 4.612 leads em outubro de 2026" no cartão; nunca o percentual isolado.
Paradoxo de SimpsonO agregado sobe enquanto todos os grupos caem (conversão total pode subir só porque o canal de conversão alta cresceu).O agregado ao lado dos grupos, na mesma figura ou em múltiplos pequenos.
Sazonalidade lida como tendênciaO pico coincide com o mesmo mês do ano anterior.Comparação com o mesmo mês do ano anterior e ciclos sobrepostos quando houver histórico.
Acumulado que parece crescerUma curva acumulada nunca cai, por definição.O valor por período ao lado do acumulado; nunca o acumulado sozinho.
Correlação lida como causaDois indicadores andam juntos numa dispersão de poucos pontos.Rodapé com "associação, e não causa" e o número de pontos; investigação fora do painel.
SobrevivênciaSó os clientes ativos entram na média de NPS; os que cancelaram não respondem.Base da pesquisa escrita ("47 pontos em 38 respostas de 91 campanhas ativas") e taxa de resposta.
Arredondamento que inventa variação"11,8%" e "10,9%" arredondados de 11,75 e 10,94: a diferença muda com a casa decimal.Uma casa decimal fixa em taxas, duas em moeda, com o valor completo na tabela.
Períodos com dias diferentesOutubro tem 23 dias úteis e setembro 22 (em 2026); o total cresce 4,5% só por isso.Métrica por dia útil ao lado do total, ou a comparação com o mesmo número de dias.

A detecção dessas armadilhas é trabalho de ciência de dados, e o painel é o lugar onde a detecção vira hábito: cada figura que mostra o denominador, a mediana ao lado da média ou o valor por dia útil poupa uma reunião de discussão. A seção seguinte trata do instrumento que mais distorce a leitura quando é mal escolhido: a escala.

Escolha a escala pela comparação que a pessoa precisa fazer, e escreva a escolha na figura

A escala é a decisão que mais muda a leitura de um gráfico sem mudar um único dado. A mesma série de cancelamentos da agência Mirante (fictícia, exemplo didático), de 2,2% em abril a 3,1% em outubro de 2026, parece estável num eixo que começa em zero, parece uma escalada num eixo cortado em 2%, parece plana num eixo logarítmico e parece um crescimento de 41% num eixo indexado. Nenhuma das quatro leituras é falsa; cada uma responde a uma pergunta diferente, e o erro está em usar uma escala para a pergunta de outra sem avisar. Esta seção fixa quando cada escala serve, como escrevê-la na figura e quais convenções de número valem em português.

Barra e área partem do zero; linha e dispersão podem cortar, com o início escrito

A barra codifica o valor pelo comprimento, e o comprimento só é proporcional ao valor quando a base é zero. Uma barra de 4.612 leads (outubro de 2026) ao lado de outra de 4.180, num eixo que começa em 4.000, mostra uma diferença de 612 contra 180, mais de três vezes a diferença real de 10%. O mesmo vale para a área. A regra do guia de Gráficos desta coleção é sem exceção: barra e área partem do zero.

A linha codifica o valor pela posição, e a posição continua legível com o eixo cortado, desde que o início esteja escrito. Franconeri e colegas registram em 2021 que começar o eixo em zero nem sempre é a melhor escolha: o zero pode esconder uma variação relevante (uma temperatura corporal de 36,5 a 39 °C num eixo de zero a 40 fica plana) ou criar a ilusão de estabilidade onde há mudança. O painel da Mirante desenha o churn de 2026 de 2,0% a 3,5% com os dois valores escritos nas extremidades do eixo e com a meta de 2,5% tracejada, de modo que a inclinação da linha seja lida contra uma referência e não contra o nada. O Microsoft Learn, nas dicas de relatório, lista o eixo truncado entre os recursos a evitar; a conciliação é esta: truncar só em linha e dispersão, nunca em barra, e sempre com o corte visível.

Escala logarítmica para ordens de grandeza e taxas de crescimento

Na escala logarítmica, cada passo do eixo multiplica o valor por um fator fixo (10, 100, 1.000) em vez de somar uma quantia fixa. Ela serve a duas perguntas. A primeira é comparar valores que diferem por ordens de grandeza, como visitas (48.200), leads (4.612) e fechamentos (61) no mesmo eixo: na escala linear, os fechamentos viram uma linha invisível na base. A segunda é comparar taxas de crescimento: na escala log, uma série que cresce 10% ao mês, como a de leads da Mirante em 2026, desenha uma reta, e duas séries com a mesma taxa desenham retas paralelas, qualquer que seja o tamanho de cada uma.

O custo é a leitura. Claus Wilke, em Fundamentals of Data Visualization (O'Reilly, disponível em clauswilke.com sob CC BY-NC-ND 4.0), dedica o capítulo 3 aos eixos não lineares e o 17 à tinta proporcional, e mostra que a barra em escala log perde o sentido: o comprimento deixa de ser proporcional a qualquer coisa. Log só em linha e dispersão, com os rótulos do eixo em valores redondos (1, 10, 100, 1.000) e a palavra "escala logarítmica" escrita no título do eixo. Valores zero e negativos não existem na escala log; o ECharts 6.1.0 (19 de maio de 2026) passou a excluir automaticamente valores não positivos de um eixo do tipo log, o que evita o erro de renderização mas não dispensa o aviso na figura de que esses pontos ficaram de fora.

Escala indexada: o substituto do eixo duplo

O painel antigo da Mirante tinha um gráfico com leads no eixo da esquerda (de 0 a 5.000) e receita recorrente no da direita (de R$ 0 a R$ 500.000), e as duas linhas se cruzavam num ponto que a diretoria lia como "a receita ultrapassou os leads". O cruzamento era um artefato da escolha dos dois eixos; mudar o máximo de um deles mudava o mês do cruzamento. O guia de Gráficos proíbe o eixo duplo, com a única exceção do Pareto; a skill dataviz o coloca em primeiro lugar entre os erros de gráfico.

A escala indexada resolve a pergunta que o eixo duplo tentava responder: "as duas séries crescem no mesmo ritmo?". Cada série é dividida pelo seu valor num período-base e multiplicada por 100; a partir daí, as duas cabem num único eixo e a leitura é "quanto cada uma variou em relação a abril". Leads de setembro para outubro: 4.612 ÷ 4.180 × 100 = 110,3. MRR: 428.900 ÷ 412.300 × 100 = 104,0. O gráfico indexado mostra as duas linhas partindo de 100 e a diferença de ritmo (6,3 pontos de índice) sem nenhum cruzamento inventado. O título do eixo diz "índice, setembro de 2026 = 100". Quando a pergunta é a magnitude, e não o ritmo, a resposta são dois gráficos lado a lado, cada um com o seu eixo.

Escalas compartilhadas em múltiplos pequenos

Múltiplos pequenos (vários gráficos iguais, um por grupo, numa grade) só permitem comparar os grupos quando todos usam a mesma escala. O Power BI aplica a regra por construção: segundo o Microsoft Learn, os small multiples têm os eixos sincronizados, com um eixo Y à esquerda de cada linha da grade e um eixo X na base de cada coluna. Na Mirante, os doze painéis de leads por semana, um por cliente, usam a escala de 0 a 160 em todos; a Padaria Horizonte enche o painel e o Café Ladeira ocupa o terço inferior, e é exatamente essa diferença que a figura precisa mostrar.

A escala livre por painel (cada um com o próprio máximo) tem um uso legítimo: quando a pergunta é a forma de cada série, e não a magnitude, como no padrão semanal de cada cliente. Nesse caso cada painel escreve o seu máximo no canto e o conjunto leva o aviso "escalas independentes; compare a forma em vez da altura". Sem o aviso, o leitor compara alturas iguais que representam valores diferentes. Wilke trata disso no capítulo 21, sobre figuras com vários painéis.

Eixo quebrado só com a quebra visível

O eixo quebrado retira um trecho da escala para que um valor muito distante caiba na figura. O ECharts 6.0 (30 de julho de 2025) incluiu o recurso "axis break" entre os doze destaques da versão, com a marca de quebra desenhada no eixo. A quebra é legítima em uma condição: a marca de quebra fica visível, com os dois valores de borda escritos, e o gráfico é uma linha ou uma dispersão, nunca uma barra. Uma barra cruzando um eixo quebrado tem o comprimento mutilado, e a leitura volta ao problema da base fora do zero. Na Mirante, a quebra não foi necessária em nenhuma figura da primeira tela; o único candidato (as 48.200 visitas no funil) virou barra em escala própria, com a taxa escrita entre etapas.

Marcas do eixo em números redondos

Um eixo com as marcas 0, 1.000, 2.000 e 3.000 é lido em um relance; um com 0, 1.153, 2.306 e 3.459 obriga a pessoa a fazer contas. O algoritmo de Wilkinson, estendido por Justin Talbot, Sharon Lin e Pat Hanrahan em 2010 ("An Extension of Wilkinson's Algorithm for Positioning Tick Labels on Axes", InfoVis 2010; dado de resultado de busca, sem leitura da página), escolhe marcas que terminam em 1, 2, 5 ou 10 e cobrem o intervalo dos dados com folga mínima. O ECharts 6.1.0 acrescentou as opções dataMin e dataMax ao eixo para calcular uma extensão "bonita" a partir dos dados sem fixar o valor na mão. Em SVG escrito à mão, uma função de 20 linhas resolve o mesmo problema, e está no bloco de código desta seção.

O número de marcas também tem limite: de 3 a 6 linhas de grade a 390 px, de 6 a 10 a 1.280 px, como fixa o guia de Gráficos. Mais do que isso, a grade vira textura e as marcas deixam de ser lidas.

Normalizar antes de comparar

Comparar o total de leads de doze clientes de tamanhos diferentes responde "quem é maior", e não "quem vai melhor". Para a segunda pergunta, o painel normaliza: leads por 1.000 visitas (a conversão de visita em lead), leads por dia útil (para comparar meses com 22 e 23 dias úteis), custo por lead (gasto dividido pela base). A regra é escrever a unidade normalizada no rótulo ("leads por 1.000 visitas") e manter o total absoluto ao alcance de um clique, porque uma taxa boa sobre uma base minúscula não paga a conta.

A diferença entre percentuais pede unidade própria: ponto percentual (p.p.). A conversão caiu de 11,8% para 10,9% entre setembro e outubro de 2026, uma queda de 0,9 p.p. Escrever "caiu 0,9%" é falso para outubro de 2026 (a queda relativa é de 7,6%), e escrever "caiu 7,6%" sem o "relativa" faz a pessoa imaginar que a conversão foi a 4,2%. O cartão mostra a diferença em p.p.; a dica mostra a variação relativa com a palavra "relativa".

Tempo: sazonalidade, média móvel, acumulado e período

Uma série semanal oscila por motivos que não interessam à decisão: feriado, fim de mês, campanha pontual. A média móvel (a média dos últimos N períodos, recalculada a cada ponto) suaviza o ruído e mostra a direção; o Power BI a oferece como cálculo visual, com a função MOVINGAVERAGE que roda direto no visual, sem alterar o modelo semântico, segundo o Microsoft Learn. A janela precisa estar escrita na legenda ("média móvel de 4 semanas"), e a série original continua visível em cinza por trás, porque a média móvel atrasa a detecção de uma virada em metade da janela.

Acumulado e período respondem perguntas diferentes e não se substituem. O acumulado (soma desde o início do mês, RUNNINGSUM no Power BI) responde "quanto já fizemos da meta do mês" e nunca desce, o que o torna inútil para detectar queda. O valor por período responde "como foi esta semana". A regra é mostrar os dois ou só o período; nunca só o acumulado. Para comparação entre períodos, mês contra mês anterior mostra o curto prazo e sofre com sazonalidade; mês contra o mesmo mês do ano anterior mostra o crescimento real e precisa de 13 meses de histórico. O painel da Mirante, com dados desde abril de 2026, escreve no rodapé a data em que a comparação anual ficará disponível.

Intervalo quando a base é pequena

Uma taxa calculada sobre poucos casos muda muito com um caso a mais. O Café Ladeira, com 255 leads e 28 reuniões em outubro de 2026, tem conversão de 11,0%; com 31 reuniões teria 12,2%, e com 25, 9,8%. Esses saltos são ruído de amostra, sem relação com desempenho. Abaixo de algumas dezenas de casos, o painel mostra o intervalo (a faixa em que a taxa verdadeira provavelmente está) ou, no mínimo, o aviso "base pequena: 255 leads" no cartão, e Wilke dedica o capítulo 16 à representação da incerteza. A oficina Escala certa deixa a pessoa variar a base e ver a faixa abrir e fechar.

A mesma série em quatro escalas

A série de cancelamentos de abril a outubro de 2026 em quatro escalas: de zero, cortada, logarítmica e indexada Quatro painéis com a mesma linha de sete pontos (2,2; 2,4; 2,3; 2,8; 2,5; 2,6; 3,1% da base). No painel de zero a 3,5% a linha sobe pouco; no painel cortado de 2,0 a 3,2% a mesma subida ocupa toda a altura; no painel logarítmico de 1 a 10% a linha fica quase plana; no painel indexado, com abril igual a 100, outubro chega a 141. Os valores estão na tabela abaixo. A. Eixo de zero a 3,5% 0 1,75 3,5 3,1% B. Eixo cortado, de 2,0 a 3,2% 2,0 2,6 3,2 3,1% início do eixo escrito: 2,0% C. Eixo logarítmico, de 1 a 10% 1 3,2 10 3,1% escala logarítmica; só linha e dispersão D. Indexado, abril = 100 90 120 150 141 índice: abril de 2026 = 100 abroutabrout
A mesma série de cancelamentos em quatro escalas: cada painel responde a uma pergunta diferente, e a escolha precisa estar escrita no eixo. Dados fictícios.
Cancelamentos (churn) da base por mês, em percentual e em índice com abril de 2026 igual a 100 (dados fictícios)
MêsChurn (% da base)Índice (abril = 100)Variação sobre o mês anterior
Abril2,2100,0sem mês anterior
Maio2,4109,1+0,2 p.p.
Junho2,3104,5-0,1 p.p.
Julho2,8127,3+0,5 p.p.
Agosto2,5113,6-0,3 p.p.
Setembro2,6118,2+0,1 p.p.
Outubro3,1140,9+0,5 p.p.

Formatos em português e os sinais de ausência

Todo número do painel passa por Intl.NumberFormat com a localidade pt-BR: ponto de milhar, vírgula decimal, "R$" com espaço, percentual com vírgula. A notação compacta ("4,6 mil") entra no cartão; o valor completo fica na dica e na tabela. Ausência de dado não é zero: o guia de Gráficos adota os sinais do IBGE, com traço para zero real, dois pontos para "não se aplica", reticências para "não disponível" e "x" para valor omitido, sempre com a nota que explica. Na tabela acima, a primeira variação recebe o traço porque não existe mês anterior na série, e o rodapé diz isso.

// Formatos pt-BR para cartões, dicas e eixos. Um formatador por papel, criado uma vez.
const fmt = {
  inteiro: new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 0 }),
  compacto: new Intl.NumberFormat("pt-BR", { notation: "compact", maximumFractionDigits: 1 }),
  moeda: new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", maximumFractionDigits: 0 }),
  taxa: new Intl.NumberFormat("pt-BR", { style: "percent", minimumFractionDigits: 1, maximumFractionDigits: 1 }),
};
fmt.inteiro.format(4612);        // "4.612"
fmt.compacto.format(4612);       // "4,6 mil"
fmt.moeda.format(428900);        // "R$ 428.900"
fmt.taxa.format(0.109);          // "10,9%"

// Diferença entre percentuais em pontos percentuais, com sinal e uma casa.
function pontosPercentuais(atual, anterior) {
  const pp = (atual - anterior) * 100;
  return `${pp > 0 ? "+" : ""}${pp.toLocaleString("pt-BR", { maximumFractionDigits: 1 })} p.p.`;
}
pontosPercentuais(0.109, 0.118); // "-0,9 p.p."

// Índice com período-base igual a 100: duas séries de magnitudes diferentes num eixo só.
const indexar = (serie, base = serie[0]) => serie.map((v) => Math.round((v / base) * 1000) / 10);
indexar([2.2, 2.4, 2.3, 2.8, 2.5, 2.6, 3.1]); // [100, 109.1, 104.5, 127.3, 113.6, 118.2, 140.9]

// Marcas "redondas" para o eixo: passos que terminam em 1, 2, 5 ou 10 cobrindo o intervalo com folga mínima.
function marcasRedondas(minimo, maximo, quantidade = 5) {
  const bruto = (maximo - minimo) / Math.max(1, quantidade - 1);
  const potencia = Math.pow(10, Math.floor(Math.log10(bruto)));
  const fracao = bruto / potencia;
  const passo = (fracao < 1.5 ? 1 : fracao < 3 ? 2 : fracao < 7 ? 5 : 10) * potencia;
  const inicio = Math.floor(minimo / passo) * passo;
  const fim = Math.ceil(maximo / passo) * passo;
  const marcas = [];
  for (let v = inicio; v <= fim + passo / 2; v += passo) marcas.push(Math.round(v * 1e6) / 1e6);
  return marcas; // barra: chame com minimo = 0; linha: escreva o início do eixo na figura
}
marcasRedondas(0, 4612);     // [0, 1000, 2000, 3000, 4000, 5000]
marcasRedondas(2.2, 3.1, 4); // [2, 2.5, 3, 3.5]

Com a escala decidida e escrita, a figura ainda precisa de um estilo que não compita com o dado e de uma notação que distinga realizado, meta e período anterior sem legenda nova a cada painel. É o assunto da seção Estilos e notação.

Gaste a cor em um lugar só e deixe a notação dizer o que cada marca significa

O painel anterior da agência Mirante (exemplo didático, agência fictícia) tinha 12 cores, uma por cliente, e a cor de cada um mudava quando Helena filtrava a lista. A Padaria Horizonte era azul numa tela e laranja na seguinte, e a diretora passou a desconfiar de todo gráfico, porque não sabia se estava olhando a mesma coisa. O estilo de um painel denso existe para o contrário disso: a pessoa aprende uma vez o que cada marca quer dizer e reaproveita esse aprendizado em todas as células, todos os meses. Esta seção fixa o orçamento de cor, a notação de cenário, as marcas e a tipografia que o painel inteiro repete.

Orçamento de cor: um acento, um cinza e três estados reservados

Uma tela com 9 a 11 indicadores e uma matriz de 12 clientes suporta pouquíssima cor. A regra que a Mirante adotou tem quatro linhas. O acento (a cor da marca, --db-accent) marca o período atual e a série que importa. O cinza de contexto (--db-gray) carrega tudo o que serve de comparação: mês anterior, outras equipes, média do mercado. Os três estados (bom, atenção, ruim) ficam reservados para o que de fato é um estado, com símbolo e palavra ao lado, como pede o critério 1.4.1 da WCAG 2.2, e nunca viram "série 4". As séries categóricas, quando a pergunta exige distinguir identidades, usam a ordem fixa --db-c1 a --db-c6, atribuída pela entidade e não pela posição na lista; filtrar a lista não repinta quem ficou.

Essa ordem fixa vem de paletas medidas em laboratório, sem espaço para gosto pessoal. A paleta Okabe-Ito, de 2008, tem oito cores distinguíveis pelas formas mais comuns de visão de cores alterada; as paletas "bright" (7 cores) e "muted" (9 cores) de Paul Tol seguem a mesma lógica com mais passos. A skill dataviz, quando existir na máquina da vez, traz o script validate_palette.js, que recebe a lista de cores e o fundo e aplica seis checagens: faixa de claridade, piso de croma, distância ΔE entre pares vizinhos para visão de cores alterada (alvo de 8 ou mais em OKLab multiplicado por 100), piso de 15 para visão comum, e contraste contra o fundo claro e o fundo escuro. O procedimento da skill é claro: a cor entra por último, depois da forma, e a paleta não se aprova no olho, porque ninguém enxerga ΔE.

# Paleta categórica da Mirante, nos dois fundos; sai com erro se um par vizinho falhar
node scripts/validate_palette.js "#14589c,#c8641c,#2a7a4b,#7a4ea6,#b5355a,#5b6b7c" --mode light
node scripts/validate_palette.js "#8fc8ff,#ffb47a,#7fd79b,#c9a7ff,#ff9bb8,#aab8c8" --mode dark
# Com --pairs all, a checagem vale para todo par (dispersão, mapa): acima de 3 séries, facetar

Ênfase: uma série colorida, o resto em cinza

A forma mais subusada em painel é a ênfase. Quando a história é "a Loja Marés caiu 7% em outubro de 2026 enquanto a carteira subiu 10%", o gráfico que conta isso tem uma linha no acento e onze linhas em cinza; doze cores competindo entre si escondem a exceção. A skill dataviz chama esse caso de emphasis e o coloca antes da paleta categórica na escolha da forma: categórica só quando as séries são o assunto. Na Mirante, a matriz de clientes da seção Empilhar, sobrepor, matriz usa cinza em todas as sparklines e pinta de acento só as células cujo desvio em outubro de 2026 passou de 5% para qualquer lado; Helena encontra as exceções em menos de cinco segundos, que é o tempo que a primeira tela tem.

O texto nunca veste a cor da série. Valores, rótulos e legendas ficam nos tokens de texto (--db-ink, --db-muted); a identidade vem de uma marca colorida ao lado (um ponto, um traço curto, um quadrado). Um amarelo de série é ilegível como texto sobre fundo claro, e um azul de série sobre fundo escuro cai abaixo de 4,5:1 com facilidade. A exceção é o rótulo dentro de um preenchimento colorido (segmento de barra, célula de mapa), que fica branco ou quase preto conforme a luminância do preenchimento, com os mesmos dois valores nos dois temas.

Notação de cenário: realizado, ano anterior, plano e previsão

O painel de operação compara o mesmo indicador em quatro cenários: o realizado de outubro, o realizado de setembro (ou do ano anterior, quando a sazonalidade manda), a meta do mês e a previsão de novembro. Sem notação, cada gráfico inventa um jeito de mostrar a meta (linha tracejada num, barra num outro, texto num terceiro) e o leitor reaprende em cada célula. Os IBCS Standards, da IBCS Association, resolvem isso com uma notação semântica que se repete em tabelas, gráficos e textos. A versão 2.0 foi aprovada pela assembleia da associação em 11 de junho de 2026, alinhada à norma ISO 24896 ("Notation for business reporting"), reorganizada em duas partes (Notation e Composition) e publicada sob licença Creative Commons; a versão 1.2 usava CC BY-SA 4.0.

A convenção de preenchimento para os quatro cenários é a seguinte (dado de resultado de busca, sem leitura do PDF da versão 2.0): realizado (AC) em preenchimento escuro sólido, ano anterior (PY) em cinza claro, plano ou orçamento (PL, BU) só com contorno, sem preenchimento, e previsão (FC) com hachura. A notação funciona porque não depende de matiz: imprime em preto e branco, sobrevive ao tema escuro e continua legível em forced-colors. A Mirante adotou a mesma convenção nos cartões, na matriz e na tabela de variância: realizado em --db-ink, mês anterior em --db-gray, meta em contorno e previsão em hachura de 45°.

Em SVG, a hachura vem de um <pattern> definido uma vez e usado em qualquer barra por fill="url(#hachura-45)". O padrão de 8 por 8 pixels com uma linha de 2 px dá a textura visível a 390 px sem virar ruído; a rotação de 45° (ou 135°, no espelho) evita que a hachura seja lida como grade. A textura entra como canal de reserva, sem papel decorativo: na skill dataviz ela fica ligada só por configuração de acessibilidade, impressão ou forced-colors: active, e sempre em um passo da própria rampa da cor, para ter a mesma intensidade visual em todas as séries.

<svg viewBox="0 0 320 160" role="img" aria-labelledby="t-ibcs d-ibcs">
  <title id="t-ibcs">Leads captados: setembro, outubro, meta e previsão</title>
  <desc id="d-ibcs">Outubro teve 4.612 leads, acima da meta de 4.400; setembro 4.180; previsão de novembro 4.700.</desc>
  <defs>
    <!-- Hachura de 45°: a previsão (FC) na notação IBCS -->
    <pattern id="hachura-45" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
      <rect width="8" height="8" fill="var(--db-paper)"/>
      <line x1="0" y1="0" x2="0" y2="8" stroke="var(--db-ink)" stroke-width="2"/>
    </pattern>
  </defs>
  <!-- PY: ano ou mês anterior, cinza claro -->
  <rect x="40"  y="56" width="40" height="84" fill="var(--db-gray)"/>
  <!-- AC: realizado, preenchimento escuro -->
  <rect x="100" y="48" width="40" height="92" fill="var(--db-ink)"/>
  <!-- PL: meta, só contorno -->
  <rect x="160" y="52" width="40" height="88" fill="none" stroke="var(--db-ink)" stroke-width="2"/>
  <!-- FC: previsão, hachura -->
  <rect x="220" y="46" width="40" height="94" fill="url(#hachura-45)" stroke="var(--db-ink)" stroke-width="1"/>
  <text x="60"  y="156" font-size="14" text-anchor="middle" fill="var(--db-muted)">Set</text>
  <text x="120" y="156" font-size="14" text-anchor="middle" fill="var(--db-ink)" font-weight="700">Out</text>
  <text x="180" y="156" font-size="14" text-anchor="middle" fill="var(--db-muted)">Meta</text>
  <text x="240" y="156" font-size="14" text-anchor="middle" fill="var(--db-muted)">Nov (prev.)</text>
</svg>

Marcas finas e dois espaçadores feitos de fundo

O que deixa um painel "carregado" raramente é a quantidade de gráficos; é a espessura das marcas. A skill dataviz fixa as medidas que a Mirante passou a usar em todas as células: barra com no máximo 24 px de espessura (a sobra da faixa vira ar), ponta arredondada de 4 px só no lado do dado e quadrada na linha de base, linha de 2 px com junção redonda, marcador de 8 px ou mais (raio de 4), área de série a cerca de 10% de opacidade (uma lavagem, nunca um bloco; medidas da skill dataviz lidas em 2026), e grade em linha fina de 1 px, sólida, um passo acima do fundo. Grade tracejada é ruído e lê como "projeção".

Separar marcas vizinhas é trabalho do fundo; o contorno acrescenta tinta sem acrescentar dado. Um vão de 2 px na cor da superfície separa cada segmento de uma barra empilhada e cada par de barras encostadas, sempre com a mesma largura em toda a pilha; um anel de 2 px na cor da superfície envolve pontos e marcadores de ponta, para que continuem legíveis onde cruzam uma linha. O anel faz parte do alvo de hover e de foco, o que importa porque um ponto de 8 px sozinho é um alvo que ninguém acerta.

Rótulo direto é seletivo. Um número em todo ponto vira caos e ninguém lê; o guia de Gráficos já pedia rótulo só na ponta, no extremo e na série que a história destaca, com o eixo, a legenda e a tabela carregando o resto. A exceção é a barra segmentada alta (até 28 px, até 6 partes), em que cada bloco imprime a própria fatia na borda de entrada: ela existe para ser lida de uma vez e dispensa eixo. Legenda sempre presente a partir de duas séries; uma série só precisa do título.

Tipografia: 14 px no eixo, tabulares na coluna, proporcionais no número grande

Rótulo e eixo têm 14 px ou mais na dimensão exibida; abaixo disso, a Mirante tira o rótulo e deixa o valor na tabela. Em SVG com viewBox de 720, um texto de 14 vira 7,6 px reais a 390 px de largura, por isso a figura do celular usa um viewBox próprio, de 390, ou troca as colunas por barras horizontais. A regra da coleção é a mesma dos outros seis guias: nenhuma letra abaixo de 12 px a 390 px.

Algarismos tabulares (font-variant-numeric: tabular-nums, cada dígito com a largura do zero) entram só onde números se alinham em coluna: tabela de variância, eixo, lista de clientes. No número grande do cartão e no "hero" da primeira tela, os algarismos ficam proporcionais, porque "121" tabular parece solto em 48 px. O hero usa a mesma família sem serifa de todo o painel, com 48 px ou mais, e existe um só por tela; um segundo número grande deixa de ser hierarquia.

Seis estilos para o mesmo indicador

A figura mostra os leads da Mirante em seis estilos que cabem em pouca altura. A barra IBCS responde "como o mês ficou diante do anterior, da meta e da previsão". O gráfico de marcador (bullet graph), criado por Stephen Few para substituir velocímetros, empilha rótulo, escala, faixas qualitativas em um só matiz (de 2 a 5), medida principal em barra fina e marca comparativa em traço. A sparkline, que Edward Tufte chamou em 2004 de "palavra-gráfico", mostra 10 semanas em 24 px de altura com o período atual no acento. O gráfico de horizonte, estudado por Heer, Kong e Agrawala no CHI de 2009, dobra o desvio em bandas sobrepostas para mostrar dezenas de séries numa altura mínima; o estudo encontrou pontos de transição em que reduzir a altura derruba a precisão da leitura e recomendou bandas em camadas nas alturas pequenas (o limiar numérico não foi lido na fonte, e por isso não entra aqui). A tabela de variância entrega o valor exato, a diferença absoluta e a relativa, alinhadas à direita. O mapa de calor usa um só matiz, do claro ao escuro, e nunca arco-íris.

Leads captados da Mirante em seis estilos: barra IBCS, bullet, sparkline, horizonte, variância e mapa de calor Outubro de 2026 teve 4.612 leads, acima da meta de 4.400 e de setembro, 4.180; a previsão de novembro é 4.700. As semanas de setembro e outubro vão de 780 a 1.004 leads. O churn ficou acima da meta de 2,5% em julho, setembro e outubro. Os doze clientes variam de 255 a 612 leads. Os valores estão na tabela abaixo. 1. Barra IBCS Set Out Meta Nov 4.612 2. Bullet (Few) Leads 0 2.750 5.500 Barra 4.612 · traço = meta 4.400 3. Sparkline 1.004 set (cinza) e out (acento), por semana 4. Horizonte (churn - meta) abr out Banda de 0,3 p.p.; outubro usa duas bandas (+0,6) Acima da meta em julho, setembro e outubro 5. Tabela de variância IndicadorOutvs. set Leads4.612+10,3% CPL (R$)35,10-8,6% Conversão10,9%-0,9 p.p. MRR (R$)428.900+4,0% Churn3,1%+0,5 p.p. 6. Mapa de calor (um matiz) Horizonte V. Verde Marés Compasso Pulso Lume Trilho Serena Bússola Prisma Aurora Ladeira
Seis estilos para o mesmo painel. A barra, o bullet e a sparkline cabem num cartão; o horizonte e o mapa de calor cabem numa matriz; a tabela de variância entrega o valor exato. Dados fictícios de exercício.
Valores dos seis painéis da figura (leads em unidades; churn em % da base; outubro de 2026)
PainelValores
1. Barra IBCSSetembro 4.180; outubro 4.612; meta 4.400; previsão de novembro 4.700.
2. BulletFaixas 0 a 3.800 (abaixo), 3.800 a 4.400 (perto), 4.400 a 5.500 (meta e acima); realizado 4.612; meta 4.400.
3. SparklineSetembro por semana: 790, 835, 870, 905, 780. Outubro: 860, 912, 1.004, 958, 878.
4. HorizonteChurn menos meta de 2,5%, de abril a outubro: -0,3; -0,1; -0,2; +0,3; 0; +0,1; +0,6 p.p.
5. VariânciaLeads +10,3%; CPL -8,6%; conversão -0,9 p.p.; MRR +4,0%; churn +0,5 p.p., sobre setembro.
6. Mapa de calorHorizonte 612; Vale Verde 540; Marés 498; Compasso 431; Pulso 402; Lume 377; Trilho 341; Serena 318; Bússola 296; Prisma 274; Aurora 268; Ladeira 255.

O bullet graph em código, pronto para o cartão

O bullet substitui o velocímetro porque ocupa um sexto da altura e mostra mais: onde o realizado está, onde fica a meta e em que faixa qualitativa o número caiu. As faixas usam passos do mesmo matiz neutro, nunca verde, amarelo e vermelho, porque o status fica reservado para o símbolo com palavra ao lado do número. O bloco abaixo reproduz o painel 2 da figura, com os valores nos atributos para que o agente de código os substitua sem mexer na geometria.

<svg viewBox="0 0 220 60" role="img" aria-labelledby="t-bullet d-bullet" class="db-bullet">
  <title id="t-bullet">Leads captados: 4.612 de uma meta de 4.400</title>
  <desc id="d-bullet">Realizado acima da meta; faixa superior começa em 4.400 numa escala de 0 a 5.500.</desc>
  <!-- Faixas qualitativas: um matiz neutro em três passos (escala 200 px = 5.500 leads) -->
  <rect x="0"   y="8" width="138" height="24" fill="var(--db-soft)"/>
  <rect x="138" y="8" width="22"  height="24" fill="var(--db-line-soft)"/>
  <rect x="160" y="8" width="40"  height="24" fill="var(--db-line)" opacity=".35"/>
  <!-- Medida principal: barra fina, ponta quadrada na base -->
  <rect x="0" y="15" width="167.7" height="10" fill="var(--db-ink)"/>
  <!-- Comparativo: a meta, em traço vertical de 3 px -->
  <line x1="160" y1="4" x2="160" y2="36" stroke="var(--db-ink)" stroke-width="3"/>
  <text x="0"   y="54" font-size="14" fill="var(--db-muted)">0</text>
  <text x="200" y="54" font-size="14" text-anchor="end" fill="var(--db-muted)">5.500</text>
</svg>

Estados com forma e palavra, nunca só cor

O cartão de churn da Mirante em outubro de 2026 mostra 3,1% com um triângulo de alerta e a frase "0,6 p.p. acima da meta; abrir os cancelamentos do mês". O triângulo e a frase fazem o trabalho; a cor de atenção só reforça. Esse arranjo cumpre o critério 1.4.1 da WCAG 2.2 e resiste ao tema escuro, à impressão em preto e branco e ao reflexo na tela, situações em que um em cada 12 homens e todo mundo em algum momento perde a cor. O guia de Ícones traz a oficina que testa os quatro estados sem cor; aqui a regra se aplica ao delta do cartão: seta para cima ou para baixo, sinal escrito e a direção "bom ou ruim" explicada pelo indicador (churn que sobe é ruim, leads que sobem são bons); a cor sozinha fica incapaz de dizer isso.

Empilhe com eixo compartilhado, sobreponha só na mesma escala e faça matriz quando a pergunta se repete

Os 23 gráficos do painel antigo da Mirante (exemplo didático, agência fictícia) estavam dispostos na ordem em que foram pedidos ao longo de dois anos. Um gráfico de leads por semana ficava ao lado de um de churn por trimestre; a coluna de outubro tinha 60 px de largura num e 24 px no outro; e três gráficos de linha dividiam o mesmo espaço com escalas diferentes, uma delas em eixo duplo. Helena rolava quatro telas e ainda assim saía sem saber se a queda de conversão era geral ou de um cliente. Composição é a resposta a esse problema: decidir, antes de desenhar, se os gráficos vão empilhados, sobrepostos ou em matriz, e o que cada arranjo obriga a manter igual.

Empilhar: o mesmo eixo horizontal, alinhado pixel a pixel

Empilhar é colocar dois ou mais gráficos um sobre o outro para que o leitor compare o mesmo momento em métricas diferentes. O arranjo só funciona com três igualdades. O eixo X é o mesmo e fica alinhado: a semana 3 de outubro está na mesma coordenada em leads, em CPL e em conversão, o que exige a mesma margem esquerda para os rótulos do eixo Y e a mesma largura de coluna em todos os painéis. A unidade e a escala do eixo Y podem mudar entre os painéis, porque cada um tem o seu eixo, mas a altura é igual, para que uma variação de 10% ocupe o mesmo espaço visual em todas as linhas, como nos painéis de 2026 da Mirante. E a legenda fica numa linha só, acima da pilha, porque as séries são as mesmas de cima a baixo.

Na Mirante, a pilha de três painéis (leads, CPL, conversão) mostrou que a conversão caiu de 11,8% para 10,9% em outubro de 2026, justamente na semana em que os leads subiram para 1.004; a queda vinha da qualidade do lead, enquanto o volume de atendimento acompanhou. Em um gráfico de eixo duplo as duas curvas teriam se cruzado num ponto arbitrário, e a história teria sido outra. O guia de Gráficos já proíbe o eixo duplo, com a exceção do Pareto; a pilha é o substituto.

Sobrepor: só com a mesma unidade e a mesma escala

Sobrepor é desenhar duas ou mais séries no mesmo plano. A condição é uma só: mesma unidade e mesma escala. Leads de setembro sobre leads de outubro, sim; leads sobre CPL, nunca. Quando a pergunta exige comparar grandezas de unidades diferentes, a saída é indexar as duas a uma base comum (igual a 100 na primeira semana) e sobrepor os índices, ou empilhar. A Mirante indexou leads e reuniões a 100 na semana 1 de setembro de 2026 e viu, no mesmo plano, as reuniões crescerem 2% enquanto os leads cresciam 10%, que é a forma visual da conversão em queda.

As camadas de uma sobreposição obedecem a uma ordem fixa de peso: área da série em cerca de 10% de opacidade por baixo (especificação da skill dataviz, 2026), linha de 2 px por cima, marcador de 8 px com anel de 2 px na cor do fundo nas pontas. Até quatro séries com legenda; a quinta exige rótulo direto na ponta; a partir de oito, a sobreposição vira matriz. A regra de "cor por entidade" da seção Estilo e notação vale aqui com mais força: filtrar um cliente não pode repintar os que ficaram.

Matriz de múltiplos pequenos: a mesma pergunta, repetida na mesma escala

Matriz de múltiplos pequenos (em inglês, small multiples) é o arranjo que repete o mesmo gráfico, com a mesma escala, uma vez por categoria. É a forma que mais cabe numa tela densa e a que menos engana, porque o olho compara formas iguais em posições iguais. O Power BI chama o recurso de small multiples ou trellising: a documentação do Microsoft Learn explica que ele divide um visual em versões de si mesmo, só em barra, coluna, linha e área, numa grade padrão de 2×2 que vai até 6×6, com um eixo Y por linha e um eixo X por coluna, sincronizados; linha de tendência, previsão e zoom de eixo ficam desligados nesse modo. Essas restrições do produto são, na prática, as boas regras do arranjo.

A Mirante montou a matriz dos 12 clientes em 3×4 a 1280 px (até 6 células por linha é o teto de leitura; a 390 px vira uma coluna), todas as células na mesma escala de 0 a 140 leads por semana, ordenadas por volume de outubro em vez de ordem alfabética, porque a ordem por valor já é uma leitura. Cada célula traz o nome, a variação escrita sobre setembro e uma sparkline de cinco semanas; as células com desvio acima de 5% sobre setembro de 2026 para qualquer lado recebem o acento, e as demais ficam em cinza, como ensina a ênfase. Acima de 12 categorias, a matriz agrupa as menores em "Outros" ou vira um carrossel de páginas de 6 a 12 células, que é o que a Mirante fez quando a carteira passou a 19 clientes.

Leads por semana de outubro de 2026 nos 12 clientes da Mirante, em matriz 3 por 4 na mesma escala Doze sparklines de cinco semanas, todas de 0 a 140 leads por semana, ordenadas do maior ao menor volume. Padaria Horizonte lidera com 612 leads no mês e alta de 9%; Loja Marés e Studio Prisma caem 7% e 11%; Escola Compasso e Cursos Bússola sobem 12% e 15%. Os valores semanais estão na tabela abaixo. Horizonte+9% Vale Verde+4% Marés-7% Compasso+12% Pulso+2% Lume-3% Trilho+6% Serena+1% Bússola+15% Prisma-11% Aurora+5% Ladeira0%
Matriz 3×4 dos 12 clientes, mesma escala (0 a 140 leads por semana), ordem por volume de outubro, acento só nas células com desvio acima de 5% sobre setembro. Dados fictícios de exercício.
Leads por semana de outubro de 2026 (S1 a S5), total do mês e variação sobre setembro, por cliente da Mirante
ClienteS1S2S3S4S5Outubrovs. setembro
Padaria Horizonte110118132130122612+9%
Clínica Vale Verde100105115112108540+4%
Loja Marés112104989490498-7%
Escola Compasso7080929891431+12%
Academia Pulso7880848278402+2%
Imobiliária Lume8078767271377-3%
Oficina Trilho6266727170341+6%
Pet Serena6263656464318+1%
Cursos Bússola4855626764296+15%
Studio Fotográfico Prisma6458545048274-11%
Dental Aurora5052565654268+5%
Café Ladeira51505251512550%

A grade de 12 colunas e o bento que se adapta ao contêiner

A tela inteira do painel usa uma grade de 12 colunas com vão de 16 px e cartões com 24 px de preenchimento interno, os mesmos números do guia de Tailwind. O arranjo em "bento" (cartões de tamanhos diferentes encaixados como numa marmita japonesa) ganha sentido quando cada tamanho carrega uma hierarquia: o hero ocupa 4 colunas, os indicadores 2 cada, a matriz 8 e a tabela de variância 4. A adaptação não vem da largura da janela, e sim do contêiner, com @container: abaixo de 640 px o bento vira duas colunas e abaixo de 400 px vira uma, o que faz o mesmo cartão funcionar dentro da gaveta de detalhe e na página inteira. As áreas nomeadas da grade mantêm a ordem de leitura e de tabulação igual à ordem visual, que é o que o leitor de tela e o teclado seguem.

/* Grade do painel: 12 colunas, vão 16 px, cartão com padding 24 px.
   A adaptação segue o contêiner (a página ou a gaveta) em vez da janela. */
.db-painel { container-type: inline-size; container-name: painel; }
.db-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 16px;
  grid-template-areas:
    "hero hero hero hero kpi1 kpi1 kpi2 kpi2 kpi3 kpi3 kpi4 kpi4"
    "matriz matriz matriz matriz matriz matriz matriz matriz var var var var"
    "pilha pilha pilha pilha pilha pilha pilha pilha var var var var";
}
.db-grid > * { padding: 24px; border: 1px solid var(--db-line-soft); border-radius: var(--db-radius); background: var(--db-paper); }
.db-hero   { grid-area: hero; }
.db-kpi-1  { grid-area: kpi1; } .db-kpi-2 { grid-area: kpi2; } .db-kpi-3 { grid-area: kpi3; } .db-kpi-4 { grid-area: kpi4; }
.db-matriz { grid-area: matriz; }
.db-var    { grid-area: var; }
.db-pilha  { grid-area: pilha; }

/* Dentro de uma gaveta ou coluna estreita, o mesmo HTML reempilha em duas colunas. */
@container painel (max-width: 640px) {
  .db-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-areas:
    "hero hero" "kpi1 kpi2" "kpi3 kpi4" "matriz matriz" "var var" "pilha pilha"; }
}
/* Celular: uma coluna, ordem de leitura preservada. */
@container painel (max-width: 400px) {
  .db-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas:
    "hero" "kpi1" "kpi2" "kpi3" "kpi4" "matriz" "var" "pilha"; }
}

Linhas de base alinhadas e um hero por tela

Cartões lado a lado precisam de linhas de base alinhadas: o rótulo na mesma altura, o número grande na mesma altura, a sparkline na mesma altura. Sem isso, o olho lê a diferença de posição como se fosse informação. A Mirante fixou a estrutura do cartão em três linhas de altura definida (rótulo 20 px, valor 48 px, delta e sparkline 32 px) e usou align-items: start na grade. Um único hero por tela; quando dois indicadores disputam o título de "número mais importante", o painel ainda não sabe qual decisão alimenta, e volta à pergunta da seção que abre o guia.

A densidade tem teto. Até 11 indicadores na primeira tela, até 6 células de matriz por linha a 1280 px e 1 a 390 px, até 4 séries sobrepostas, até 3 painéis empilhados por bloco. Acima disso, a tela ganha uma segunda página pelo carrossel ou um filtro que troca o recorte, e a primeira tela continua cabendo em cinco segundos. O painel que atualiza sozinho calcula a manchete (o número que mais se afastou da meta) e a coloca no hero, como o guia de Gráficos recomenda.

A matriz no ECharts 6: um sistema de coordenadas que funciona como tabela

O Apache ECharts 6.0.0, de 30 de julho de 2025, trouxe o sistema de coordenadas em matriz, descrito pela documentação como a forma de combinar tipos de gráfico e componentes "como uma tabela": cada célula da matriz recebe um grid próprio, e cada grid recebe eixos e séries. A versão 6.1.0, de 19 de maio de 2026, acrescentou matrix.x.length e matrix.y.length para criar a matriz sem cabeçalho e triggerEvent nas células, o que permite abrir a gaveta de detalhe do cliente ao tocar na célula. O bloco abaixo monta a matriz da figura com os mesmos dados; os eixos Y de todas as células compartilham min: 0 e max: 140, que é a regra de ouro do arranjo, e as cores saem de getComputedStyle para acompanhar o tema. A opção é longa de propósito: um agente de código tende a gerar uma célula e esquecer a escala compartilhada nas outras onze.

// ECharts 6.1.0 vendorizado na mesma origem (a CSP bloqueia CDN de terceiros).
const echarts = await import("/assets/vendor/echarts/6.1.0/echarts.esm.min.js");
const estilo = getComputedStyle(document.documentElement);
const cor = (nome) => estilo.getPropertyValue(nome).trim();

const clientes = [
  ["Horizonte", [110, 118, 132, 130, 122], 9], ["Vale Verde", [100, 105, 115, 112, 108], 4], ["Marés", [112, 104, 98, 94, 90], -7],
  ["Compasso", [70, 80, 92, 98, 91], 12], ["Pulso", [78, 80, 84, 82, 78], 2], ["Lume", [80, 78, 76, 72, 71], -3],
  ["Trilho", [62, 66, 72, 71, 70], 6], ["Serena", [62, 63, 65, 64, 64], 1], ["Bússola", [48, 55, 62, 67, 64], 15],
  ["Prisma", [64, 58, 54, 50, 48], -11], ["Aurora", [50, 52, 56, 56, 54], 5], ["Ladeira", [51, 50, 52, 51, 51], 0],
];
const COLUNAS = 3, LINHAS = 4;

const option = {
  // 6.1: matriz sem cabeçalho, só com o número de colunas e linhas
  matrix: { x: { length: COLUNAS }, y: { length: LINHAS }, body: { itemStyle: { borderColor: cor("--db-line-soft") } } },
  grid: clientes.map((_, i) => ({ coordinateSystem: "matrix", coord: [i % COLUNAS, Math.floor(i / COLUNAS)], top: 28, bottom: 8, left: 12, right: 12 })),
  xAxis: clientes.map((_, i) => ({ gridIndex: i, type: "category", data: ["S1", "S2", "S3", "S4", "S5"], show: false })),
  // A MESMA escala em todas as células: sem isso a matriz mente
  yAxis: clientes.map((_, i) => ({ gridIndex: i, type: "value", min: 0, max: 140, show: false })),
  title: clientes.map(([nome, , delta], i) => ({
    text: `${nome}  ${delta > 0 ? "+" : ""}${delta}%`, textStyle: { fontSize: 14, color: cor("--db-ink") },
    // o título vai para a célula correspondente
    coordinateSystem: "matrix", coord: [i % COLUNAS, Math.floor(i / COLUNAS)], left: 12, top: 6,
  })),
  series: clientes.map(([, serie, delta], i) => ({
    type: "line", xAxisIndex: i, yAxisIndex: i, data: serie, symbol: "circle", symbolSize: 8, showSymbol: false,
    lineStyle: { width: 2, color: Math.abs(delta) > 5 ? cor("--db-accent") : cor("--db-gray") },
    itemStyle: { color: Math.abs(delta) > 5 ? cor("--db-accent") : cor("--db-gray") },
    emphasis: { focus: "series" },
  })),
  tooltip: { trigger: "axis", valueFormatter: (v) => `${v} leads` },
  aria: { enabled: true, decal: { show: true } },
};
const grafico = echarts.init(document.getElementById("matriz-clientes"), null, { renderer: "svg" });
grafico.setOption(option);
// Tema: ao trocar data-theme, releia os tokens e aplique de novo
new MutationObserver(() => grafico.setOption(option, { replaceMerge: ["series", "title"] }))
  .observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });

Ordem de leitura e de tabulação

A ordem em que o DOM apresenta os cartões é a ordem em que o teclado e o leitor de tela os percorrem, e as áreas nomeadas da grade mantêm essa ordem igual à visual. Dois erros comuns em painel gerado por agente de código: reordenar cartões com order no CSS, o que desalinha o Tab da tela, e colocar a legenda dentro de cada célula da matriz, o que faz o leitor de tela ouvir "setembro, outubro" doze vezes. Legenda uma vez, acima da matriz; cada célula com o nome do cliente como título e a variação escrita no texto, e a tabela gêmea logo abaixo, como a seção Acessibilidade do painel detalha.

Aprenda o modelo mental do Power BI e construa o equivalente com HTML, CSS, JavaScript e bibliotecas abertas

Helena, diretora de operações da Mirante (exemplo didático, agência fictícia), usou Power BI em outra empresa e trouxe de lá o vocabulário: dashboard, tile, slicer, drillthrough, bookmark. Otávio e Camila não tinham licença nem modelo semântico, mas tinham um portal em HTML e um agente de código. A decisão da equipe foi tratar o Power BI como referência de produto em vez de ferramenta de trabalho: cada recurso que Helena pedia pelo nome foi traduzido para um padrão da web que o portal já ensina. Esta seção faz essa tradução, recurso por recurso, com o que a documentação do Microsoft Learn diz sobre cada um. Nada aqui descreve funcionalidades da Leadlovers; o assunto é o que o produto da Microsoft ensina sobre painéis.

Dashboard de uma página, relatório de várias

A página "Dashboards for designers" do Microsoft Learn, atualizada em 23 de julho de 2026, abre com uma definição que serve a qualquer painel: "A Power BI dashboard is a single page, often called a canvas, that tells a story through visualizations. Because it's limited to one page, a well-designed dashboard contains only the highlights of that story." As visualizações do dashboard são tiles, fixadas (pinned) a partir de relatórios, e cada relatório se apoia em um modelo semântico. A tabela da mesma página separa as duas coisas com clareza, e a Mirante a usou para decidir o que fica na primeira tela e o que vai para a gaveta de detalhe.

Dashboard e relatório no Power BI, conforme a tabela do Microsoft Learn (23 de julho de 2026), e a leitura que a Mirante fez para a web
CapacidadeDashboardRelatórioNa web da Mirante
PáginasUma páginaUma ou maisPrimeira tela de uma página; detalhe em gaveta ou página própria.
Fontes de dadosUm ou mais relatórios e modelosUm único modelo semânticoA primeira tela reúne vários conjuntos; cada gaveta lê um só.
Drill nos visuaisSó ao fixar a página inteiraSimA primeira tela não tem drill; a gaveta tem.
Filtro e slicerNão ("You can't filter or slice a dashboard")SimA primeira tela tem só o período; a gaveta tem a linha de filtros.
AlertasSim, em tiles, em certas condiçõesNãoAlerta por e-mail sai da primeira tela, nunca do detalhe.
Disponível no DesktopNão (só no serviço)SimSem equivalente: tudo é HTML servido.

A lição que atravessa a tabela: o dashboard é a entrada, com os destaques e sem filtro; o relatório é o lugar da exploração. Misturar os dois na mesma tela foi o defeito do painel antigo de 23 gráficos, e é o defeito mais comum em painel gerado por agente de código, que coloca um filtro em cada cartão.

Design tips do Learn, em uma lista para colar no pedido

A página "Tips and tricks for creating reports" do Microsoft Learn reúne regras que a Mirante copiou para o contrato do painel: "Tell a story and keep it to one screen"; "Make the most important information biggest"; "Put the most important information in the upper corner"; evitar 3D, eixo truncado e, quando outro visual serve, pizza, rosca e velocímetro; "Pie charts are best if they have fewer than eight categories"; não passar de 3 ou 4 algarismos (3,4 milhões em vez de 3.400.000); não misturar medidas grandes e pequenas na mesma escala; ordenar por medida para destacar o maior e o menor; manter escalas, ordem das dimensões e cores consistentes. A única regra do Learn que o portal substitui é a do gráfico combinado com segundo eixo para medidas de escalas diferentes: aqui a resposta é empilhar ou indexar, como a seção Empilhar, sobrepor, matriz explica.

Cartão novo, reference labels e small multiples

O cartão (card) do Power BI é o "tile" de número grande. Conforme resultados de busca (sem leitura da página primária), o novo visual de cartão passou a ser o padrão do Power BI Desktop em novembro de 2025, aceita várias medidas num só contêiner, ganhou reference labels (rótulos de referência: meta, mês anterior, variação, ao lado do número) e small multiples (um cartão por categoria), e o Multi-row Card legado caminha para a aposentadoria. Esse desenho é o mesmo do cartão de indicador da Mirante: rótulo, número, delta com referência nomeada e sparkline, descrito na seção Anatomia da primeira tela. Em HTML, o cartão é um <article class="db-kpi">; em React, um componente com as mesmas quatro partes; a "categoria" do small multiple vira uma lista de cartões gerada por map.

Cálculos visuais: o que RUNNINGSUM, MOVINGAVERAGE e PREVIOUS querem dizer

A documentação do Microsoft Learn define: "A visual calculation is a DAX calculation that runs directly on a visual instead of in the semantic model". O cálculo fica guardado no visual, opera sobre os dados já agregados e por isso costuma ser mais rápido que uma medida equivalente; o exemplo da página é Running sum = RUNNINGSUM([Sales Amount]). As funções principais têm nomes que explicam a si mesmas: RUNNINGSUM (soma acumulada até a linha), MOVINGAVERAGE (média móvel das últimas N linhas), PREVIOUS e NEXT (o valor da linha anterior e da seguinte), FIRST e LAST, COLLAPSE e EXPAND (subir e descer um nível da hierarquia), LOOKUP. Duas limitações importam para a tradução: cálculos visuais não podem ser fixados em dashboards e ficam indisponíveis em Key Influencers, Decomposition Tree, Q&A e visuais customizados. Conforme resultado de busca (sem leitura da página primária), o resumo de setembro de 2026 do Power BI colocou os cálculos visuais em small multiples como disponíveis em geral.

Na web, os mesmos cálculos são funções de poucas linhas sobre o array de uma série, ou funções de janela no SQL que alimenta o painel. O que a Mirante fixou foi a regra, independente da ferramenta: a média móvel de 4 semanas do CPL só entra no gráfico com a janela escrita no subtítulo ("média móvel de 4 semanas") e com os valores brutos na tabela gêmea, porque uma média móvel sem janela declarada é um número que ninguém consegue conferir.

// Equivalentes em JavaScript dos cálculos visuais do Power BI, sobre uma série ordenada no tempo.
export const somaAcumulada = (serie) => serie.map((v, i) => serie.slice(0, i + 1).reduce((a, b) => a + b, 0));          // RUNNINGSUM
export const mediaMovel = (serie, janela) => serie.map((_, i) => {                                                     // MOVINGAVERAGE
  if (i + 1 < janela) return null;                       // sem janela completa, sem valor: a lacuna continua lacuna
  const fatia = serie.slice(i + 1 - janela, i + 1);
  return fatia.reduce((a, b) => a + b, 0) / janela;
});
export const anterior = (serie) => serie.map((_, i) => (i === 0 ? null : serie[i - 1]));                              // PREVIOUS
export const variacao = (serie) => serie.map((v, i) => (i === 0 || serie[i - 1] === 0 ? null : (v - serie[i - 1]) / serie[i - 1]));

// Leads por semana de setembro e outubro de 2026 (dados de exercício)
const leads = [790, 835, 870, 905, 780, 860, 912, 1004, 958, 878];
mediaMovel(leads, 4);   // [null, null, null, 850, 847.5, 853.75, 864.25, 888.5, 932, 952]
somaAcumulada(leads).at(-1); // 8792

-- A mesma coisa em SQL, com funções de janela (PostgreSQL)
-- SELECT semana, leads,
--   SUM(leads)  OVER (ORDER BY semana ROWS UNBOUNDED PRECEDING)          AS soma_acumulada,
--   AVG(leads)  OVER (ORDER BY semana ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS media_movel_4,
--   LAG(leads)  OVER (ORDER BY semana)                                    AS anterior
-- FROM leads_semana WHERE agencia = 'mirante';

Drillthrough, bookmarks e cross-filter viram URL, gaveta e um filtro só

Drillthrough, na documentação do Learn, é a navegação para uma página de detalhe "filtrada para um valor específico", acionada pelo menu de contexto de um ponto do gráfico; ao configurar o campo de drillthrough, o Power BI acrescenta sozinho um botão Voltar à página de destino. O equivalente web é a gaveta de detalhe aberta por pushState, com o Voltar do navegador fechando a gaveta e devolvendo o foco ao cartão, exatamente como a oficina de gaveta e histórico do guia de Navegação ensina. Na Mirante, tocar na célula da Loja Marés abre a gaveta com o funil daquele cliente, e a URL passa a ter ?cliente=mares.

Bookmarks guardam, segundo o Learn, a página atual, os filtros, o estado dos slicers, a seleção e o destaque cruzado dos visuais, a ordenação, o nível de drill e a visibilidade do painel de seleção; têm as opções Data, Display e Current page; e um slicer acrescentado depois limpa o bookmark. Na web, o bookmark é a URL: período, cliente, métrica e ordenação vivem nos parâmetros, e "salvar um bookmark" é copiar o link. O aceite é o do guia de Gráficos: abrir o link em outra máquina e ver a mesma seleção.

Cross-filter e cross-highlight (selecionar uma barra e ver os outros visuais filtrarem ou destacarem) são o recurso que mais encanta e mais confunde. A tradução da Mirante segue a regra de interação da skill dataviz: uma linha de filtros acima de tudo, à esquerda, com o período primeiro, e todos os cartões, matriz e tabela lendo o mesmo recorte; nenhum filtro dentro de cartão. Field parameters (o seletor que troca a métrica de um visual) viram um <select> na mesma linha, que troca a série sem repintar as entidades. A seção Interação detalha os quatro padrões.

Small multiples, Gantt e temas escuros

Small multiples no Power BI valem só para barra, coluna, linha e área, numa grade de 2×2 a 6×6 com eixos sincronizados por linha e por coluna, e desligam linha de tendência, previsão e zoom; é a mesma disciplina da matriz da seção anterior. O gráfico de Gantt (tarefas, duração, progresso e marcos numa linha do tempo) entrou como prévia na atualização de outubro de 2026, conforme a página "What's new" do Microsoft Learn com data de 31 de outubro de 2026, que também registra como disponíveis em geral a cor de fundo da área de plotagem, o eixo de hierarquia que expande com field parameters e os bookmarks pessoais que preservam a largura das colunas. Conforme resultado de busca (sem leitura da página primária), o resumo de setembro de 2026 trouxe uma faixa Design com dois temas escuros embutidos. Na web, o tema escuro é um conjunto de tokens trocado por data-theme, com contraste medido nos dois fundos, como o guia de Tailwind ensina.

Acessibilidade: o que o Learn exige do autor do relatório

A página de acessibilidade para autores do Microsoft Learn é um checklist que a web deveria copiar inteiro. Alt+Shift+F11 abre a tabela de dados de um visual ("Show data table"), e Shift+? lista os atalhos. A ordem de Tab se define no painel Selection, e objetos decorativos saem dela. O texto alternativo de um visual tem até 250 caracteres e descreve só o conteúdo, porque o leitor de tela já anuncia título e tipo. O contraste mínimo é 4,5:1 (WCAG 2.1, critério 1.4.3). E a recomendação mais citada: "Don't use tooltips to convey important information", porque leitores de tela não leem o conteúdo da dica. A seção Acessibilidade do painel traduz cada item para HTML e SVG, com a tabela gêmea no lugar do Alt+Shift+F11.

Copilot e a regra do resumo que só lê a tabela

A atualização de outubro de 2026 registra, segundo a página "What's new" do Learn, o "Power BI Report skill" para visuais customizados no Copilot, disponível em geral. O portal adota a mesma ideia com uma condição: o resumo escrito por modelo de linguagem só pode citar números que estejam na tabela gêmea, com a linha e a coluna de cada número indicada, e nunca cor ou forma, como o guia de Gráficos fixa no prompt de resumo verificável. Na Mirante, o resumo de segunda-feira sai desse prompt e passa pelo portão "é fiel à base": total, mínimo, máximo e número de linhas conferem com a tabela antes de o texto entrar no painel.

Tradução completa: recurso, equivalente e onde o portal ensina

Recurso do Power BI, o equivalente construído em HTML, CSS e JavaScript, e o guia deste portal que ensina o padrão
Recurso do Power BIEquivalente na webOnde o portal ensina
Dashboard de uma página com tilesPrimeira tela com hero, cartões e matriz, sem filtro além do períodoAnatomia da primeira tela
Cartão novo com reference labelsarticle.db-kpi com rótulo, valor, delta nomeado e sparklineAnatomia; Gráficos
Small multiplesMatriz de múltiplos pequenos na mesma escala, grade de 2×2 a 6×6Empilhar, sobrepor, matriz; oficina da matriz
Cálculos visuaisFunções sobre o array da série ou janelas no SQL, com a janela escrita no subtítuloEscalas
Drillthrough com botão VoltarGaveta com pushState, Voltar do navegador fecha e devolve o focoNavegação: gaveta e histórico
BookmarksEstado na URL: período, cliente, métrica, ordenaçãoNavegação: catálogo compartilhável; React (nuqs)
Slicers, cross-filter, field parametersUma linha de filtros acima de tudo, período primeiro, seletor de métrica na mesma linhaInteração
Páginas de relatórioCarrossel de painéis por cliente, com marcadores e tecladoCarrossel; oficina do carrossel
Temas escuros da faixa DesignTokens por data-theme, contraste medido nos dois fundosTailwind e tokens
Show data table (Alt+Shift+F11)Tabela gêmea em <details>, sempre no DOMAcessibilidade do painel
Alertas em tilesLimiar escrito no cartão e e-mail disparado pelo serviço que gera o painelInteração (estados)
Copilot e Report skillResumo gerado só a partir da tabela gêmea, com linha e coluna de cada númeroPrompts; Gráficos (resumo verificável)

O que o Power BI não ensina, e o portal cobra: peso da página (o vendor do ECharts tem 1.121.654 bytes e só entra sob demanda), renderização sem JavaScript para os rastreadores de IA, e a prova de aceite medida em 1280 e 390 px. Um produto fechado resolve isso por conta própria; a web resolve com os critérios das oficinas deste guia.

Deixe o filtro, o detalhe e o estado do painel no mesmo lugar em que o gestor os procura

O painel antigo da agência Mirante (exemplo didático, agência fictícia) tinha um filtro de período dentro de cada gráfico. Helena, a diretora de operações, trocava o mês no gráfico de leads e esquecia de trocar no gráfico de receita; a reunião de segunda-feira começava com dois números que não falavam do mesmo mês. O problema estava na interação, com o desenho correto: cada gráfico tinha o seu próprio estado, e o estado nunca saía da tela. Esta seção organiza as sete interações que um painel denso precisa ter (filtrar, recortar o período, ler um valor, abrir um detalhe, destacar um item em todos os gráficos, guardar uma visão e exportar) e dá a cada uma um lugar fixo, um comportamento previsível e um critério de aceite.

Uma linha de filtros acima de tudo, e o período vem primeiro

Os filtros ficam em uma única linha, alinhada à esquerda, acima de todo o conteúdo que eles alteram. O período é o primeiro controle, porque é o que a pessoa mais procura, e vem com predefinições em linhas (hoje, últimos 7 dias, últimos 30 dias, últimos 90 dias) antes do intervalo personalizado; ninguém quer brigar com um calendário para escolher "últimos 30 dias". Depois do período entram cliente, canal e campanha, nessa ordem de frequência de uso na Mirante. Tudo o que está abaixo da linha de filtros obedece à mesma seleção: cartões de indicador, gráficos, carrossel de clientes e a tabela. Se um gráfico precisa de um período próprio, ele pertence a outro painel. O guia de gráficos trata o mesmo princípio em "filtro e período vivem na URL"; a skill dataviz, quando existir na máquina da vez, exige a linha única de filtros no seu passo de interação.

O controle é formulário comum: <select>, <input type="date"> e botões, estilizados para combinar com o painel. Cada controle tem <label> visível. Ao mudar um filtro, a página atualiza a URL, e só depois redesenha; a ordem importa para o passo seguinte.

Filtro e período vivem na URL

O estado do painel é uma URL. "Leads de outubro de 2026, cliente Loja Marés, canal Instagram" vira ?periodo=2026-10&cliente=loja-mares&canal=instagram. Isso resolve três problemas de uma vez. Helena copia o endereço e cola no grupo da equipe, e todo mundo abre a mesma seleção. O botão Voltar do navegador desfaz o último filtro em vez de sair do painel. E a seleção sobrevive ao recarregar da página. O aceite é literal: copiar o link, abrir em outra máquina e ver a mesma seleção, incluindo a ordem da tabela e o cliente aberto no carrossel.

A técnica é URLSearchParams para ler e escrever, history.pushState para gravar uma mudança que a pessoa escolheu e popstate para reagir ao Voltar. Digitar numa busca sem enviar usa replaceState, para não encher o histórico; a oficina de catálogo compartilhável do guia de navegação mostra a mesma regra num catálogo de produtos. O código abaixo é o núcleo que a Mirante usa.

// O estado do painel mora na URL. Os controles escrevem nela; a URL redesenha o painel.
const PADRAO = { periodo: "2026-10", cliente: "todos", canal: "todos", ordem: "leads-desc" };
const PERMITIDOS = {
  periodo: /^\d{4}-\d{2}$/,
  cliente: /^[a-z0-9-]{1,40}$/,
  canal: /^(todos|instagram|google|whatsapp|email|outros)$/,
  ordem: /^(leads|cpl|conversao)-(asc|desc)$/,
};

function lerEstado() {
  const params = new URLSearchParams(location.search);
  const estado = { ...PADRAO };
  for (const chave of Object.keys(PADRAO)) {
    const valor = params.get(chave);
    if (valor !== null && PERMITIDOS[chave].test(valor)) estado[chave] = valor;
  }
  return estado;
}

function gravarEstado(estado, { substituir = false } = {}) {
  const params = new URLSearchParams();
  for (const [chave, valor] of Object.entries(estado)) {
    if (valor !== PADRAO[chave]) params.set(chave, valor); // só o que difere do padrão
  }
  const url = params.size ? `${location.pathname}?${params}` : location.pathname;
  if (substituir) history.replaceState(estado, "", url);
  else history.pushState(estado, "", url);
}

function aplicar(estado) {
  // Escreve o estado nos controles e redesenha cada bloco; o padrão de redesenho
  // (ECharts, SVG ou tabela) é o de cada peça, mas todos recebem o MESMO estado.
  document.querySelectorAll("[data-filtro]").forEach((controle) => {
    const chave = controle.dataset.filtro;
    if (chave in estado) controle.value = estado[chave];
  });
  document.dispatchEvent(new CustomEvent("painel:estado", { detail: estado }));
}

document.querySelectorAll("[data-filtro]").forEach((controle) => {
  controle.addEventListener("change", () => {
    const estado = { ...lerEstado(), [controle.dataset.filtro]: controle.value };
    gravarEstado(estado);     // o Voltar desfaz este filtro
    aplicar(estado);
  });
});
window.addEventListener("popstate", () => aplicar(lerEstado())); // Voltar só sincroniza
aplicar(lerEstado());        // primeira carga: a URL manda

Repare na lista de valores permitidos: o painel nunca aceita um valor da URL sem validar, porque a URL é texto que qualquer pessoa pode editar. Um cliente que não existe cai no padrão "todos", e o status informa que a seleção foi ajustada.

Caminho do estado no painel: controles escrevem na URL e a URL redesenha gráficos e tabela Quatro caixas ligadas por setas da esquerda para a direita: linha de filtros, URL com os parâmetros, gráficos e cartões, e tabela equivalente. Uma seta de retorno sai do botão Voltar do navegador e entra na URL, mostrando que o Voltar altera a URL e a URL redesenha tudo. Linha de filtros período, cliente, canal URL ?periodo=2026-10 Gráficos e cartões mesmo recorte Tabela mesmos números Voltar do navegador altera a URL e tudo redesenha Um estado, uma origem
Um estado, uma origem. Os controles não redesenham nada diretamente: escrevem na URL, e a URL redesenha cartões, gráficos e tabela com o mesmo recorte. O Voltar do navegador entra pelo mesmo caminho.

Ao recarregar os dados, o quadro fica no lugar

Quando o período muda e os dados novos ainda não chegaram, o painel mantém o desenho anterior com opacidade reduzida (0,55 funciona bem nos dois temas) e um aviso em role="status" dizendo "Atualizando para outubro de 2026". Nada de esqueleto cinza piscando, nada de altura que muda e empurra a tabela para baixo. O esqueleto é para a primeira carga, quando não há nada para mostrar; no refetch (nova busca dos mesmos dados com outro recorte) ele apaga a referência visual que a pessoa estava lendo. Se a resposta demorar mais de 10 segundos, o aviso muda para "A fonte está demorando; os números em tela são de setembro" e oferece o botão de tentar de novo. Cada estado de carga tem causa e próxima ação, como a seção de estados do guia de gráficos pede: carregando, atualizando, sem credencial, fonte fora do ar, sem dado no recorte e dado atrasado.

Ler um valor: a mira encontra o X, o cartão mostra todas as séries

Em gráfico de linha, a pessoa mira numa data, nunca numa linha de 2 px. Uma linha vertical fina acompanha o ponteiro e salta para a posição de dado mais próxima; o cartão de leitura (tooltip) lista todas as séries daquele X, com o valor em destaque e o nome da série em segundo plano, porque ali a pessoa já sabe qual série quer e procura o número. Em barra, célula de mapa de calor e ponto, a própria marca é o alvo, e ela reage ao passar o ponteiro (clareia um passo ou ganha contorno). A área de acerto é maior que a marca: um ponto de 8 px ganha uma área transparente de 24 px, e dispersões densas usam o ponto mais próximo em vez de exigir pontaria.

O cartão de leitura nunca é o único caminho até um número. Cada valor também está em rótulo direto, no eixo ou na tabela equivalente abaixo da figura; o Microsoft Learn, na página de acessibilidade de relatórios, resume a regra em uma frase: "Don't use tooltips to convey important information". O cartão aparece também no foco de teclado, com o mesmo conteúdo, e obedece ao critério 1.4.13 da WCAG 2.2: dá para fechar com Esc sem mover o ponteiro, ele continua enquanto o ponteiro está sobre ele e só some quando a pessoa sai. Nomes de série e de cliente entram no cartão por textContent, nunca por concatenação em innerHTML, porque vêm de planilhas e de respostas de API que ninguém revisou.

<!-- A figura é um grupo focável; a leitura é uma região de status já presente no documento -->
<div class="db-plot" tabindex="0" role="group" aria-label="Leads por semana, outubro e setembro de 2026" data-plot>
  <svg viewBox="0 0 720 300" aria-hidden="true">…marcas com data-x e data-valores…</svg>
  <div class="db-tip" role="status" aria-live="polite" hidden></div>
</div>
<script>
(() => {
  "use strict";
  const raiz = document.querySelector("[data-plot]");
  const tip = raiz.querySelector(".db-tip");
  const pontos = [...raiz.querySelectorAll("[data-x]")];   // um grupo por posição no eixo X
  let atual = -1;
  function mostrar(i) {
    const g = pontos[i]; if (!g) return;
    atual = i;
    tip.replaceChildren();                                   // limpa sem innerHTML
    const titulo = document.createElement("strong");
    titulo.textContent = g.dataset.x;                        // "Semana 3"
    tip.appendChild(titulo);
    for (const [serie, valor] of Object.entries(JSON.parse(g.dataset.valores))) {
      const linha = document.createElement("div");
      const v = document.createElement("b"); v.textContent = valor;      // valor na frente
      const s = document.createElement("span"); s.textContent = serie;   // rótulo atrás
      linha.append(v, " ", s); tip.appendChild(linha);
    }
    tip.hidden = false;
    tip.style.transform = `translateX(${g.dataset.px}px)`;  // só transform
    pontos.forEach((p, k) => p.classList.toggle("ativo", k === i));
  }
  function esconder() { tip.hidden = true; atual = -1; pontos.forEach((p) => p.classList.remove("ativo")); }
  raiz.addEventListener("pointermove", (e) => {
    const r = raiz.getBoundingClientRect();
    const x = e.clientX - r.left;
    let melhor = 0;                                          // a mira salta para o X mais próximo
    pontos.forEach((p, k) => { if (Math.abs(p.dataset.px - x) < Math.abs(pontos[melhor].dataset.px - x)) melhor = k; });
    mostrar(melhor);
  });
  raiz.addEventListener("pointerleave", (e) => { if (!tip.contains(e.relatedTarget)) esconder(); });
  raiz.addEventListener("keydown", (e) => {
    if (e.key === "ArrowRight") { e.preventDefault(); mostrar(Math.min(pontos.length - 1, atual + 1)); }
    if (e.key === "ArrowLeft")  { e.preventDefault(); mostrar(Math.max(0, atual - 1)); }
    if (e.key === "Escape") esconder();                      // WCAG 1.4.13: dispensável
  });
  raiz.addEventListener("focus", () => mostrar(pontos.length - 1));
  raiz.addEventListener("blur", esconder);
})();
</script>

Abrir o detalhe sem perder o painel

O detalhe de um cliente abre numa gaveta lateral: um <dialog> aberto com showModal(), com o foco preso dentro, Esc fechando e o foco voltando ao cartão que a abriu. A gaveta grava uma entrada no histórico, para que o botão Voltar do celular feche a gaveta em vez de sair do painel; a oficina de gaveta e histórico do guia de navegação detalha a trava contra dois Voltar seguidos. É o equivalente do drillthrough do Power BI, que, segundo o Microsoft Learn, leva a uma página de detalhe filtrada pelo valor escolhido e acrescenta sozinho um botão Voltar. No painel web, a gaveta recebe o mesmo estado da URL mais o cliente aberto (&detalhe=loja-mares), então o link com a gaveta aberta também pode ser copiado.

Dentro da gaveta, a ordem é fixa: manchete do cliente (o que mudou e por quê), três indicadores com meta, o gráfico de leads por semana e a tabela de campanhas. Nada que já esteja no painel principal se repete; a gaveta aprofunda.

Destacar um item em todos os gráficos sem repintar os outros

Quando Helena seleciona a Loja Marés na matriz de clientes, a mesma Loja Marés ganha destaque no ranking de leads, na linha de churn e no funil. É o cross-highlight do Power BI aplicado à web, com uma regra que o Power BI não impõe e que importa: os outros itens vão para o cinza de contexto (var(--db-gray)) e mantêm a posição e a cor que tinham. Ninguém é repintado. Um filtro que reduz a lista de 12 para 5 clientes também não muda a cor dos cinco que sobraram, porque a cor segue a entidade em vez da posição dela na lista. A seleção é um parâmetro da URL (&destaque=loja-mares), com replaceState para não entupir o histórico com cada clique de exploração.

Guardar uma visão: o bookmark é uma URL com nome

Um bookmark do Power BI guarda, de acordo com o Microsoft Learn, a página atual, os filtros, o estado dos slicers, a seleção, a ordenação e o nível de drill, com opções para incluir ou não os dados e a visibilidade. No painel web, tudo isso já está na URL; o bookmark é só um nome para ela. A Mirante mantém cinco: "Segunda-feira" (mês atual, todos os clientes, ordem por variação), "Clientes em alerta" (destaque nos clientes com churn acima de 2,5% em 2026), "Instagram" (canal fixo), "Fechamento do mês" (comparação com a meta) e "Apresentação" (sem filtros, cartões grandes). Ficam num <select> ao fim da linha de filtros e num arquivo JSON versionado no repositório, em vez do armazenamento do navegador de uma pessoa só. Quem cria um filtro novo precisa rever os bookmarks, porque uma visão guardada com um parâmetro que deixou de existir cai no padrão sem avisar; o Power BI tem o mesmo tropeço quando um slicer novo é adicionado a uma página com bookmarks antigos.

Comparar períodos lado a lado

A comparação é o modo mais pedido depois do filtro: outubro contra setembro, outubro contra a meta, este trimestre contra o anterior. A regra é uma só: os dois períodos aparecem na mesma escala, lado a lado ou sobrepostos com o período de referência em cinza, nunca em dois eixos. O painel ganha um parâmetro comparar=2026-09, e cada cartão de indicador passa a mostrar valor, variação absoluta e variação relativa: leads 4.612, +432 sobre setembro, +10,3%. Em taxas, a diferença é em pontos percentuais: conversão 10,9%, 0,9 p.p. abaixo de setembro de 2026. A seção de escalas explica por que a variação relativa de uma base pequena engana e quando mostrar o intervalo.

Exportar o recorte com fonte e data

A exportação entrega só as linhas do recorte atual, em CSV com ponto e vírgula (o Excel em português abre direto), com a primeira linha dizendo fonte, data de atualização do dado mais novo e a URL do painel que gerou o arquivo. Helena cola a URL numa apresentação e a diretoria reabre a mesma visão. Exportar tudo ignorando o filtro é o erro mais comum, e o mais caro: o arquivo de 12 clientes vai parar na mão de um cliente que só poderia ver o seu.

Alertas por limiar e estados vazios com próximo passo

O Power BI permite alertas em tiles de dashboard quando um valor cruza um limite, e essa é uma das poucas vantagens do dashboard sobre o relatório na tabela do Microsoft Learn. No painel web, o alerta é um registro de indicador, operador e limite (churn, acima de, 2,5%) avaliado a cada atualização do dado; o cartão do indicador ganha o símbolo de alerta e a frase "Churn em 3,1%, acima do limite de 2,5% desde 14 de outubro de 2026", e um e-mail sai para quem assinou o alerta. Cor é reforço: o símbolo e a frase comunicam o estado sem ela, como a oficina de estados do guia de ícones exige.

Um recorte sem dado nunca mostra um gráfico vazio nem um zero. "Nenhum lead do canal E-mail para a Dental Aurora em outubro" vem acompanhado do próximo passo: ver todos os canais deste cliente, ou ver o E-mail de todos os clientes. Ausência de dado (null), zero real e base zero são três estados diferentes, com três frases diferentes; a oficina de dados conferíveis do guia de gráficos trata cada um.

Cada interação tem um lugar fixo, um comportamento e um teste que decide se está pronta
InteraçãoOnde ficaComportamentoAceite
FiltrarUma linha acima de tudo, período primeiroEscreve na URL com pushState; tudo abaixo obedeceCopiar o link reproduz a seleção em outra máquina
RecarregarNa própria figuraMantém o desenho anterior a 0,55 de opacidade e avisa em role="status"Nenhum salto de altura; aviso de demora após 10 s
Ler um valorCartão junto da marcaMira no X mais próximo; todas as séries; valor na frenteMesmo cartão no foco de teclado; Esc fecha; tabela tem o número
Abrir detalheGaveta lateral (dialog)Foco preso, Esc fecha, Voltar fecha, foco devolvidoLink com a gaveta aberta reabre a gaveta
DestacarQualquer cartão ou linhaMesmo item em todos os gráficos; os outros viram cinza sem mudar de lugarFiltrar de 12 para 5 não repinta ninguém
Guardar visãoSelect ao fim dos filtrosNome para uma URL; lista versionada no repositórioBookmark antigo com parâmetro extinto avisa em vez de calar
ExportarBotão ao lado da tabelaSó o recorte; CSV com fonte, data e URL na primeira linhaArquivo abre no Excel em português; linhas iguais às da tela

Na segunda versão do painel da Mirante, o filtro de período saiu de dentro dos 23 gráficos e virou um controle só, na linha de cima. Helena abriu a reunião seguinte com uma URL colada no chat, e os dois números do início desta seção passaram a falar do mesmo mês. A seção de laboratório mostra essa linha de filtros funcionando sobre o painel vivo.

Faça os cartões rodarem para os lados, como numa postagem, para paginar o painel sem perder a tela

O painel antigo da agência Mirante (exemplo didático, agência fictícia) listava os 12 clientes em 12 blocos empilhados, um abaixo do outro, e Helena rolava quatro telas para chegar ao Café Ladeira. A segunda versão guardou um cartão por cliente numa faixa horizontal que roda para a esquerda e para a direita com o dedo, com as setas do teclado ou com dois botões, igual ao gesto que qualquer pessoa já conhece das postagens em carrossel do Instagram e do TikTok. O painel principal continua numa tela só; o detalhe dos clientes mora nos cartões, e a pessoa pagina entre eles sem sair do lugar. Esta seção monta esse carrossel com a plataforma nativa, descreve o que o JavaScript acrescenta, mostra quando uma biblioteca compensa e diz quando o carrossel deve virar abas ou lista.

Quando o carrossel é a resposta certa

O carrossel serve para uma sequência de itens do mesmo tipo e do mesmo peso, que a pessoa percorre um a um e dos quais só um ou dois precisam estar à vista por vez: um cartão por cliente, por campanha ou por semana, cada um com o mesmo conjunto de indicadores. Três condições precisam ser verdadeiras ao mesmo tempo. Os itens são comparáveis entre si (mesmos indicadores, mesma escala). A ordem tem sentido (ranking, tempo ou alfabética, escrita no título). E o número de itens cabe na memória de quem navega: de 3 a cerca de 20. Com dois ou três itens de tipos diferentes, use abas; com mais de 20, ou quando a pessoa precisa achar um item pelo nome, use uma lista com busca e ordenação, como a tabela longa do guia de navegação. O carrossel nunca esconde o que decide: a manchete, os indicadores agregados e os alertas ficam fora dele, na primeira tela.

A base é nativa: flex, rolagem horizontal e snap

Sem nenhuma linha de JavaScript, um contêiner com display: flex, overflow-x: auto e scroll-snap-type: x mandatory já roda com o dedo, com a roda do mouse (Shift + roda) e com a barra de rolagem, e para sempre alinhado no início de um cartão graças ao scroll-snap-align: start em cada item. O overscroll-behavior-x: contain impede que o gesto que chega ao fim do carrossel vire um "voltar" do navegador no celular. O scroll-padding-inline deixa uma folga para o cartão não encostar na borda. O scroll-snap-type é Widely available há anos, então essa base funciona em todo navegador que o portal atende.

Cada cartão ocupa 85% da largura a 390 px (o teste de celular da casa em 2026), para que a borda do próximo apareça (o "peek", a espiada que diz "tem mais à direita"); a 640 px de contêiner passa a 3 cartões, e a 960 px a 4. A medida vem de consultas ao contêiner, e não à janela, porque o mesmo carrossel pode viver numa coluna estreita do painel. O content-visibility: auto em cada cartão evita que o navegador desenhe os que estão fora da tela, e o scroll-behavior: smooth fica só no contêiner, nunca no html, e é desligado quando a pessoa pede menos movimento, pelo sistema ou pelo interruptor do portal.

Anatomia do carrossel de cartões na largura de celular: cartão atual a 85%, espiada do próximo, setas e marcadores de página Uma moldura de celular mostra um cartão inteiro ocupando 85% da largura, com nome do cliente, número de leads, variação e linha de tendência, e a borda esquerda do cartão seguinte visível à direita. Abaixo da faixa, dois botões redondos de anterior e próximo, seis marcadores de página com o primeiro preenchido e o texto "Cartão 1 de 6". Largura de 390 px Padaria Horizonte 612 +9% sobre set próximo Cartão 1 de 6 85% de largura:o cartão inteiro 15% de espiada:"tem mais à direita" Botões de 44 px,marcadores com nome Rola com o dedo,com as setas ecom os botões;para alinhadono início do cartão
O cartão inteiro e a espiada do próximo. A 390 px, 85% de largura mostra um cliente por vez e deixa a borda do seguinte à vista. Os botões, os marcadores e o texto "Cartão 1 de 6" são o que o JavaScript acrescenta à rolagem nativa.

O que o JavaScript acrescenta, e por que não pode faltar

A rolagem nativa não tem botões, não anuncia a posição e não deixa o teclado pular de cartão em cartão. O JavaScript liga quatro coisas, e todas degradam bem quando ele falha. Os botões de anterior e próximo chamam scrollIntoView({ inline: "start" }) no cartão-alvo, com behavior: "auto" quando a pessoa pede menos movimento. Os marcadores de página são botões reais, um por cartão, com nome acessível ("Ir ao cartão 3 de 6: Loja Marés") e aria-current="true" no atual; um parágrafo em role="status" diz "Cartão 3 de 6: Loja Marés" a cada troca, para quem usa leitor de tela. As setas do teclado trocam o cartão quando o foco está no trilho ou num cartão, e Home e End vão ao primeiro e ao último; Tab continua entrando nos cartões, porque cada um é um role="group" com rótulo e pode conter links. E o evento scrollend sincroniza o estado depois de um gesto, com a retaguarda de um temporizador de 120 ms sobre scroll onde o evento não existe; um IntersectionObserver marca cada cartão visível, para que um gráfico pesado dentro dele só seja desenhado quando o cartão entra na tela.

O contêiner recebe role="group", aria-roledescription="carrossel" e um aria-label que diz o que está dentro e em que ordem ("Clientes de outubro de 2026, do maior ao menor em leads"). O padrão de carrossel do W3C ARIA Authoring Practices Guide pede exatamente isso, mais a regra de que rotação automática, se existir, pausa ao passar o ponteiro e ao focar; no painel, autoplay nem existe, porque ninguém quer o cartão trocar enquanto lê um número. A regra da casa, escrita no guia de navegação, fecha a conta: carrossel só com CSS e sem retaguarda é proibido, porque o leitor de tela e o teclado ficam sem caminho.

Demonstração: seis clientes da Mirante num carrossel

O carrossel abaixo é o da segunda versão do painel da Mirante, com seis dos doze clientes. Cada cartão traz o nome, os leads de outubro de 2026, a variação sobre setembro e a linha das cinco semanas do mês. Os números são de exercício. Role com o dedo ou com a roda, use as setas e os botões, e troque o tema do portal: a borda do cartão atual e os marcadores mudam com ele. Com movimento reduzido, a rolagem salta sem deslizar.

Padaria Horizonte

612 +9% sobre setembro Leads por semana em outubro: 108, 118, 134, 130 e 122

Clínica Vale Verde

540 +4% sobre setembro Leads por semana em outubro: 102, 106, 116, 110 e 106

Loja Marés

498 -7% sobre setembro Leads por semana em outubro: 112, 104, 98, 94 e 90

Escola Compasso

431 +12% sobre setembro Leads por semana em outubro: 72, 80, 94, 96 e 89

Academia Pulso

402 +2% sobre setembro Leads por semana em outubro: 78, 82, 86, 80 e 76

Imobiliária Lume

377 -3% sobre setembro Leads por semana em outubro: 80, 76, 74, 75 e 72

    Sem JavaScript, os seis cartões continuam rolando para os lados com o dedo e com a barra de rolagem; só os botões e os marcadores deixam de aparecer.

    Leads por semana de outubro de 2026 nos seis clientes do carrossel (dados de exercício)
    ClienteS1S2S3S4S5TotalSobre setembro
    Padaria Horizonte108118134130122612+9%
    Clínica Vale Verde102106116110106540+4%
    Loja Marés112104989490498-7%
    Escola Compasso7280949689431+12%
    Academia Pulso7882868076402+2%
    Imobiliária Lume8076747572377-3%

    Paginar o painel: uma página por cliente, com indicador de pontos

    O mesmo mecanismo pagina um painel inteiro. Em vez de seis cartões pequenos, no painel de 2026 da Mirante cada "página" do carrossel é um painel completo de um cliente (indicadores, gráfico de leads, funil e tabela de campanhas), com flex-basis: 100% e scroll-snap-stop: always, para que um gesto forte nunca pule duas páginas. Os marcadores viram o indicador de pontos que o Instagram popularizou, e a URL ganha &pagina=loja-mares, para que a página aberta possa ser copiada e reaberta, como pede a seção de interação. Os gráficos de cada página só são desenhados quando a página entra na tela (o evento cartao:visivel do código abaixo), o que mantém o custo de 12 painéis ECharts no custo de um; a seção stack mostra o import dinâmico do ECharts que esse evento dispara.

    Carrossel só com CSS, dentro de uma melhoria progressiva

    Chrome e Edge 135, lançados em abril de 2025, trouxeram os pseudo-elementos ::scroll-button(), ::scroll-marker e a propriedade scroll-marker-group, que geram botões de anterior e próximo e marcadores de página sem JavaScript, com foco e teclado tratados pelo navegador; Firefox e Safari não os implementam (dado de resultado de busca, sem leitura do webstatus). A posição desses recursos no Baseline é "Limited availability", então eles só entram atrás de @supports selector(::scroll-marker) e por cima de um carrossel que já funciona com o JavaScript desta seção. A regra da casa para recursos Limited é a mesma do guia de Tailwind: melhoria que degrada, nunca a única implementação. Na prática, a Mirante marca o trilho com data-css-marcadores só no ambiente de testes, até que dois dos três motores suportem o recurso.

    Quando uma biblioteca compensa

    O código nativo desta seção resolve o carrossel de painel. Uma biblioteca entra em três situações: arrasto livre com inércia no computador (o mouse arrastando os cartões, que o CSS não dá), laço infinito com clonagem de cartões, ou plugins de autoplay e sincronização de dois carrosséis (miniaturas e página). O Embla Carousel 8.6.0 (MIT, publicado em 8 de outubro de 2026) é a escolha da casa para esses casos: sem dependências, API pequena, plugins separados (dots, autoplay, wheel) e cerca de 7 KB com gzip (aproximado, confira no PR). O Swiper 14.4.0 (MIT, 30 de outubro de 2026) traz efeitos de transição (cubo, fade, cartas), virtual slides e navegação pronta, por cerca de 40 KB com gzip (aproximado, confira no PR); vale quando o efeito faz parte do produto, como numa vitrine. Nos dois casos, o PR informa o peso medido no bundle e mantém a versão fixa com --save-exact. Dentro do carrossel do Embla, cada cartão continua um role="group" com rótulo, e os marcadores continuam botões com nome: a biblioteca move os cartões, e a semântica é sua.

    // Embla Carousel 8.6.0: só quando o nativo deixa de bastar (arrasto no computador, laço, sincronia).
    import EmblaCarousel from "embla-carousel";
    
    const viewport = document.querySelector(".db-carrossel-trilho");
    const embla = EmblaCarousel(viewport, {
      align: "start",
      containScroll: "trimSnaps",
      dragFree: false,         // snap por cartão, como o CSS
      loop: false,             // laço só com pedido explícito
    });
    const status = document.querySelector(".db-carrossel-status");
    const marcadores = [...document.querySelectorAll(".db-carrossel-marcadores button")];
    
    function pintar() {
      const i = embla.selectedScrollSnap();
      marcadores.forEach((b, k) => { if (k === i) b.setAttribute("aria-current", "true"); else b.removeAttribute("aria-current"); });
      status.textContent = `Cartão ${i + 1} de ${embla.scrollSnapList().length}`;
    }
    marcadores.forEach((b, k) => b.addEventListener("click", () => embla.scrollTo(k)));
    document.querySelector("[data-carrossel-anterior]").addEventListener("click", () => embla.scrollPrev());
    document.querySelector("[data-carrossel-proximo]").addEventListener("click", () => embla.scrollNext());
    embla.on("select", pintar).on("init", pintar);
    // Movimento reduzido: o Embla respeita a opção duration; zero salta sem deslizar.
    if (matchMedia("(prefers-reduced-motion: reduce)").matches) embla.reInit({ duration: 0 });

    Código de referência: HTML, CSS e JavaScript do carrossel nativo

    Os três blocos abaixo são o carrossel da demonstração, prontos para copiar. O HTML traz a semântica; o CSS, a rolagem, o snap, a espiada e a melhoria progressiva; o JavaScript, os botões, os marcadores, o status, o teclado e a sincronização.

    <div class="db-carrossel" role="group" aria-roledescription="carrossel"
         aria-label="Clientes de outubro de 2026, do maior ao menor em leads">
      <div class="db-carrossel-trilho" tabindex="0" aria-label="Cartões dos clientes; use as setas para trocar">
        <article class="db-cartao" role="group" tabindex="0"
                 aria-label="Cartão 1 de 6: Padaria Horizonte" data-nome="Padaria Horizonte">
          <h4>Padaria Horizonte</h4>
          <strong class="db-cartao-valor">612</strong>
          <span class="db-cartao-delta" data-direction="up">…ícone com rótulo… +9% sobre setembro</span>
          <svg class="db-spark" viewBox="0 0 120 36" role="img" aria-labelledby="spk-1">
            <title id="spk-1">Leads por semana em outubro: 108, 118, 134, 130 e 122</title>
            <polyline points="4,32 32,22 60,6 88,10 116,18"/><circle cx="116" cy="18" r="3.5"/>
          </svg>
        </article>
        <!-- …mais cinco cartões, "Cartão 2 de 6" etc.… -->
      </div>
      <div class="db-carrossel-controles">
        <button type="button" data-carrossel-anterior aria-label="Cartão anterior">Anterior</button>
        <ol class="db-carrossel-marcadores" aria-label="Ir a um cartão"></ol>
        <button type="button" data-carrossel-proximo aria-label="Próximo cartão">Próximo</button>
        <p class="db-carrossel-status" role="status" aria-live="polite"></p>
      </div>
    </div>
    <!-- A tabela equivalente, com os mesmos números, vem logo abaixo do carrossel. -->
    .db-carrossel { position: relative; container-type: inline-size; }
    .db-carrossel-trilho {
      display: flex; gap: 12px;
      overflow-x: auto;
      overscroll-behavior-x: contain;      /* o fim do carrossel não vira "voltar" */
      scroll-snap-type: x mandatory;       /* para sempre no início de um cartão */
      scroll-padding-inline: 4px;
      padding: 4px 4px 12px;
      scrollbar-width: thin;
    }
    @media (prefers-reduced-motion: no-preference) { .db-carrossel-trilho { scroll-behavior: smooth; } }
    html[data-llx-motion="reduce"] .db-carrossel-trilho { scroll-behavior: auto; }
    .db-cartao {
      flex: 0 0 85%;                       /* 85%: o cartão inteiro e a espiada do próximo */
      min-width: 0;
      scroll-snap-align: start;
      scroll-snap-stop: always;            /* um gesto forte não pula dois cartões */
      content-visibility: auto;            /* fora da tela, o navegador não desenha */
      contain-intrinsic-size: auto 140px;
      padding: 16px; border: 1px solid var(--db-line-soft); border-radius: var(--db-radius); background: var(--db-paper);
    }
    @container (min-width: 640px) { .db-cartao { flex-basis: calc((100% - 24px) / 3); } }
    @container (min-width: 960px) { .db-cartao { flex-basis: calc((100% - 36px) / 4); } }
    .db-cartao[data-atual="true"] { border-color: var(--db-accent); box-shadow: inset 0 0 0 1px var(--db-accent); }
    .db-carrossel-controles button { min-width: 44px; min-height: 44px; }           /* alvo de painel */
    .db-carrossel-marcadores button { min-width: 24px; min-height: 24px; }         /* alvo mínimo 2.5.8 */
    .db-carrossel-marcadores button::before { content: ""; display: block; width: 10px; height: 10px; border-radius: 50%; background: var(--db-line-soft); }
    .db-carrossel-marcadores button[aria-current="true"]::before { background: var(--db-accent); transform: scale(1.3); }
    
    /* Melhoria progressiva (Chrome e Edge 135): marcadores gerados pelo navegador, por cima do JS. */
    @supports selector(::scroll-marker) {
      .db-carrossel-trilho[data-css-marcadores] { scroll-marker-group: after; }
      .db-carrossel-trilho[data-css-marcadores]::scroll-marker-group { display: flex; gap: 6px; justify-content: center; padding: 8px; }
      .db-carrossel-trilho[data-css-marcadores] .db-cartao::scroll-marker { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--db-line-soft); }
      .db-carrossel-trilho[data-css-marcadores] .db-cartao::scroll-marker:target-current { background: var(--db-accent); }
    }
    (() => {
      "use strict";
      const carrossel = document.querySelector(".db-carrossel");
      const trilho = carrossel.querySelector(".db-carrossel-trilho");
      const cartoes = [...trilho.querySelectorAll(".db-cartao")];
      const anterior = carrossel.querySelector("[data-carrossel-anterior]");
      const proximo = carrossel.querySelector("[data-carrossel-proximo]");
      const lista = carrossel.querySelector(".db-carrossel-marcadores");
      const status = carrossel.querySelector(".db-carrossel-status");
      const semMovimento = () => document.documentElement.dataset.llxMotion === "reduce"
        || matchMedia("(prefers-reduced-motion: reduce)").matches;
      let atual = 0;
    
      const marcadores = cartoes.map((cartao, i) => {          // um botão por cartão, com nome
        const li = document.createElement("li");
        const b = document.createElement("button");
        b.type = "button";
        b.setAttribute("aria-label", `Ir ao cartão ${i + 1} de ${cartoes.length}: ${cartao.dataset.nome}`);
        b.addEventListener("click", () => irPara(i, true));
        li.appendChild(b); lista.appendChild(li);
        return b;
      });
    
      function pintar() {
        cartoes.forEach((c, i) => { c.dataset.atual = String(i === atual); });
        marcadores.forEach((b, i) => { if (i === atual) b.setAttribute("aria-current", "true"); else b.removeAttribute("aria-current"); });
        anterior.disabled = atual === 0;
        proximo.disabled = atual === cartoes.length - 1;
        status.textContent = `Cartão ${atual + 1} de ${cartoes.length}: ${cartoes[atual].dataset.nome}`;
      }
      function irPara(i, focar) {
        atual = Math.max(0, Math.min(cartoes.length - 1, i));
        cartoes[atual].scrollIntoView({ behavior: semMovimento() ? "auto" : "smooth", inline: "start", block: "nearest" });
        pintar();
        if (focar) cartoes[atual].focus({ preventScroll: true });
      }
      anterior.addEventListener("click", () => irPara(atual - 1, false));
      proximo.addEventListener("click", () => irPara(atual + 1, false));
      trilho.addEventListener("keydown", (e) => {                 // setas, Home e End
        if (e.key === "ArrowRight") { e.preventDefault(); irPara(atual + 1, true); }
        else if (e.key === "ArrowLeft") { e.preventDefault(); irPara(atual - 1, true); }
        else if (e.key === "Home") { e.preventDefault(); irPara(0, true); }
        else if (e.key === "End") { e.preventDefault(); irPara(cartoes.length - 1, true); }
      });
    
      function sincronizar() {                                   // depois de um gesto, o cartão à esquerda é o atual
        const esquerda = trilho.scrollLeft;
        let melhor = 0;
        cartoes.forEach((c, i) => {
          if (Math.abs(c.offsetLeft - trilho.offsetLeft - esquerda) < Math.abs(cartoes[melhor].offsetLeft - trilho.offsetLeft - esquerda)) melhor = i;
        });
        if (melhor !== atual) { atual = melhor; pintar(); }
      }
      if ("onscrollend" in window) trilho.addEventListener("scrollend", sincronizar);
      else { let t = 0; trilho.addEventListener("scroll", () => { clearTimeout(t); t = setTimeout(sincronizar, 120); }, { passive: true }); }
    
      const obs = new IntersectionObserver((entradas) => {       // gráfico pesado só quando o cartão entra
        entradas.forEach((en) => { if (en.isIntersecting) en.target.dispatchEvent(new CustomEvent("cartao:visivel", { bubbles: true })); });
      }, { root: trilho, threshold: 0.6 });
      cartoes.forEach((c) => obs.observe(c));
    
      carrossel.dataset.pronto = "";
      pintar();
    })();

    Quando o carrossel vira abas ou lista

    Dois sinais mandam trocar de padrão. Se Helena procura um cliente pelo nome mais do que percorre a sequência, o carrossel vira uma lista com busca, ordenação e cartões em grade, porque o carrossel esconde o que não está na tela e a busca precisa ver tudo. Se os itens deixam de ser comparáveis (um cartão de leads, outro de receita, outro de NPS), viram abas, com estado na URL e rótulo que diz o que há em cada painel, como a seção de abas do guia de navegação descreve. E um terceiro sinal pede atenção: se o cartão atual precisa de mais de uma tela de altura no celular, ele deixou de ser cartão e virou uma página de detalhe, que abre pela gaveta da seção de interação.

    Três padrões para percorrer vários painéis, e o sinal que escolhe cada um
    PadrãoUse quandoQuantos itensEstado na URLTeclado
    CarrosselItens do mesmo tipo, percorridos em ordem, um ou poucos à vistaDe 3 a cerca de 20pagina= ou cartao=Setas trocam; Tab entra no cartão
    AbasVisões diferentes do mesmo objeto, poucasDe 2 a 5aba=Setas trocam a aba; Tab vai ao painel
    Lista com buscaA pessoa procura pelo nome ou compara muitos de uma vezAcima de 20busca=, ordem=, pagina=Tab por linha; busca com atalho

    Na Mirante, o carrossel de clientes ficou; a comparação de canais, que tinha cinco itens de naturezas diferentes, virou abas; e a lista de 91 campanhas ativas virou tabela com busca. Os três padrões convivem no mesmo painel, cada um no lugar em que o gesto da pessoa é o mais natural. A oficina de carrossel de painéis monta, passo a passo, a versão paginada com um painel completo por cliente.

    Use o movimento uma vez na entrada e depois só para mostrar o que mudou

    O painel antigo da agência Mirante (exemplo didático, agência fictícia) animava tudo: cada cartão deslizava de baixo ao aparecer, cada barra crescia de novo a cada filtro, e um velocímetro girava sem parar. Helena desligou as animações no segundo dia. A segunda versão tem um único momento orquestrado, de menos de um segundo, quando o painel abre: os números contam até o valor, as barras crescem a partir da base e a linha de leads se desenha da esquerda para a direita, em cadeia. Depois disso, o movimento só responde a uma ação e só dura o suficiente para a pessoa ver o que mudou. Esta seção fixa os tempos, lista as técnicas da plataforma e das bibliotecas vendorizadas no portal, e diz o que fica de fora.

    Um momento orquestrado, com os números do guia de gráficos

    A entrada do painel é uma sequência de no máximo seis itens, cada um com duração entre 100 e 500 ms, deslocados entre si por 60 a 90 ms, com o último terminando antes de 800 ms. É o mesmo contrato do guia de gráficos. Na Mirante, a cadeia é: manchete (opacidade, 200 ms), quatro cartões de indicador contando até o valor (400 ms, com 70 ms entre um e outro) e a linha de leads se desenhando (420 ms). Soma: o último item começa a 350 ms e termina a 770 ms. Abaixo disso a pessoa percebe uma coisa só, acima disso ela espera. O que anima é sempre transform e opacity; nada de altura, largura, margem ou cor de fundo, porque essas propriedades forçam o navegador a recalcular o layout a cada quadro.

    Mapa de tempo da entrada do painel: seis itens em cadeia entre 0 e 800 milissegundos Eixo horizontal de 0 a 800 milissegundos com marcas a cada 200. Seis barras horizontais, uma por linha: manchete de 0 a 200; cartão 1 de 70 a 470; cartão 2 de 140 a 540; cartão 3 de 210 a 610; cartão 4 de 280 a 680; linha de leads de 350 a 770. Uma linha vertical tracejada em 800 marca o limite. Entrada do painel: tudo termina antes de 800 ms 0 ms200400600800 Manchete Cartão 1 Cartão 2 Cartão 3 Cartão 4 Linha de leads 70 ms entre cartões; 400 ms cada
    Seis itens, uma cadeia. A manchete abre, os quatro cartões contam com 70 ms de intervalo e a linha fecha a sequência a 770 ms. O traço vermelho em 800 ms é o teto; com movimento reduzido, tudo aparece pronto no primeiro quadro.

    O estado final mora no CSS; a animação parte dele

    O erro mais comum em painel gerado por agente de código é escrever opacity: 0 no estado base e contar com o JavaScript para trazer o elemento de volta. Quando o script falha, o painel fica invisível; quando o rastreador de IA lê a página, ele encontra cartões vazios. A regra da casa é a inversa: o CSS base descreve o painel pronto, e a animação só existe dentro de @supports ou de @media (prefers-reduced-motion: no-preference), partindo de um estado inicial declarado por @starting-style. Quem pede menos movimento, pelo sistema ou pelo interruptor do portal (html[data-llx-motion="reduce"]), recebe o estado final de imediato. O bloco abaixo é a barra que cresce a partir da base.

    /* Estado final no CSS base: a barra já está no tamanho certo, com ou sem JavaScript. */
    .db-barra-fill {
      transform-origin: left center;   /* cresce a partir da base, nunca do meio */
      transform: scaleX(1);
    }
    
    /* O movimento só existe para quem não pediu menos movimento. */
    @media (prefers-reduced-motion: no-preference) {
      html:not([data-llx-motion="reduce"]) .db-barra-fill {
        transition: transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
        transition-delay: calc(var(--i, 0) * 70ms);          /* stagger de 70 ms por índice */
      }
      /* @starting-style: o ponto de partida quando o elemento entra no documento. */
      @starting-style {
        html:not([data-llx-motion="reduce"]) .db-barra-fill { transform: scaleX(0); }
      }
    }
    
    /* O rótulo do valor não deforma: fica fora do elemento que escala. */
    .db-barra { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; }
    .db-barra-track { height: 12px; border-radius: 4px; background: var(--db-soft); overflow: hidden; }
    .db-barra-fill { height: 100%; border-radius: 0 4px 4px 0; background: var(--db-c1); }

    Repare que o rótulo numérico fica fora do elemento que escala; se estivesse dentro, o scaleX esticaria os algarismos. O índice --i é escrito no HTML (style="--i: 2"), e a transição de cada barra começa 70 ms depois da anterior.

    Contar até o número com number-flow, e sem ele

    O portal já vendoriza o number-flow 0.6.2, um web component <number-flow> que transiciona um número até outro trocando algarismo por algarismo, com formatação pelo Intl.NumberFormat (locales="pt-BR", format com notation: "compact" ou style: "currency") e método update() para o valor novo. Ele respeita prefers-reduced-motion por conta própria, mas o interruptor do portal precisa ser lido pelo seu código. O import é dinâmico, de /assets/vendor/number-flow/0.6.2/index.js, porque a CSP do portal bloqueia CDN de terceiros e porque 16 KB (aproximado) só valem a pena quando a pessoa está olhando os cartões. Sem o componente, a retaguarda é um contador com requestAnimationFrame de 400 ms; sem JavaScript, o número já está escrito no HTML. Em todos os casos o valor final fica em aria-label ou no texto, para que o leitor de tela leia "4.612" em vez dos algarismos passando.

    // Os números já estão no HTML (estado final). O script só os faz contar até lá.
    (async () => {
      "use strict";
      const semMovimento = document.documentElement.dataset.llxMotion === "reduce"
        || matchMedia("(prefers-reduced-motion: reduce)").matches;
      const alvos = [...document.querySelectorAll("[data-contar]")];   // <strong data-contar="4612">4.612</strong>
      if (!alvos.length || semMovimento) return;                       // sem movimento: fica como está
    
      const formato = new Intl.NumberFormat("pt-BR");
      let pronto = false;
      try {
        await import("/assets/vendor/number-flow/0.6.2/index.js");   // vendor próprio, mesma origem
        await customElements.whenDefined("number-flow");
        pronto = true;
      } catch { pronto = false; }
    
      alvos.forEach((el, i) => {
        const valor = Number(el.dataset.contar);
        const atraso = i * 70;                                         // stagger de 70 ms
        if (pronto) {
          const nf = document.createElement("number-flow");
          nf.locales = "pt-BR";
          nf.setAttribute("aria-label", formato.format(valor));
          nf.update(0);
          el.replaceChildren(nf);
          setTimeout(() => nf.update(valor), atraso);
          return;
        }
        // Retaguarda: contador de 400 ms com requestAnimationFrame.
        const inicio = performance.now() + atraso;
        const passo = (agora) => {
          const t = Math.min(1, Math.max(0, (agora - inicio) / 400));
          const suave = 1 - Math.pow(1 - t, 3);                        // easing out
          el.textContent = formato.format(Math.round(valor * suave));
          if (t < 1) requestAnimationFrame(passo);
        };
        requestAnimationFrame(passo);
      });
    })();

    A linha que se desenha, e o que responde à ação

    A linha de leads se desenha com stroke-dasharray igual ao comprimento do caminho (getTotalLength() em JS, ou um valor generoso como 2000 no CSS) e stroke-dashoffset transicionando desse valor até zero em 420 ms. Depois da entrada, cada movimento tem um gatilho: trocar o filtro cruza o gráfico antigo e o novo em 200 ms (opacidade), trocar o cartão do carrossel usa View Transitions do mesmo documento (document.startViewTransition(), Newly Available desde 14 de outubro de 2025) para a pessoa ver de onde o cartão veio, e o tooltip entra com @starting-style em 150 ms. No ECharts, animationDuration, animationEasing e um animationDelay por índice reproduzem o stagger dentro do gráfico, e um setOption com universalTransition faz a barra de um cliente virar a mesma barra em outra ordenação, em vez de sumir e reaparecer. A opção abaixo é a dos cartões de cliente da Mirante.

    // ECharts 6.1: stagger por índice, teto de 800 ms e transição entre estados.
    const semMovimento = document.documentElement.dataset.llxMotion === "reduce"
      || matchMedia("(prefers-reduced-motion: reduce)").matches;
    
    const option = {
      animation: !semMovimento,
      animationDuration: 400,
      animationEasing: "cubicOut",
      animationDelay: (indice) => Math.min(indice, 5) * 70,       // no máximo 6 itens em cadeia
      animationDurationUpdate: 200,                                // resposta a filtro: 200 ms
      xAxis: { type: "value", min: 0 },
      yAxis: { type: "category", data: ["Padaria Horizonte", "Clínica Vale Verde", "Loja Marés", "Escola Compasso", "Academia Pulso", "Imobiliária Lume"] },
      series: [{
        type: "bar",
        universalTransition: true,                                 // a barra muda de lugar em vez de sumir
        data: [612, 540, 498, 431, 402, 377].map((v, i) => ({ value: v, groupId: `cliente-${i}` })),
        barMaxWidth: 24,
        itemStyle: { borderRadius: [0, 4, 4, 0] },
        label: { show: true, position: "right", formatter: ({ value }) => value.toLocaleString("pt-BR") },
      }],
    };
    grafico.setOption(option);
    // Ao reordenar por variação, o mesmo setOption com os dados em outra ordem anima a troca de posição.

    Animação presa à rolagem, atrás de @supports

    Um painel longo, como a página de um cliente com histórico de doze meses, pode revelar cada bloco quando ele entra na tela com animation-timeline: view(). As scroll-driven animations passaram a Newly Available em 27 de outubro de 2026, com o Firefox 159 (lido no Web Features Explorer em 3 de novembro de 2026), o que as coloca na política de adoção da casa como recurso que entra atrás de @supports (animation-timeline: view()), com o estado final no CSS base e um IntersectionObserver de reserva só se a revelação for essencial. Um detalhe que custou uma tarde à Mirante: o animation-timeline precisa vir depois do shorthand animation, porque o shorthand zera a timeline. Scrub de conteúdo entre 0,5 e 1 segundo de rolagem; fundos entre 1,5 e 2. O guia de React cobre a limpeza dessas animações ao desmontar o componente.

    Motion e GSAP: quando a plataforma não chega

    A plataforma cobre o que esta seção descreveu. O Motion (UMD 13.4.1 vendorizado no portal como window.Motion; 14.9.0 no npm em 3 de novembro de 2026) entra quando é preciso orquestrar com animate, stagger e inView em sequências que dependem do resultado de outra, com a função animate mini pesando cerca de 3 KB em projetos com empacotador. O GSAP 3.15.0, gratuito desde a 3.13, fica reservado a pin (fixar um bloco enquanto a rolagem avança) e sincronia entre elementos, no máximo uma narrativa com pin por página, como o guia de gráficos limita. Nenhuma das duas substitui o CSS para a entrada do painel; as duas obrigam a ler o interruptor de movimento do portal e a limpar tudo ao sair da tela.

    O que fica de fora

    Cada movimento do painel tem gatilho, técnica, duração e comportamento com movimento reduzido
    MovimentoGatilhoTécnicaDuraçãoCom movimento reduzido
    Manchete apareceCarga da páginaopacity com @starting-style200 msJá aparece pronta
    Números contamCarga da páginanumber-flow ou requestAnimationFrame400 ms, 70 ms entre cartõesValor final no HTML
    Barras crescemCarga da páginatransform: scaleX da base400 ms, 70 ms entre barrasscaleX(1) no CSS base
    Linha se desenhaCarga da páginastroke-dashoffset420 msTraço completo
    Troca de filtroAção da pessoaCruzamento por opacity; animationDurationUpdate no ECharts200 msTroca direta
    Troca de cartãoAção da pessoaView Transitions mesmo documento250 msTroca direta
    Tooltip entraPonteiro ou foco@starting-style em opacity150 msAparece sem transição
    Bloco revela ao rolarRolagemanimation-timeline: view() em @supportsScrub de 0,5 a 1 sBloco visível

    Na Mirante, Helena religou as animações na segunda versão e não notou que tinham voltado: o painel abre em menos de um segundo, os números chegam ao valor enquanto ela lê a manchete e, dali em diante, o movimento só aparece quando ela toca em alguma coisa. A seção de laboratório roda essa entrada no painel vivo, com o botão de repetir a sequência.

    Use cada peça da stack pelo que ela faz melhor, com um exemplo que você pode copiar

    Depois de decidir o que o painel responde, para quem e com que gráficos, chega a hora de construir, e a agência Mirante (exemplo didático, agência fictícia) descobriu que o problema deixou de ser "qual biblioteca" e virou "qual peça para qual trabalho". O ECharts desenha o painel operacional; o Observable Plot compila a figura editorial no build; o Vega-Lite guarda o contrato do gráfico que a IA gerou; o D3 entra só no sob medida; o number-flow conta os números; o CSS roda o carrossel; e as skills de design, ícones, dados e revisão fecham a conferência. Esta seção dá, para cada peça, a dica de uso e um exemplo prático sobre os dados da Mirante. Cite ferramentas pelo nome do pacote ou da skill; kits locais, plugins e servidores MCP entram só quando existirem na máquina da vez, sem travar a entrega.

    Apache ECharts 6.1: o painel operacional, importado por demanda

    O ECharts é a primeira escolha para o painel com filtro, zoom e série longa, e o portal já o vendoriza em ESM (/assets/vendor/echarts/6.1.0/echarts.esm.min.js, 1.121.654 bytes sem gzip, licença Apache-2.0). O tamanho manda carregar por import() dinâmico quando o primeiro gráfico interativo entra na tela, com o SVG estático já no HTML como estado sem JavaScript. A versão 6.0, de 30 de julho de 2025, trouxe o tema novo, a troca dinâmica com setTheme, o modo escuro por prefers-color-scheme, o eixo quebrado, o jitter e o beeswarm em dispersão, o gráfico de cordas e o sistema de coordenadas em matriz, que a documentação descreve como combinar tipos de gráfico e componentes "like a table". A 6.1.0, de 19 de maio de 2026, acrescentou dataMin e dataMax para uma extensão de eixo arredondada, containShape e clip para marcas que não estouram, log que ignora valores não positivos, visualMap.seriesTargets e matrix.x.length para matriz sem cabeçalho. Quatro dicas de uso valem para todo painel da casa.

    // Painel da Mirante em ECharts 6.1: dois grids (leads por semana e churn por mês), zoom, decal e tema.
    let echarts = null, grafico = null;
    const token = (nome) => getComputedStyle(document.documentElement).getPropertyValue(nome).trim();
    
    async function montar(el) {
      if (!echarts) echarts = await import("/assets/vendor/echarts/6.1.0/echarts.esm.min.js"); // vendor próprio
      const escuro = document.documentElement.dataset.theme === "dark";
      grafico = echarts.init(el, escuro ? "dark" : null, { renderer: "svg" });              // SVG para leitura
      grafico.setOption(option());
      new ResizeObserver(() => grafico.resize()).observe(el);                               // o cartão muda de largura
      new MutationObserver(() => { grafico.setTheme(document.documentElement.dataset.theme === "dark" ? "dark" : "default"); grafico.setOption(option()); })
        .observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
    }
    
    function option() {
      const cor = [token("--db-c1"), token("--db-gray"), token("--db-c2")];
      const ink = token("--db-ink"), grid = token("--db-grid");
      return {
        color: cor,
        backgroundColor: "transparent",
        textStyle: { fontFamily: "system-ui, sans-serif", fontSize: 14, color: ink },
        aria: { enabled: true, decal: { show: true } },                                     // descrição e textura
        tooltip: { trigger: "axis", axisPointer: { type: "cross" }, valueFormatter: (v) => v.toLocaleString("pt-BR") },
        legend: { top: 0, textStyle: { color: ink } },
        grid: [ { top: 48, left: 56, right: 24, height: "36%" }, { top: "62%", left: 56, right: 24, height: "28%" } ],
        xAxis: [
          { gridIndex: 0, type: "category", data: ["S1", "S2", "S3", "S4", "S5"], axisLine: { lineStyle: { color: grid } } },
          { gridIndex: 1, type: "category", data: ["abr", "mai", "jun", "jul", "ago", "set", "out"], axisLine: { lineStyle: { color: grid } } },
        ],
        yAxis: [
          { gridIndex: 0, type: "value", min: 0, name: "leads", splitLine: { lineStyle: { color: grid } } },
          { gridIndex: 1, type: "value", min: 0, max: 4, name: "churn %", splitLine: { lineStyle: { color: grid } } },
        ],
        dataZoom: [{ type: "inside", xAxisIndex: [1] }],                                     // zoom só na série de meses
        series: [
          { name: "Outubro", type: "line", xAxisIndex: 0, yAxisIndex: 0, data: [860, 912, 1004, 958, 878], lineStyle: { width: 2 }, symbolSize: 8,
            endLabel: { show: true, formatter: "Outubro", color: ink } },
          { name: "Setembro", type: "line", xAxisIndex: 0, yAxisIndex: 0, data: [790, 835, 870, 905, 780], lineStyle: { width: 2 }, symbolSize: 8 },
          { name: "Churn", type: "bar", xAxisIndex: 1, yAxisIndex: 1, data: [2.2, 2.4, 2.3, 2.8, 2.5, 2.6, 3.1], barMaxWidth: 24,
            itemStyle: { borderRadius: [4, 4, 0, 0] }, markLine: { data: [{ yAxis: 2.5, name: "Meta" }], lineStyle: { type: "dashed" } } },
        ],
      };
    }
    
    // Só desenha quando o contêiner entra na tela; o SVG estático fica no HTML até lá.
    const alvo = document.querySelector("[data-echarts]");
    new IntersectionObserver((e, obs) => { if (e[0].isIntersecting) { obs.disconnect(); montar(alvo); } }, { rootMargin: "200px" }).observe(alvo);

    Três opções da 6.x resolvem pedidos que antes exigiam gambiarra. O eixo quebrado (axis.breaks) mostra a Padaria Horizonte com 612 leads ao lado do Café Ladeira com 255 sem esmagar os pequenos, com a quebra desenhada e escrita no eixo. O jitter (jitter e jitterOverlap) espalha 503 reuniões por dia da semana sem sobreposição. E a matriz (matrix) monta a grade de 12 clientes por 6 indicadores em que cada célula é um gráfico pequeno, na mesma escala por coluna; a seção de composição trata quando a matriz é a resposta e a oficina de matriz monta uma.

    Observable Plot 0.6.18: a figura editorial compilada no build

    O Plot gera SVG leve a partir de uma gramática de marcas (Plot.lineY, Plot.barX, Plot.ruleY) e é a escolha para o gráfico que vai num relatório de leitura, numa página de artigo ou num e-mail de resumo: poucas interações, uma conclusão no título, a tabela embaixo. A dica central é renderizar no build, com jsdom ou com o script grafico da skill design-leadlovers quando existir na máquina da vez, e servir o SVG estático; rastreadores de IA não executam JavaScript. O recurso que mais importa num painel é o facet (fx e fy), que produz múltiplos pequenos na mesma escala em uma linha de código. A opção tip: true acrescenta o cartão de leitura quando o Plot roda no navegador.

    // Observable Plot 0.6.18: churn por mês em seis clientes, múltiplos pequenos na MESMA escala (facet).
    import * as Plot from "@observablehq/plot";
    import { JSDOM } from "jsdom";                       // no build; no navegador, use document
    
    const dados = [];                                    // { cliente, mes, churn } para 6 clientes e 7 meses
    for (const [cliente, serie] of Object.entries({
      "Padaria Horizonte": [2.0, 2.1, 2.0, 2.4, 2.2, 2.3, 2.6],
      "Clínica Vale Verde": [1.8, 1.9, 1.9, 2.2, 2.0, 2.1, 2.4],
      "Loja Marés": [2.6, 2.9, 2.8, 3.4, 3.1, 3.3, 4.0],
      "Escola Compasso": [2.1, 2.2, 2.1, 2.5, 2.3, 2.4, 2.7],
      "Academia Pulso": [2.4, 2.6, 2.5, 3.0, 2.7, 2.8, 3.3],
      "Imobiliária Lume": [2.3, 2.5, 2.4, 2.9, 2.6, 2.7, 3.2],
    })) serie.forEach((churn, i) => dados.push({ cliente, mes: ["abr","mai","jun","jul","ago","set","out"][i], churn }));
    
    const { window } = new JSDOM("");
    const svg = Plot.plot({
      document: window.document,
      width: 720, height: 300, marginLeft: 44,
      style: { fontSize: "14px", fontFamily: "system-ui, sans-serif" },
      fx: { label: null },                                // uma coluna por cliente
      y: { domain: [0, 4.5], label: "churn (%)", grid: true },
      x: { label: null },
      marks: [
        Plot.ruleY([2.5], { stroke: "var(--db-warn)", strokeDasharray: "6 4" }),   // meta, igual em todos
        Plot.lineY(dados, { x: "mes", y: "churn", fx: "cliente", stroke: "var(--db-c1)", strokeWidth: 2, tip: true }),
        Plot.dot(dados.filter((d) => d.mes === "out"), { x: "mes", y: "churn", fx: "cliente", r: 4, fill: "var(--db-c1)" }),
        Plot.text(dados.filter((d) => d.mes === "out"), { x: "mes", y: "churn", fx: "cliente", text: (d) => d.churn.toLocaleString("pt-BR"), dy: -10 }),
      ],
    });
    // Injete title e desc antes de gravar o arquivo; a tabela equivalente vai no HTML ao lado.
    process.stdout.write(svg.outerHTML);

    Vega-Lite 6.5.0: o contrato do gráfico que a IA gerou

    O Vega-Lite descreve o gráfico numa spec JSON declarativa (dados, codificação, marcas), e é por isso que ele é a ferramenta de auditoria da casa: quando um agente de código gera um gráfico, pedir a spec em vez do código deixa a revisão ler, em dez linhas, que campo foi para que eixo, se o eixo começa em zero e se a lacuna continuou lacuna. A 6.5.0, de 8 de outubro de 2026, corrigiu o dimensionamento por ResizeObserver no contêiner. As operações repeat e facet montam matrizes de pequenos múltiplos a partir de uma spec só, e resolve: { scale: { y: "shared" } } garante a mesma escala em todas as células.

    {
      "$schema": "https://vega.github.io/schema/vega-lite/v6.json",
      "description": "Leads por canal em outubro de 2026, barras ordenadas, uma cor. Dados de exercício da agência Mirante.",
      "data": { "values": [
        { "canal": "Instagram", "leads": 1845 }, { "canal": "Google", "leads": 1291 }, { "canal": "WhatsApp", "leads": 738 },
        { "canal": "E-mail", "leads": 461 }, { "canal": "Outros", "leads": 277 }
      ] },
      "mark": { "type": "bar", "cornerRadiusEnd": 4, "size": 24, "color": "#185c96" },
      "encoding": {
        "y": { "field": "canal", "type": "nominal", "sort": "-x", "title": null },
        "x": { "field": "leads", "type": "quantitative", "scale": { "zero": true }, "axis": { "format": ",d", "labelFontSize": 14 }, "title": "leads" }
      },
      "config": { "locale": { "number": { "decimal": ",", "thousands": ".", "grouping": [3], "currency": ["R$ ", ""] } }, "font": "system-ui, sans-serif" }
    }

    A cor na spec é a única exceção à regra de tokens, porque o Vega-Lite compila para SVG fora do CSS da página; o build troca o hexadecimal pelo valor do token do tema antes de renderizar, e grava uma versão por tema. Ao auditar uma spec gerada, confira três coisas: "zero": true em barra e área, "sort" explícito em ranking e "invalid": "break-paths" (ou equivalente) para que valor ausente abra um buraco na linha em vez de virar zero.

    D3 7.9.2: só no sob medida, e com escalas que arredondam

    O D3 é um conjunto de módulos para escalas, eixos, formas e layouts, diferente de uma biblioteca de gráficos prontos; ele entra quando o painel precisa de um formato que nenhuma das anteriores oferece, com narrativa e orçamento de projeto. Mesmo fora desse caso, dois módulos valem em qualquer SVG à mão: d3-scale, cujo scaleLinear().nice() arredonda o domínio para limites "bonitos" (0, 300, 600, 900 em vez de 0 a 1.004), e d3-format com formatLocale em pt-BR. A sparkline abaixo tem doze linhas e é a que a Mirante usa nos cartões do carrossel quando os dados vêm de uma API em vez de estarem no HTML.

    // D3 7.9.2 (só d3-scale e d3-shape): sparkline de 5 semanas com escala arredondada.
    import { scaleLinear, scalePoint } from "d3-scale";
    import { line } from "d3-shape";
    
    export function sparkline(valores, largura = 120, altura = 36) {
      const x = scalePoint().domain(valores.map((_, i) => i)).range([4, largura - 4]);
      const y = scaleLinear().domain([Math.min(...valores), Math.max(...valores)]).nice().range([altura - 4, 4]);
      const caminho = line().x((_, i) => x(i)).y((v) => y(v))(valores);
      const ultimo = valores.length - 1;
      return `<svg class="db-spark" viewBox="0 0 ${largura} ${altura}" role="img" aria-label="Leads por semana: ${valores.join(", ")}">
      <path d="${caminho}" fill="none" stroke="var(--db-c1)" stroke-width="2" stroke-linejoin="round"/>
      <circle cx="${x(ultimo)}" cy="${y(valores[ultimo])}" r="3.5" fill="var(--db-c1)" stroke="var(--db-paper)" stroke-width="2"/>
    </svg>`;
    }
    // sparkline([118, 124, 132, 126, 112]) devolve o SVG do cartão da Padaria Horizonte.

    uPlot e Lightweight Charts: telemetria e finanças

    O uPlot 1.6.32 desenha dezenas de milhares de pontos em canvas com cerca de 50 KB e API mínima; é a escolha para um painel de telemetria que recebe um ponto por segundo (tempo de resposta ao lead em tempo real, por exemplo), e exige a camada de acessibilidade à mão, com resumo e tabela em HTML, como a oficina de séries densas do guia de gráficos mostra. O Lightweight Charts 5.2.1, do TradingView, desenha candles, volume e linha de preço em cerca de 35 KB, com a atribuição obrigatória à empresa na tela; entra só em painel financeiro de verdade, nunca para dar "cara de bolsa" ao MRR.

    Recharts, MUI X Charts e ApexCharts: quando o painel é React

    Em projeto React, três opções cobrem o que o ECharts cobre fora dele, cada uma com um custo. O Recharts 3.10.1 compõe gráficos com componentes JSX e liga accessibilityLayer (teclado e leitor de tela) por padrão; custa um bundle que cresce por componente importado. O MUI X Charts 9.17.0 traz teclado ligado e integração com o tema do MUI; custa a dependência do MUI inteiro e a leitura da licença Pro antes de usar zoom ou exportação. O ApexCharts 8.7.0 tem anotações, zoom e exportação prontos para painel investigativo, por cerca de 130 KB com gzip (aproximado, confira no PR). Em todos, o estado do filtro continua na URL (nuqs, como o guia de React pede), e a tabela equivalente continua no DOM.

    number-flow, Motion, Embla e Swiper: as peças de movimento e navegação

    O number-flow 0.6.2 (vendor próprio do portal) faz os indicadores contarem até o valor com update() e formata por Intl; a seção de movimento mostra o import dinâmico e a retaguarda. O Motion (13.4.1 vendorizado, 14.9.0 no npm) orquestra sequências com animate, stagger e inView quando o CSS não basta. O Embla Carousel 8.6.0 e o Swiper 14.4.0 entram no carrossel só com arrasto no computador, laço ou efeito; a seção de carrossel tem o nativo e o bloco mínimo do Embla. Para as quatro, a dica é a mesma: leia o interruptor de movimento do portal, use só transform e opacity, e limpe ao desmontar.

    Tailwind 4.3 e Lucide: tokens das séries e ícones com rótulo

    As cores das séries do painel moram no @theme do Tailwind, com nome de papel (--color-serie-1) em vez de nome de tom; o ECharts e os SVGs leem o mesmo token, então trocar a paleta é trocar um arquivo. O guia de Tailwind explica o @theme inline para apontar variáveis de outro arquivo e o @custom-variant dark para o tema escuro por data-theme. Os ícones vêm do sprite próprio da Lucide (o portal usa a 1.27.0; o npm está na 1.67.0), sempre com rótulo ao lado: no cartão, a seta de tendência vem com "+9% sobre setembro de 2026"; no alerta, o triângulo vem com a frase do limite. O guia de ícones e a skill icones-leadlovers (scripts buscar, gerar-sprite e contraste, quando existirem na máquina da vez) cobrem a família, o sprite e o contraste de 3:1.

    @import "tailwindcss";
    
    /* Tailwind 4.3: tokens das séries com nome de papel; o ECharts lê os mesmos valores por getComputedStyle. */
    @theme {
      --color-serie-1: oklch(48% 0.13 250);   /* leads, série principal */
      --color-serie-2: oklch(62% 0.15 40);    /* meta e referência */
      --color-serie-3: oklch(55% 0.12 150);
      --color-serie-4: oklch(58% 0.14 320);
      --color-serie-contexto: oklch(70% 0.01 250);  /* o cinza das séries que não são o assunto */
      --color-ok: oklch(45% 0.13 150);
      --color-atencao: oklch(50% 0.13 70);
      --color-ruim: oklch(48% 0.17 25);
    }
    @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
    
    /* No tema escuro, só os valores mudam; as classes e a option do ECharts continuam iguais. */
    :root[data-theme="dark"] {
      --color-serie-1: oklch(78% 0.10 250);
      --color-serie-2: oklch(80% 0.12 60);
      --color-serie-contexto: oklch(55% 0.01 250);
    }
    /* As cores de série passam pelo validate_palette.js da skill dataviz nos dois fundos antes do PR. */

    Skill dataviz: forma antes da cor, e a paleta validada por script

    A skill dataviz, quando existir na máquina da vez, impõe um procedimento de sete passos que vale mesmo sem ela: escolher a forma pelo trabalho do dado (magnitude, identidade, polaridade, um número só, mudança no tempo); atribuir cor pelo papel (categórica em ordem fixa, sequencial de um matiz, divergente com cinza no meio, status reservado); validar a paleta com o script validate_palette.js, que mede faixa de claridade, piso de croma, distância entre pares adjacentes para visão de cores alterada (ΔE de 8 ou mais em OKLab vezes 100), piso de 15 para visão normal e contraste nos dois fundos; aplicar as marcas (barra até 24 px, ponta arredondada de 4 px na base reta, linha de 2 px, marcador de 8 px ou mais, vão de 2 px na cor da superfície entre preenchimentos); acrescentar o cartão de leitura; fazer a passagem de acessibilidade (legenda sempre com duas séries ou mais, tabela gêmea, tema escuro escolhido e não invertido); e olhar o resultado renderizado. O comando que a Mirante roda antes de cada PR, nas duas superfícies:

    # Skill dataviz: seis checagens sobre as cores de série, no fundo claro e no escuro.
    node scripts/validate_palette.js "#185c96,#c2571a,#2a7f5a,#8a4f9e,#9aa3ad" --mode light
    node scripts/validate_palette.js "#8fc8ff,#f0a870,#7fd79b,#d9a8f0,#7c8793" --mode dark
    # Saída: pass ou fail por checagem; um FAIL em "normal-vision floor" obriga a reescalonar o par;
    # um WARN de contraste obriga rótulo visível ou tabela, e não pode ser dispensado.

    Skills design-leadlovers e icones-leadlovers: compilar no build e conferir medido

    A skill design-leadlovers reúne os cinco guias anteriores e dois scripts que fecham o ciclo do painel: grafico recebe uma option do ECharts, uma spec Vega-Lite ou um programa do Plot e grava o SVG estático com título e descrição, para o build; conferir abre a URL em 1280 e 390 px, roda o axe, liga o movimento reduzido e passa o detector da Impeccable. A skill icones-leadlovers acha o símbolo (buscar leads procura nas coleções locais, --online consulta o Iconify em inglês), gera o sprite com licenças no topo e mede o contraste do ícone sobre o fundo real. Nenhuma das duas é obrigatória: quando não existirem na máquina da vez, o mesmo trabalho se faz com jsdom e Playwright à mão, e o aceite continua o mesmo.

    # 1. Compilar as figuras estáticas (ECharts, Vega-Lite ou Plot viram SVG com title e desc)
    node scripts/grafico.mjs painel/leads-por-semana.echarts.json --titulo "Leads por semana" --descricao "Outubro cruza setembro na S2"
    node scripts/grafico.mjs painel/leads-por-canal.vl.json --escuro
    
    # 2. Conferir a página publicada em 1280 e 390 px, com axe, movimento reduzido e detector
    node scripts/conferir.mjs https://exemplo.com/painel/?periodo=2026-10
    
    # 3. Detector da Impeccable nas duas larguras (corrigir só contraste, tamanho de texto e foco)
    npx impeccable detect --json https://exemplo.com/painel/
    npx impeccable detect --json --viewport 390x844 https://exemplo.com/painel/
    
    # 4. Ícones: achar, gerar o sprite e medir o contraste sobre o fundo do cartão
    node scripts/buscar.mjs trending-up
    node scripts/gerar-sprite.mjs icones/painel.json --saida site/assets/sprite-painel.svg
    node scripts/contraste.mjs "#1f7a3f" "#ffffff"

    Impeccable, frontend-design e Playwright: a revisão que qualquer pessoa repete

    O detector da Impeccable (npx impeccable detect --json) varre a página publicada sem modelo de linguagem e lista contraste baixo, texto miúdo, foco invisível e padrões suspeitos; rode nas duas larguras, porque metade dos defeitos só aparece a 390 px, e corrija só o que é medido (contraste, tamanho de texto, foco), deixando as escolhas de estilo para a decisão do time. A skill frontend-design entra antes do código: um plano de tokens (quatro a seis cores com nome, tipografia e papéis, layout em uma frase e um esboço) revisto contra os cinco defaults de página gerada por máquina, para que o painel não saia com o cartão arredondado idêntico em tudo e o gradiente de decoração. E o Playwright com @axe-core/playwright fixa o aceite num teste: abre o painel, emula movimento reduzido, confere que nada se move, que o carrossel responde às setas e que a tabela gêmea tem os mesmos números do SVG.

    // Playwright 1.64 + axe: o aceite do painel como teste, em 1280 e 390 px.
    import { test, expect } from "@playwright/test";
    import AxeBuilder from "@axe-core/playwright";
    
    for (const largura of [1280, 390]) {
      test(`painel a ${largura} px: sem violação do axe, sem rolagem horizontal, carrossel por teclado`, async ({ browser }) => {
        const contexto = await browser.newContext({ viewport: { width: largura, height: 844 }, reducedMotion: "reduce" });
        const page = await contexto.newPage();
        await page.goto("/painel/?periodo=2026-10");
    
        const axe = await new AxeBuilder({ page }).analyze();
        expect(axe.violations).toEqual([]);
    
        const larguraDoc = await page.evaluate(() => document.documentElement.scrollWidth);
        expect(larguraDoc).toBeLessThanOrEqual(largura);                         // 390 px sem rolagem horizontal
    
        const trilho = page.locator(".db-carrossel-trilho");
        await trilho.focus();
        await page.keyboard.press("ArrowRight");
        await expect(page.locator(".db-carrossel-status")).toHaveText(/Cartão 2 de 6/);
    
        const primeiroValorDaTabela = await page.locator("table tbody tr:first-child td:first-child").textContent();
        expect(primeiroValorDaTabela.trim()).toBe("118");                        // tabela gêmea igual ao SVG
      });
    }

    Árvore de decisão: qual biblioteca para qual painel

    Seis tipos de painel e a peça que cada um pede, com o peso que o PR precisa confirmar
    Tipo de painelPeçaPesoCondição de uso
    Estático editorial (relatório, artigo, e-mail)Observable Plot ou Vega-Lite no build; SVG servido0 KB no navegadorPoucas interações; conclusão no título; tabela embaixo
    Operacional com filtro, zoom e série longaApache ECharts 6.1 por import dinâmico do vendor1.121.654 bytes sem gzip (vendor do portal)Carregar só quando o primeiro gráfico interativo entra; SVG estático antes
    Telemetria (um ponto por segundo)uPlotCerca de 50 KB (aproximado, confira no PR)Canvas com camada de acessibilidade e tabela à mão
    Financeiro (candles, volume)Lightweight Charts 5.2Cerca de 35 KB (aproximado, confira no PR)Atribuição ao TradingView na tela
    ReactRecharts 3.10, MUI X Charts 9.17 ou ApexCharts 8.7Varia por componente; medir no bundleEstado na URL com nuqs; tabela gêmea no DOM; licença Pro lida
    Sob medida com narrativaD3 7.9 (módulos) e SVG à mãoSó os módulos usadosProjeto com prazo e orçamento; aceite de acessibilidade escrito
    Números, movimento e carrosselnumber-flow, CSS; Motion, Embla ou Swiper só quando crescernumber-flow cerca de 16 KB; Motion UMD 146.976 bytes sem gzip; Embla cerca de 7 KB e Swiper cerca de 40 KB com gzip (aproximados, confira no PR)Interruptor de movimento lido; só transform e opacity

    Na Mirante, o painel ficou com ECharts para os gráficos interativos, Plot no build para as figuras do relatório mensal, number-flow nos cartões e carrossel nativo; o Vega-Lite entrou como formato de auditoria dos gráficos que o agente de código propõe. A seção de versões e licenças fixa os números e as obrigações de cada peça, e a de prompts mostra como pedir cada uma delas a um agente com aceite numerado.

    Fixe a versão, anote a data, leia a licença e meça o peso antes de aprovar a dependência

    O painel antigo da agência Mirante (exemplo didático, agência fictícia) carregava três bibliotecas de gráfico de três CDNs diferentes, sem versão fixa, porque cada bloco tinha sido gerado por um agente de código em uma tarde diferente. Numa segunda-feira, um dos CDNs serviu uma versão nova com outra API e o gráfico de funil sumiu da reunião. A regra que esta seção fixa é a do guia de Tailwind aplicada a gráficos e carrosséis: uma biblioteca principal por projeto, versão exata, data de conferência no comentário do arquivo, peso medido no PR e nenhum script de terceiros na CSP do portal.

    Quadro de versões conferidas em 3 de novembro de 2026

    As versões abaixo foram lidas no registro do npm em 3 de novembro de 2026, com a data da última publicação de cada pacote. A licença vem do campo license do pacote quando foi lida; quando a leitura não aconteceu, a linha diz "dado de memória, confira a licença no pacote". Quadro de versão envelhece em semanas: reconfira antes de citar num PR.

    Bibliotecas da stack de painéis, com versão e data lidas no npm em 3 de novembro de 2026
    BibliotecaVersãoPublicada emLicençaPapel no painel
    Apache ECharts6.1.019/05/2026Apache-2.0 (lida no vendor do portal)Painel operacional com filtro, zoom, matriz e séries longas; vendor próprio em /assets/vendor/echarts/6.1.0/
    Observable Plot0.6.1803/11/2026ISC (dado de memória, confira a licença no pacote)Figura editorial em SVG, múltiplos pequenos por facet, compilada no build
    D37.9.203/11/2026ISC (dado de memória, confira a licença no pacote)Projeto sob medida; escalas e eixos para SVG à mão
    Vega-Lite6.5.008/10/2026BSD-3-Clause (dado de memória, confira a licença no pacote)Spec declarativa que serve de contrato auditável do que a IA gerou
    number-flow0.6.218/07/2026MIT (lida no vendor do portal)Números que contam até o valor; vendor próprio em /assets/vendor/number-flow/0.6.2/
    Motion14.9.0 (portal: 13.4.1)30/10/2026MIT (lida no vendor do portal)Orquestração com animate, stagger e inView quando o CSS não chega
    GSAP3.15.013/04/2026Gratuito desde a 3.13 (dado de memória, confira a licença no pacote)Pin e sincronia, no máximo uma narrativa por página
    Embla Carousel8.6.008/10/2026MIT (dado de memória, confira a licença no pacote)Carrossel com arrasto no computador, laço e plugins
    Swiper14.4.030/10/2026MIT (dado de memória, confira a licença no pacote)Carrossel com efeitos de transição e slides virtuais
    uPlot1.6.3214/03/2025MIT (dado de memória, confira a licença no pacote)Telemetria em tempo real, dezenas de milhares de pontos
    Lightweight Charts5.2.112/08/2026Apache-2.0 com atribuição obrigatória ao TradingView (dado de memória, confira a licença no pacote)Séries financeiras: candles, volume, linha de preço
    Recharts3.10.103/11/2026MIT (dado de memória, confira a licença no pacote)Gráficos em React com accessibilityLayer
    MUI X Charts9.17.030/10/2026Núcleo MIT; recursos Pro comerciais (dado de memória, confira a licença no pacote)Gráficos em React com teclado ligado, dentro do MUI
    ApexCharts8.7.031/10/2026MIT (dado de memória, confira a licença no pacote)Painel investigativo com anotações e zoom
    Tailwind CSS4.3.402/11/2026MIT (dado de memória, confira a licença no pacote)Tokens das séries em @theme e layout do painel
    Lucide1.67.0 (portal: sprite 1.27.0)03/11/2026ISC (lida no guia de ícones)Ícones com rótulo nos cartões, alertas e controles

    Política de adoção: nativo primeiro, biblioteca com peso medido

    A ordem é a mesma de todos os guias desta coleção. Primeiro a plataforma: SVG estático compilado no build, CSS para o movimento, scroll-snap para o carrossel, dialog para a gaveta. Uma biblioteca entra quando o problema cresce (filtro com zoom em série longa, arrasto no computador, dezenas de milhares de pontos), e entra com quatro obrigações escritas no PR: o nome e a versão exata instalada com --save-exact; o peso medido no bundle, com e sem gzip; a licença e a atribuição que ela exige; e a data de conferência no comentário do topo do arquivo, no formato "ECharts 6.1.0, conferido em 3 de novembro de 2026". Uma segunda biblioteca de gráfico no mesmo projeto pede justificativa escrita, porque dois motores de desenho significam dois tooltips, duas regras de cor e dois comportamentos de teclado.

    A CSP do portal não aceita script de CDN de terceiros, e isso é uma decisão, mais do que uma limitação: o arquivo que chega ao navegador é o que está no repositório, copiado de node_modules para /assets/vendor/<nome>/<versão>/ por um script de vendorização com manifesto (nome, versão, licença, bytes e hash de cada arquivo). A versão no caminho torna a URL imutável e permite cache longo; trocar de versão é trocar o caminho. Quem reutiliza este guia fora do portal mantém a mesma regra: vendor próprio ou empacotador, nunca @latest em uma tag de script.

    {
      "dependencies": {
        "echarts": "6.1.0",
        "number-flow": "0.6.2",
        "embla-carousel": "8.6.0"
      },
      "scripts": {
        "vendor": "node scripts/build-vendor-frontend.mjs",
        "vendor:check": "node scripts/build-vendor-frontend.mjs --check",
        "peso": "node scripts/medir-peso.mjs site/assets/vendor"
      }
    }
    // Instalação sempre com a versão exata:
    //   npm install --save-exact echarts@6.1.0 number-flow@0.6.2 embla-carousel@8.6.0
    // No topo de cada arquivo que usa a biblioteca:
    //   /* ECharts 6.1.0 (Apache-2.0), conferido em 3 de novembro de 2026; vendor em /assets/vendor/echarts/6.1.0/ */

    Licenças que mudam a conta

    MIT, ISC, BSD-3 e Apache-2.0 permitem uso comercial sem pagamento; a Apache-2.0 exige preservar o aviso de licença e o arquivo NOTICE, que o vendor do portal copia junto com o ECharts. O Lightweight Charts, do TradingView, acrescenta à Apache-2.0 a obrigação de exibir a atribuição à empresa na interface, e é por isso que ele aparece num painel financeiro com um link discreto no rodapé do gráfico. O MUI X Charts separa o núcleo MIT dos recursos Pro, que exigem licença comercial por desenvolvedor; a linha de corte muda por versão e precisa ser lida na documentação antes de um PR usar zoom ou exportação. O GSAP ficou gratuito, inclusive para uso comercial, a partir da 3.13, com os plugins que antes eram pagos. O Highcharts fica fora: exige licença comercial para uso em empresa, e a casa não tem motivo para pagar por algo que o ECharts entrega. O Chart.js não entra na tabela porque é protótipo: serve para validar uma ideia numa tarde e sai antes do PR. "Gratuito" nunca significa sem obrigação; o guia de ícones trata o mesmo ponto para Font Awesome, Remix e SF Symbols.

    O que fica de fora e por quê

    O que a Mirante fixou

    O painel da segunda versão tem uma biblioteca de gráfico (ECharts 6.1.0, importada por demanda do vendor do portal), um web component de número (number-flow 0.6.2) e nenhuma biblioteca de carrossel, porque o carrossel da seção anterior é nativo. O package.json tem as três versões exatas, o topo de cada arquivo diz a data em que foi conferido, e o PR que trocou os CDNs pelo vendor informou o peso: 1.121.654 bytes do ECharts sem gzip, baixados só quando a pessoa chega ao primeiro gráfico interativo, e cerca de 16 KB do number-flow (aproximado, confira no PR). O funil não some mais numa segunda-feira. A seção de stack na prática mostra como cada uma dessas peças é usada; a de preparar e conferir lista o que o PR precisa informar.

    Dê a cada gráfico uma tabela gêmea, um resumo em quatro níveis e um caminho pelo teclado

    O painel antigo da Mirante (exemplo didático, agência fictícia) era um conjunto de <canvas> sem texto. Um leitor de tela anunciava "imagem" 23 vezes e nada mais; o Ctrl+F não achava "Loja Marés"; a impressão em preto e branco saía com doze linhas cinzas iguais; e o rastreador de um modelo de linguagem, que não executa JavaScript, lia uma página vazia. Nenhum desses problemas é de "acessibilidade" no sentido estreito: são quatro públicos (quem usa leitor de tela, quem busca, quem imprime e quem indexa) que o painel novo atende com o mesmo conjunto de decisões. Esta seção lista essas decisões e traz o código de duas delas.

    A tabela gêmea: todo gráfico tem uma, sempre no DOM

    Cada gráfico do painel nasce com uma tabela equivalente: <table> com <caption> que diz o indicador, a unidade e o período, th scope="col" e th scope="row", e os mesmos números da figura, sem arredondar diferente. O botão "Ver como tabela" não cria nem esconde nada do DOM: ele abre um <details> que já contém a tabela, para que o Ctrl+F, a impressão e o rastreador a encontrem fechada ou aberta. O Power BI resolve o mesmo problema com o atalho Alt+Shift+F11, que abre a "Show data table" de um visual, conforme a documentação de acessibilidade do Microsoft Learn; na web, a tabela não precisa de atalho porque já está na página. Quando o dado é real, a tabela ganha um link de CSV com ; como separador, que abre no Excel em português.

    <figure class="db-panel" aria-describedby="leads-resumo">
      <svg viewBox="0 0 720 360" role="img" aria-labelledby="leads-t leads-d">
        <title id="leads-t">Leads por semana, setembro e outubro de 2026</title>
        <desc id="leads-d">Outubro vai de 860 a 1.004 leads por semana, acima de setembro em todas as semanas; o pico é a semana 3.</desc>
        <!-- marcas -->
      </svg>
      <figcaption>Leads por semana. Dados de exercício.</figcaption>
      <!-- O details já contém a tabela: fechado ou aberto, ela está no DOM -->
      <details class="db-tabela-gemea">
        <summary>Ver como tabela</summary>
        <div class="db-table-wrap" tabindex="0" role="region" aria-label="Leads por semana em tabela">
          <table>
            <caption>Leads por semana (unidades), setembro e outubro de 2026</caption>
            <thead><tr><th scope="col">Semana</th><th scope="col">Setembro</th><th scope="col">Outubro</th></tr></thead>
            <tbody>
              <tr><th scope="row">S1</th><td>790</td><td>860</td></tr>
              <tr><th scope="row">S2</th><td>835</td><td>912</td></tr>
              <tr><th scope="row">S3</th><td>870</td><td>1.004</td></tr>
              <tr><th scope="row">S4</th><td>905</td><td>958</td></tr>
              <tr><th scope="row">S5</th><td>780</td><td>878</td></tr>
            </tbody>
          </table>
        </div>
        <p><a href="/dados/leads-semana-2026-10.csv" download>Baixar CSV (separador ;)</a></p>
      </details>
    </figure>
    <p id="leads-resumo">Gráfico de linhas com duas séries por semana. Outubro somou 4.612 leads, 10,3% acima de setembro (4.180); a semana 3 de outubro teve o máximo, 1.004, e a semana 5 de setembro o mínimo, 780. Outubro ficou acima de setembro em todas as cinco semanas. Interpretação: a alta acompanha as 91 campanhas ativas, 5 a mais que em setembro.</p>

    Resumo em quatro níveis: construção, estatística, tendência e contexto

    O parágrafo que acompanha a figura no código acima segue os quatro níveis de conteúdo semântico que Alan Lundgard e Arvind Satyanarayan descreveram em 2021 ao estudar descrições de gráficos com pessoas cegas e com baixa visão. O primeiro nível diz como o gráfico é construído (tipo, eixos, séries). O segundo dá a estatística (total, máximo, mínimo, variação). O terceiro descreve a tendência (acima em todas as semanas, pico na semana 3). O quarto traz o contexto e a interpretação, e fica marcado com a palavra "Interpretação:", porque é o único nível que mistura leitura com opinião. A Mirante escreve os três primeiros níveis a partir da tabela gêmea, com o prompt de resumo verificável do guia de Gráficos, e o quarto é escrito por Helena, que assina.

    O resumo não cita cor nem forma ("a linha azul", "a barra mais alta"), porque o critério 1.3.3 da WCAG 2.2 proíbe instruções que dependam só de características sensoriais, e porque um resumo que diz "azul" é inútil para quem não vê azul. Ele cita o nome da série e o valor.

    Teclado dentro da figura: Tab entra, setas percorrem, o anúncio diz valor e posição

    Uma figura SVG com role="img" é uma imagem só: o leitor de tela lê o título e a descrição e segue adiante, o que serve ao painel de leitura. Quando a pessoa precisa consultar valor a valor sem abrir a tabela, a figura ganha navegação própria: o contêiner recebe tabindex="0" e role="group" com rótulo; a seta para a direita e para a esquerda percorrem os pontos; cima e baixo trocam de série; Home e End vão às pontas; e uma região com aria-live="polite" anuncia "Semana 3, outubro: 1.004 leads, ponto 3 de 5". O foco visível é um anel de 3 px no acento sobre o marcador, e o anel de 2 px na cor do fundo garante que o marcador continue legível onde cruza a linha. Com <canvas>, nada disso existe por padrão: o canvas exige uma camada DOM paralela com os mesmos valores, o que na prática é a tabela gêmea com foco por célula.

    // Navegação por teclado numa figura SVG: setas percorrem os pontos, aria-live anuncia.
    (() => {
      "use strict";
      const figura = document.getElementById("leads-figura");       // div tabindex="0" role="group" aria-label="..."
      const status = document.getElementById("leads-status");       // <p role="status" aria-live="polite">
      if (!figura || !status) return;
      const series = [
        { nome: "Setembro", pontos: [790, 835, 870, 905, 780] },
        { nome: "Outubro",  pontos: [860, 912, 1004, 958, 878] },
      ];
      const marcas = [...figura.querySelectorAll("[data-serie][data-ponto]")]; // círculos com data-serie e data-ponto
      let serie = 1, ponto = 0;
      const numero = new Intl.NumberFormat("pt-BR");
      const anunciar = () => {
        const s = series[serie];
        marcas.forEach((m) => m.classList.toggle("db-foco", Number(m.dataset.serie) === serie && Number(m.dataset.ponto) === ponto));
        status.textContent = `Semana ${ponto + 1}, ${s.nome}: ${numero.format(s.pontos[ponto])} leads, ponto ${ponto + 1} de ${s.pontos.length}`;
      };
      figura.addEventListener("keydown", (e) => {
        const n = series[serie].pontos.length;
        const teclas = {
          ArrowRight: () => { ponto = Math.min(n - 1, ponto + 1); },
          ArrowLeft:  () => { ponto = Math.max(0, ponto - 1); },
          ArrowUp:    () => { serie = (serie + 1) % series.length; },
          ArrowDown:  () => { serie = (serie - 1 + series.length) % series.length; },
          Home:       () => { ponto = 0; },
          End:        () => { ponto = n - 1; },
        };
        if (!teclas[e.key]) return;
        e.preventDefault();
        teclas[e.key]();
        anunciar();
      });
      figura.addEventListener("focus", anunciar);
    })();

    O anúncio usa textContent, nunca innerHTML, porque o nome da série pode vir de um CSV ou de uma API e não é confiável. O mesmo vale para a dica (tooltip) e para a legenda.

    Dica que não é o único caminho e que obedece ao critério 1.4.13

    A dica (tooltip) de um gráfico realça; ela nunca é a única forma de ler um valor, porque todo valor está no rótulo direto ou na tabela gêmea, e porque o Microsoft Learn é direto: "Don't use tooltips to convey important information". Quando existe, a dica cumpre o critério 1.4.13 da WCAG 2.2: pode ser dispensada com Esc sem mover o ponteiro, persiste enquanto o ponteiro está sobre ela, e fica até a pessoa fechar; no celular, abre por toque e fecha com o botão ou com Esc, sem sumir sozinha. O foco por teclado mostra a mesma dica do hover.

    Contraste medido no fundo real, nos dois temas

    Texto comum precisa de 4,5:1 (critério 1.4.3) e formas e linhas de 3:1 contra as cores adjacentes (critério 1.4.11), medidos sobre o fundo real do cartão e não sobre o branco da página, em cada tema. O guia de Ícones registrou um erro que se repete em painel: medir o tema escuro durante a transição de cor do portal, que acusa 1,99:1 em links que terminam a 4,72:1; a medição espera a transição acabar. A exceção do 1.4.11 que vale para gráficos: a marca dispensa 3:1 quando os valores estão escritos no gráfico ou na tabela, o que é mais um motivo para a tabela gêmea. Em forced-colors: active (o modo de alto contraste do Windows), as cores de série somem e sobram CanvasText, GrayText e Highlight; SVG com currentColor se adapta, canvas não, e a textura de reserva da seção Estilo e notação passa a carregar a identidade das séries.

    Alvo, foco, zoom e movimento

    O alvo de toque de uma barra, de um marcador ou de uma célula de matriz tem 24 por 24 px CSS no mínimo (critério 2.5.8) e 44 px em painel usado no celular; o anel de 2 px faz parte do alvo, e um ponto de 8 px ganha uma área transparente de 24 px ao redor. O foco visível é um anel de 3 px com deslocamento de 2 px, nunca escondido pela barra fixa do topo (critério 2.4.11, com scroll-margin-top). Com zoom de 200%, exigido pelo critério 1.4.4 da WCAG 2.2 (2023), o painel reempilha em uma coluna sem rolagem horizontal, o que a grade por contêiner da seção anterior garante. Com movimento reduzido, pela preferência do sistema ou pelo interruptor do portal, cada animação entrega o estado final de uma vez: o contador mostra o número, a barra já tem o tamanho, a sparkline já está desenhada.

    O <number-flow> do vendor do portal (0.6.2) anima os dígitos ao trocar o valor; para o leitor de tela, o elemento recebe aria-label com o número final formatado, para que a pessoa ouça "4.612" uma vez em vez de cada dígito que passa. O mesmo cuidado vale para qualquer contador: anunciar o resultado, nunca os quadros.

    Checklist Chartability, resumido para o painel

    O Chartability, de Frank Elavsky, é um conjunto de critérios de auditoria de acessibilidade em visualização de dados, organizado em princípios; a tabela abaixo resume os que a Mirante aplica a cada gráfico antes de publicar, com a conferência que cabe num pedido a agente de código.

    Critérios de acessibilidade aplicados a cada gráfico do painel, com a conferência e o critério normativo quando existe
    PrincípioO que conferirComo conferirNorma
    PerceptívelTítulo, descrição e tabela gêmea existem; contraste 4,5:1 e 3:1 nos dois temasLeitor de tela lê título e desc; medir no estilo calculado após a transiçãoWCAG 1.1.1, 1.4.3, 1.4.11
    OperávelTab entra na figura, setas percorrem, Esc fecha a dica, alvo de 24 pxSó teclado, do início ao fim; medir o alvo com o inspetorWCAG 2.1.1, 2.5.8, 1.4.13
    CompreensívelResumo em quatro níveis sem cor nem forma; legenda a partir de 2 séries; unidade e período na captionLer o resumo sem olhar o gráfico e responder "o que mudou"WCAG 1.3.3
    RobustoSVG com role, title e desc; canvas com camada DOM; nada depende de JS para existirDesligar o JavaScript e conferir figura, tabela e resumoWCAG 4.1.2
    CompromissadoFonte, definição, data e dono do número em até dois cliquesContar os cliques do número até a fonteRegra dos dois cliques do guia de Gráficos
    AssistivoMovimento reduzido entrega o estado final; forced-colors mantém a identidade por textura; zoom de 200% sem rolagem horizontalLigar a preferência do sistema, o modo de alto contraste e o zoom, nessa ordemWCAG 2.3.3, 1.4.10
    FlexívelPeríodo, cliente e métrica na URL; tabela com CSV quando o dado é realAbrir o link em outra máquina e ver a mesma seleçãoAceite do guia de Gráficos

    Prova em Playwright: axe, teclado e movimento reduzido

    A prova de aceite roda no Playwright com @axe-core/playwright, numa página criada por browser.newContext(), em 1280 e 390 px, e repete a passagem com page.emulateMedia({ reducedMotion: "reduce" }) para garantir que o estado final apareça sem animação. Os defeitos que o detector corrige sozinho são três: contraste, tamanho de texto e foco; o resto é decisão do time. Na Mirante, a primeira rodada acusou 11 marcadores de 6 px e 4 rótulos de 12 px na matriz; os marcadores foram a 8 px com anel, e os rótulos foram para a tabela. O resumo em quatro níveis e a tabela gêmea não são detectados por máquina: entram no aceite da oficina, marcados à mão.

    Reorganize o painel para 390 px sem perder a decisão da primeira tela

    Helena, diretora de operações da agência Mirante (exemplo didático, agência fictícia), abre o Painel de operação na segunda-feira de manhã pelo celular, no carro, antes de chegar à agência. A versão antiga encolhia a tela de computador até caber: nove cartões numa linha de 40 px de altura, matriz de doze clientes com letras de 6 px e uma página que rolava para o lado. Ela desistia e esperava o computador. A versão nova trata a largura de 390 px (a medida de teste do celular em toda a coleção, ver o guia de navegação) como um layout próprio, com as mesmas respostas da tela grande: o que mudou, onde e o que fazer. O que muda é a ordem e a forma, e nunca o conteúdo.

    A regra que organiza esta seção é simples de repetir em voz alta: no celular, a primeira tela entrega a manchete e os três indicadores que a sustentam; tudo o mais fica a um gesto de distância, e nenhum gesto exige precisão de dedo. A seção Anatomia da primeira tela define os nove a onze indicadores do computador; aqui eles continuam existindo, mas três deles sobem e os outros descem.

    A linha de nove indicadores vira duas colunas, com os três principais primeiro

    No computador, a linha de indicadores cabe inteira acima da dobra. A 390 px, nove cartões em coluna única ocupam duas telas e empurram a manchete para fora da vista. A Mirante resolveu com duas colunas e uma ordem explícita: churn (cancelamentos, a parcela da base que saiu no mês) de 3,1% em outubro de 2026, leads captados de 4.612 e receita recorrente (MRR, a soma das mensalidades ativas) de R$ 428.900 ocupam a primeira linha e meia, porque são os três números que a manchete do mês cita. Os outros seis vêm depois, em cartões menores, e campanhas ativas, que não tem meta, fecha a lista.

    O cartão no celular mantém os mesmos quatro elementos do computador: rótulo, valor, variação contra setembro e contra a meta, e a série curta quando existe. O que sai é a sparkline (gráfico de linha minúsculo, sem eixos, que mostra a forma da série) dos cartões secundários, porque abaixo de 160 px de largura ela deixa de ser legível e vira ornamento. Em vez de apagar o cartão inteiro, a Mirante apaga só a linha e mantém o número.

    O cartão se adapta ao seu contêiner, e não à janela

    O mesmo cartão de indicador aparece em três lugares da Mirante: na linha do topo, dentro do cartão de cada cliente no carrossel e na gaveta de detalhe. Cada lugar tem uma largura diferente, e uma consulta de mídia, a media query (regra de CSS que reage à largura da janela), só conhece a janela. A consulta ao contêiner, @container, pergunta a largura do pai imediato, e o cartão decide sozinho se mostra uma ou duas colunas internas, com ou sem sparkline. O guia de Tailwind e tokens explica a regra em detalhe; o bloco abaixo é a versão da Mirante, pronta para copiar.

    /* O pai de cada cartão declara que pode ser consultado. */
    .db-kpis, .db-cliente-cartao, .db-gaveta-corpo { container-type: inline-size; }
    
    /* Estado base, válido em qualquer largura e sem @container: uma coluna,
       número e variação empilhados, sparkline escondida. */
    .db-kpi { display: grid; gap: 4px; padding: 12px 14px; }
    .db-kpi .db-spark { display: none; }
    
    /* A partir de 160 px de contêiner, a variação fica ao lado do valor. */
    @container (min-width: 160px) {
      .db-kpi { grid-template-columns: 1fr auto; align-items: baseline; }
      .db-kpi .db-kpi-label { grid-column: 1 / -1; }
    }
    
    /* A partir de 240 px de contêiner, a sparkline volta, abaixo do valor. */
    @container (min-width: 240px) {
      .db-kpi .db-spark { display: block; grid-column: 1 / -1; height: 28px; }
    }
    
    /* A linha de indicadores: duas colunas no celular, três principais primeiro
       (order), e auto-fit no computador. Sem @container nada quebra. */
    .db-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
    .db-kpi[data-principal] { order: -1; }
    @container (min-width: 720px) {
      .db-kpis { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
    }

    Dois detalhes evitam uma falha comum. A regra base, escrita fora de qualquer @container, já entrega uma coluna legível: um navegador antigo, ou um bloco inserido em um pai sem container-type, recebe o cartão de uma coluna e nunca um cartão quebrado. E a propriedade order reordena só o que a pessoa vê; a ordem do documento, que o leitor de tela e o Tab seguem, continua a mesma. A Mirante usa order apenas nos três principais, e mantém a lista na mesma ordem do computador, para que a leitura por teclado não pule de um lado para o outro.

    A matriz de clientes vira carrossel, e as colunas viram barras

    A matriz de múltiplos pequenos (vários gráficos iguais, um por cliente, na mesma escala) com doze clientes em três colunas cabe num monitor; a 390 px ela vira doze gráficos de 120 px de largura, sem lugar para rótulo. A Mirante troca a matriz por um carrossel de cartões, um cliente por tela, com a mesma escala em todos e a posição ("3 de 12") escrita acima do cartão. A seção Carrossel descreve o mecanismo, e a oficina de carrossel monta um de ponta a ponta. A regra que importa aqui é de conteúdo: o carrossel precisa de uma ordem explícita (por variação, do pior para o melhor, foi a escolha de Helena) e de um atalho para o cliente que a manchete citou.

    Dentro de cada cartão, o gráfico de colunas por semana vira barras horizontais, porque o nome da categoria cabe inteiro à esquerda da barra e o valor cabe na ponta. O ranking de doze clientes por leads, que no computador é uma barra ordenada completa, no celular mostra os cinco primeiros e agrupa os outros sete em "outros, 1.931 leads", com um botão que expande a lista. A legenda some: cada linha ou barra recebe rótulo direto na ponta, com o nome do cliente e o valor. A grade de fundo cai de seis para três linhas, e o período encurta: a série de churn de sete meses vira os últimos quatro, com o botão "ver 7 meses" ao lado.

    Nenhum texto abaixo de 14 px e nenhuma rolagem lateral da página

    O número que decide se um gráfico sobrevive ao celular é o tamanho real da letra, diferente do tamanho escrito no SVG. Um rótulo de 14 px num viewBox de 720 unidades, exibido em 358 px de largura (390 menos as margens), mede 7 px na tela. Por isso a Mirante desenha os gráficos do celular num viewBox de 390 unidades, em que 14 continua 14. A regra vale para eixo, rótulo direto, legenda e manchete: nada abaixo de 14 px no tamanho exibido, como a seção de tipografia do guia de gráficos fixa para toda a coleção. Quando o rótulo não cabe, o gráfico muda de forma (coluna vira barra), e nunca a letra encolhe.

    A página inteira nunca rola para o lado. A única rolagem horizontal permitida é a do carrossel, que tem botões, marcadores e teclado, e a da tabela gêmea dentro do seu próprio contêiner com tabindex="0" e role="region". O teste é uma linha de JavaScript no console: document.documentElement.scrollWidth <= window.innerWidth precisa dar verdadeiro a 390 px, com o tema escuro ligado e com o zoom de texto do sistema em 200% (critério 1.4.4 da WCAG 2.2, 2023).

    Toque, dica e zoom sem precisão de dedo

    Um painel no celular é operado com o polegar, em pé, muitas vezes com o telefone na outra mão. Todo alvo de toque mede ao menos 24 por 24 px CSS, o mínimo do critério 2.5.8 da WCAG 2.2, e 44 px nos controles do painel (filtro, botão do carrossel, "ver mais"), a medida que a coleção adota para painel desde o guia de ícones. A dica sobre um ponto do gráfico abre por toque, e não por passar o dedo, porque não existe passar o dedo no celular; ela fica aberta até a pessoa fechar com o botão "Fechar", com Esc no teclado externo ou tocando fora, como pede o critério 1.4.13. E a dica nunca é o único caminho: o valor que ela mostra está na tabela gêmea logo abaixo do gráfico.

    O gesto de pinça (dois dedos que se afastam) pertence ao navegador, que amplia a página inteira. Um gráfico que captura a pinça para dar zoom na série quebra a ampliação do sistema, que pessoas com baixa visão usam o tempo todo. A Mirante dá zoom por botão: "7 dias", "30 dias", "90 dias" numa linha acima do gráfico, com aria-pressed no período ativo, e a seleção vai para a URL, como a seção Interação descreve. O mesmo botão serve ao teclado e ao leitor de tela.

    Filtros numa gaveta inferior com o resumo sempre visível

    No computador, a linha de filtros (período, cliente, canal) fica acima dos gráficos. No celular, a mesma linha ocuparia metade da primeira tela. A Mirante guarda os filtros numa gaveta que sobe de baixo, aberta por um botão de 44 px com o texto "Filtros" e, ao lado, o resumo da seleção atual: "Outubro de 2026 · 12 clientes · todos os canais". O resumo é a parte que nunca some, porque um painel filtrado sem aviso é a causa mais comum de conclusão errada: Helena leu, numa versão anterior, um churn de 1,2% em agosto de 2026 e comemorou, sem ver que o filtro de cliente estava preso na Clínica Vale Verde.

    A gaveta é um <dialog> aberto com showModal(), que prende o foco, fecha com Esc e devolve o foco ao botão, no padrão da oficina de gaveta do guia de navegação. O botão Voltar do celular fecha a gaveta em vez de sair do painel, porque a abertura grava uma entrada no histórico. Aplicar um filtro reescreve a URL, e copiar o endereço reproduz a seleção em outro aparelho.

    A tabela gêmea vira cartões abaixo de 600 px

    Toda figura do painel tem uma tabela equivalente, e no celular a tabela de doze clientes por seis colunas não cabe. Abaixo de 600 px de contêiner, cada linha vira um cartão com o nome do cliente em negrito e os pares rótulo e valor um abaixo do outro, com o rótulo vindo do atributo data-rotulo de cada célula, a técnica da oficina de tabela longa do guia de navegação. A tabela continua uma tabela no documento: o leitor de tela lê cabeçalho e célula, a busca do navegador acha o cliente, e o CSV exporta as mesmas linhas.

    Peso: SVG estático primeiro, biblioteca só quando o cartão entra na tela

    A rede do celular de Helena no caminho da agência é 4G irregular. O ECharts 6.1.0 do vendor próprio do portal pesa 1.121.654 bytes antes de comprimir, e nenhuma primeira tela espera por isso. A Mirante serve cada gráfico da primeira tela como SVG estático dentro do HTML, com título e descrição, legível sem JavaScript e lido pelos rastreadores de IA, que não executam script. A biblioteca entra depois, por import() dinâmico, só quando um cartão que precisa de interação (zoom por período, dica com todas as séries) cruza a borda da tela num IntersectionObserver com margem de 200 px. O SVG estático fica no lugar até o gráfico interativo estar pronto, e é a versão que a impressão usa.

    Para a lista de doze cartões abaixo da primeira tela, content-visibility: auto com contain-intrinsic-size de 320 px adia o cálculo de layout do que está fora da vista; o conteúdo continua no documento, acessível à busca e ao leitor de tela, e o navegador só o desenha ao chegar lá. Os números completos ficam na seção Bibliotecas; a regra de bolso é que o celular recebe primeiro o que decide e depois o que explora.

    A primeira tela da Mirante a 390 px. Manchete, três indicadores principais em destaque, dois secundários, o atalho do carrossel com a posição escrita e a barra de filtros com o resumo da seleção. Letras de 14 px ou mais no viewBox de 390 unidades. Dados fictícios.
    Primeira tela do Painel de operação da Mirante em celular de 390 px Tela com a manchete "Churn de 3,1% ficou 0,6 p.p. acima da meta; leads superaram a meta em 4,8%", três cartões principais (churn 3,1%, leads 4.612, MRR R$ 428.900) com variação contra setembro e contra a meta, dois cartões secundários (NPS 47 e reuniões 503), um cartão de carrossel "Loja Marés, 1 de 12, 498 leads, menos 7% sobre setembro" com marcadores de página, e uma barra inferior com o botão Filtros e o resumo "Out 2026 · 12 clientes · todos os canais". Os valores estão na tabela abaixo. Painel de operação · outubro de 2026 Atualizado em 3 de novembro de 2026 · fonte: CRM e mídia Churn de 3,1% ficou 0,6 p.p. acima da meta; leads superaram a meta em 4,8%. Ver os 3 clientes com mais cancelamentos Churn (% da base) 3,1% +0,5 p.p. sobre set +0,6 p.p. sobre a meta Leads captados 4.612 +10,3% sobre set +4,8% sobre a meta Receita recorrente (MRR) R$ 428.900 +4,0% sobre set -1,4% sobre a meta NPS 47 -8 pts da meta Reuniões realizadas 503 -4,7% da meta Clientes, do pior para o melhor 1 de 12 Loja Marés 498 leads · -7% sobre set set 535 out 498 e mais 8 Filtros Out 2026 · 12 clientes · todos os canais
    O que a primeira tela de 390 px mostra, de cima para baixo, e o que muda em relação ao computador (dados fictícios de outubro de 2026)
    ElementoValor exibidoDiferença para o computador
    MancheteChurn de 3,1% ficou 0,6 p.p. acima da meta; leads superaram a meta em 4,8%Igual; ganha o atalho "ver os 3 clientes com mais cancelamentos"
    Churn3,1%; +0,5 p.p. sobre set; +0,6 p.p. sobre a metaSobe para a primeira linha
    Leads captados4.612; +10,3% sobre set; +4,8% sobre a metaSobe para a primeira linha
    MRRR$ 428.900; +4,0% sobre set; -1,4% sobre a metaÚnico cartão com sparkline (largura de 358 px)
    NPS e reuniões47 (-8 pontos da meta); 503 (-4,7% da meta)Cartões secundários, sem sparkline
    Carrossel de clientesLoja Marés, 1 de 12, 498 leads, -7% sobre set (set 535)Substitui a matriz de 12 múltiplos; ordem do pior para o melhor
    FiltrosBotão de 44 px e resumo "Out 2026 · 12 clientes · todos os canais"Linha de filtros vira gaveta inferior

    Checklist de 390 px antes de publicar

    Na segunda-feira seguinte à troca, Helena leu a manchete no carro, abriu o carrossel na Loja Marés e mandou a mensagem para o gestor da conta antes de chegar à agência. A oficina de carrossel monta o componente que ela usou; a oficina de primeira tela escolhe os indicadores que sobem para a primeira linha.

    Laboratório: o painel da primeira tela, desmontado peça por peça

    O painel que abre este guia roda nesta página com os dados de exercício da agência Mirante (fictícia). Ele existe para que você leia cada decisão no próprio objeto, em vez de numa captura de tela: a manchete escrita a partir do maior desvio contra a meta, a linha de nove indicadores com denominador e meta, três gráficos com tabela gêmea, o filtro que vive na URL, o carrossel de clientes que destaca um cliente nos outros gráficos sem repintar os demais, e o carregamento do Apache ECharts 6.1.0 só quando alguém pede. Esta seção percorre essas peças e mostra o código de cada uma.

    O que acontece quando a página abre

    Sem JavaScript, o leitor recebe o estado final: os nove cartões já têm valor, variação e meta no HTML; as três figuras são SVG estático com <title> e <desc>; as tabelas ficam em <details> logo abaixo de cada figura. Os rastreadores de IA leem esse HTML, e é por isso que o resumo e a tabela precisam estar servidos, como o guia de Gráficos explica. Com JavaScript, o painel espera entrar na tela (um IntersectionObserver com margem de um quinto da altura da janela) e então executa um único momento orquestrado: os cartões sobem com atraso de 70 ms entre si, a sparkline de leads é traçada por stroke-dashoffset e os números contam até o valor com o componente <number-flow>, carregado sob demanda do vendor do portal. Com movimento reduzido no sistema ou no interruptor do portal, nada disso roda e o estado final aparece de uma vez.

    Carrossel de clientes com destaque cruzado

    Doze clientes não cabem como doze gráficos numa tela; cabem como doze cartões num carrossel que a pessoa roda para a esquerda e para a direita, como nas postagens do Instagram. Cada cartão traz nome, leads, variação e uma sparkline das cinco semanas. O botão "Destacar no painel" seleciona o cliente: a barra dele continua colorida no gráfico de clientes enquanto as outras ficam cinza, e os indicadores passam a mostrar a parte proporcional daquele cliente. Selecionar um cliente no filtro do topo faz o carrossel rolar até o cartão dele. A seção Carrossel de cartões detalha a stack; a oficina de carrossel deixa você variar os parâmetros.

    Padaria Horizonte

    612+9% sobre set

    Leads de outubro, S1 a S5: 108, 118, 134, 130, 122.

    Clínica Vale Verde

    540+4% sobre set

    Leads de outubro, S1 a S5: 102, 106, 116, 110, 106.

    Loja Marés

    498-7% sobre set

    Leads de outubro, S1 a S5: 112, 104, 98, 94, 90.

    Escola Compasso

    431+12% sobre set

    Leads de outubro, S1 a S5: 72, 80, 94, 96, 89.

    Academia Pulso

    402+2% sobre set

    Leads de outubro, S1 a S5: 78, 82, 86, 80, 76.

    Imobiliária Lume

    377-3% sobre set

    Leads de outubro, S1 a S5: 80, 76, 74, 75, 72.

    Oficina Trilho

    341+6% sobre set

    Leads de outubro, S1 a S5: 62, 66, 72, 72, 69.

    Pet Serena

    318+1% sobre set

    Leads de outubro, S1 a S5: 63, 64, 66, 63, 62.

    Cursos Bússola

    296+15% sobre set

    Leads de outubro, S1 a S5: 46, 54, 64, 68, 64.

    Studio Fotográfico Prisma

    274-11% sobre set

    Leads de outubro, S1 a S5: 66, 58, 54, 50, 46.

    Dental Aurora

    268+5% sobre set

    Leads de outubro, S1 a S5: 50, 52, 56, 56, 54.

    Café Ladeira

    2550% sobre set

    Leads de outubro, S1 a S5: 51, 50, 52, 51, 51.

      Cliente 1 de 12: Padaria Horizonte

      Sem JavaScript, os 12 cartões rolam com o dedo ou com a barra de rolagem e param um a um por scroll-snap; os botões, os marcadores e o destaque cruzado precisam de script.

      Como o filtro, a URL e os gráficos se ligam

      O painel guarda dois valores de estado, período e cliente, e três funções os consomem: a que pinta os cartões, a que escreve a manchete e a que atualiza os gráficos. Toda mudança passa pela mesma função pintarTudo, que também grava a URL com replaceState. O carrossel conversa com o painel por eventos do DOM em vez de variável global: ao destacar um cliente, o cartão dispara db:selecionar-cliente; ao mudar o filtro, o painel dispara db:painel, e o carrossel rola até o cartão. Os dois componentes podem viver em arquivos diferentes, e o padrão vale igual em React com um estado compartilhado ou com nuqs na URL, como o guia de React mostra.

      // O recorte do painel vive na URL: copiar o link reproduz a seleção em outra máquina.
      const estado = { periodo: "out", cliente: "" };
      
      function lerURL() {
        const p = new URLSearchParams(location.search);
        if (["set", "out"].includes(p.get("painel-periodo"))) estado.periodo = p.get("painel-periodo");
        const c = p.get("painel-cliente");
        estado.cliente = c && CLIENTES[Number(c)] ? c : "";
      }
      function gravarURL() {
        const p = new URLSearchParams(location.search);
        estado.periodo === "out" ? p.delete("painel-periodo") : p.set("painel-periodo", estado.periodo);
        estado.cliente === "" ? p.delete("painel-cliente") : p.set("painel-cliente", estado.cliente);
        const q = p.toString();
        history.replaceState(history.state, "", `${location.pathname}${q ? "?" + q : ""}${location.hash}`);
      }
      function pintarTudo() { pintarKpis(); pintarManchete(); pintarTabelas(); pintarGraficos(); gravarURL(); }
      
      periodoSel.addEventListener("change", () => { estado.periodo = periodoSel.value; pintarTudo(); });
      window.addEventListener("popstate", () => { lerURL(); periodoSel.value = estado.periodo; pintarTudo(); });
      
      // O carrossel não conhece o painel: ele só anuncia o que a pessoa escolheu.
      cartao.querySelector("button").addEventListener("click", () =>
        painel.dispatchEvent(new CustomEvent("db:selecionar-cliente", { detail: { indice: i } })));

      Por que o ECharts só entra quando alguém pede

      O build ESM do Apache ECharts 6.1.0 que o portal serve do próprio domínio pesa 1.121.654 bytes. Carregá-lo para todo leitor, inclusive quem só veio ler o texto, contraria a regra de armazenamento, velocidade e memória da casa. O botão "Carregar gráficos interativos" faz um import() dinâmico, cria uma instância por área de gráfico com o renderizador SVG (melhor para leitura e impressão; canvas fica para séries densas), liga um ResizeObserver por instância e observa o atributo data-theme do documento para chamar setTheme quando o portal troca de tema. As cores da option saem dos tokens CSS lidos por getComputedStyle no momento de pintar; nenhuma cor fica escrita no JavaScript. Se o import falhar, a mensagem diz que as figuras estáticas continuam válidas.

      async function carregarVivo() {
        botao.disabled = true; botao.textContent = "Carregando ECharts 6.1.0 (1,1 MB)…";
        let echarts;
        try { echarts = await import("/assets/vendor/echarts/6.1.0/echarts.esm.min.js"); }
        catch { botao.disabled = false; status.textContent = "O ECharts não carregou. As figuras estáticas continuam válidas."; return; }
        for (const area of document.querySelectorAll("[data-db-grafico]")) {
          area.dataset.vivo = "";                               // esconde o SVG estático; a tabela fica
          const caixa = Object.assign(document.createElement("div"), { className: "db-echart" });
          caixa.setAttribute("role", "img"); caixa.setAttribute("aria-label", area.dataset.dbRotulo);
          area.append(caixa);
          const inst = echarts.init(caixa, temaEscuro() ? "dark" : null, { renderer: "svg" });
          graficos.set(area.dataset.dbGrafico, inst);
          new ResizeObserver(() => inst.resize()).observe(caixa);
        }
        pintarGraficos();
        new MutationObserver(() => { graficos.forEach((g) => g.setTheme(temaEscuro() ? "dark" : "default")); pintarGraficos(); })
          .observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
      }
      const token = (nome) => getComputedStyle(document.body).getPropertyValue(nome).trim();
      // Na option: lineStyle: { color: token("--db-accent") }, axisLabel: { fontSize: 14, color: token("--db-muted") }

      A escala logarítmica do funil é uma decisão de leitura, mais do que de estética: num eixo linear, 61 fechamentos ao lado de 48.200 visitas viram uma barra de meio pixel. A escala em base 10 mantém as cinco etapas legíveis e a taxa escrita ao lado de cada barra devolve a proporção exata que o olho perde. A seção Escalas e eixos explica quando a escala logarítmica ajuda e quando engana.

      Oficina: monte a primeira tela que responde em cinco segundos

      A primeira tela de um painel tem um trabalho só: dizer o que mudou, onde e o que fazer antes que a pessoa role. O painel antigo da agência Mirante (exemplo didático, agência fictícia) abria com 23 gráficos e nenhuma frase; Helena, a diretora de operações, levava 40 minutos para montar na cabeça o que a tela deveria ter dito. Nesta oficina você escolhe, entre dezesseis indicadores da Mirante, os nove a onze que entram na primeira tela, define qual deles é o número principal, confere a densidade (quantos têm denominador escrito e meta) e gera a manchete a partir do maior desvio contra a meta. A tela montada já está no HTML com os nove indicadores padrão; o JavaScript deixa você trocar.

      Cenário de exercício

      Quem
      Camila, designer da Mirante, e Otávio, desenvolvedor que trabalha com agente de código; a leitora é Helena, diretora de operações, que decide na segunda-feira o que cobrar de cada gestor de conta.
      Tarefa
      Escolher os indicadores da primeira tela do Painel de operação de outubro de 2026, com o número principal em destaque, a variação contra setembro e contra a meta em cada cartão e uma manchete escrita a partir dos dados.
      Falha vista
      A primeira versão mostrava 16 indicadores em cartões iguais, sem meta e sem denominador. "Conversão 10,9%" não dizia, no painel de 2026, conversão de quê em quê; o churn de 3,1% ficava no canto inferior, do mesmo tamanho que "campanhas ativas 91", que não tem meta e não pede decisão.
      Resultado esperado
      Nove a onze cartões, três deles em destaque, todo percentual com denominador escrito, toda meta visível, um número principal e uma manchete que cita o maior desvio ruim e o maior desvio bom contra a meta.

      Nove a onze indicadores, um número principal e uma frase

      O guia de gráficos da coleção fixa a medida: um painel de uma página tem de 9 a 11 indicadores, com o denominador escrito em cada porcentagem. Abaixo de nove, a tela esconde o que a gestora precisa cruzar; acima de onze, nenhum cartão ganha o olhar. Entre eles, um é o número principal, mostrado maior (a figura de destaque de 48 px ou mais que a seção Anatomia descreve), e a manchete é a frase que o sistema calcula, diferente da que alguém escreve de memória na segunda-feira. A manchete da Mirante segue uma regra fixa: o maior desvio contra a meta no sentido ruim, seguido do maior desvio no sentido bom, cada um com o número e a unidade certa (p.p. para diferença entre percentuais, pontos para o NPS, porcentagem para o resto).

      A regra dos dois cliques também vale aqui: fonte, definição, data de atualização e dono de cada número precisam estar a no máximo dois cliques do cartão. Por isso cada cartão da demonstração traz o denominador no próprio rótulo ("Conversão: reuniões ÷ leads") e a tela traz a data e a fonte no topo. Indicador sem denominador e sem meta não é proibido, mas o placar da oficina conta quantos existem, porque cada um deles custa um lugar na primeira tela sem responder a uma pergunta.

      Mapa da oficina. A primeira tela nasce da decisão que ela alimenta em vez da lista de métricas disponível. O percurso vai da decisão à conferência medida.
      1. DecisãoEscreva numa frase o que a leitora decide com a tela.
      2. IndicadoresEscolha de 9 a 11, com denominador e meta, e um principal.
      3. MancheteO sistema escreve a frase a partir do maior desvio.
      4. ConferênciaCinco segundos, 390 px, tema escuro, teclado.

      1. A frase de decisão da Mirante: "Toda segunda, Helena escolhe quais gestores de conta chamar e sobre o quê". Sem essa frase, a lista de indicadores vira a lista de tudo o que o sistema mede.
      2. As caixas de seleção escolhem os indicadores; o placar conta quantos entraram, quantos têm denominador e quantos têm meta, e avisa fora da faixa de 9 a 11. O select escolhe o número principal.
      3. O botão "Gerar manchete" ordena os indicadores pelo desvio relativo contra a meta, no sentido ruim e no bom, e escreve a frase com a unidade certa.
      4. A conferência mede: a tela responde em cinco segundos para alguém que não a montou, cabe em 390 px, mantém contraste no tema escuro e percorre tudo por teclado.

      Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

      Demonstração: escolha os indicadores, o principal e gere a manchete

      Dados de exercício: dezesseis indicadores da Mirante em outubro de 2026, com o valor de setembro e a meta quando existem. Só "leads captados" tem série semanal (S1 a S5: 860, 912, 1.004, 958 e 878), por isso só esse cartão ganha a sparkline; os outros mostram setembro e outubro como duas barras curtas, porque inventar uma série semanal para um indicador mensal seria mentir no desenho. A tela abaixo já está montada com os nove indicadores padrão.

      Indicadores que entram na primeira tela (de 9 a 11)

      9 indicadores na primeira tela; 4 com denominador escrito; 8 com meta; número principal: Receita recorrente (MRR). Dentro da faixa de 9 a 11.

      Manchete: pressione "Gerar manchete" para escrever a frase a partir do maior desvio contra a meta.

      Painel de operação · outubro de 2026Atualizado em 3 de novembro de 2026 · fonte: CRM e plataformas de mídia · dono: operações
      Receita recorrente (MRR)R$ 428.900+4,0% sobre set-1,4% sobre a meta de R$ 435.000
      Leads captados4.612+10,3% sobre set+4,8% sobre a meta de 4.400
      Custo por lead (CPL): custo de mídia ÷ leadsR$ 35,10-8,6% sobre set-2,5% sobre a meta de R$ 36,00
      Conversão lead → reunião: reuniões ÷ leads10,9%-0,9 p.p. sobre set-1,1 p.p. sobre a meta de 12,0%
      Reuniões realizadas503+2,0% sobre set-4,7% sobre a meta de 528
      Cancelamentos (churn): cancelados ÷ base ativa3,1%+0,5 p.p. sobre set+0,6 p.p. sobre a meta de 2,5%
      NPS47-4 pontos sobre set-8 pontos sobre a meta de 55
      Tempo de resposta ao lead (mediana)1,9 h-20,8% sobre set-5,0% sobre a meta de 2,0 h
      Campanhas ativas91+5,8% sobre setSem meta definida

      Variação "sobre set" compara outubro com setembro; "sobre a meta" compara outubro com a meta do mês. Verde, âmbar e vermelho reforçam a leitura, e a palavra ao lado faz o trabalho sozinha quando a cor some. Dados fictícios de exercício.

      Execute a sequência que revela o problema

      1. Marque os dezesseis indicadores. O placar avisa "16 indicadores: acima de 11" e a linha de cartões passa a ocupar duas telas; nenhum cartão se destaca, mesmo com o principal marcado.
      2. Desmarque até sobrar sete. O placar avisa "abaixo de 9" e a manchete gerada perde o churn ou o NPS, conforme o que saiu; a frase fica incompleta sem que ninguém perceba.
      3. Volte aos nove padrão e escolha "Nenhum escolhido" no número principal. O placar avisa que a tela não tem número principal: todos os cartões ficam do mesmo tamanho e a leitura de cinco segundos some.
      4. Escolha "Campanhas ativas" como principal. O placar avisa que o principal não tem meta: um número principal sem meta é um número que não pede decisão.
      5. Pressione "Gerar manchete". A frase cita o churn (0,6 p.p. acima da meta) e os leads (4,8% acima). Troque "leads" por "tempo de resposta" na seleção e gere de novo: a segunda metade da frase muda, porque o desvio bom agora é o tempo (5,0% melhor que a meta) ou o CPL, conforme o que ficou.
      6. Ligue o tema escuro no topo do portal e percorra os cartões por Tab: a ordem segue a do documento, com o principal primeiro, e os deltas continuam legíveis.

      Investigar as falhas

      Falhas típicas da primeira tela, com o sintoma visível e a correção que a oficina aplica
      SintomaCausa provávelComo confirmarCorreção
      A gestora rola antes de entender o mêsMais de 11 indicadores ou nenhum em destaque.Mostre a tela por 5 segundos a alguém que não a montou e peça a conclusão do mês.Cortar para 9 a 11, escolher o número principal e escrever a manchete.
      "Conversão 10,9%" gera discussão sobre o que éPorcentagem sem denominador escrito.O placar conta os indicadores com denominador; peça a duas pessoas a definição e compare.Rótulo com a fração ("reuniões ÷ leads") e definição a dois cliques.
      O churn de 3,1% passa despercebidoCartão igual aos outros; cor é o único sinal.Ative a versão sem cor do tema ou imprima em preto e branco.Desvio contra a meta escrito com sinal e unidade; o maior desvio ruim entra na manchete.
      A manchete muda de redação a cada semanaFrase escrita à mão por quem publica o painel.Compare as manchetes de quatro segundas; a estrutura varia e omite desvios.Manchete calculada: maior desvio ruim, maior desvio bom, unidade certa.
      O número principal é o que mais cresceu, e não o que decideEscolha pelo brilho do número.Pergunte que decisão o número principal provoca; se a resposta for "nenhuma", está errado.Principal é o indicador ligado à frase de decisão, com meta; o placar recusa principal sem meta.
      Sparkline em todo cartão, inclusive nos mensaisSérie semanal inventada por interpolação.Pergunte a fonte dos cinco pontos; se não houver dado semanal, a linha é ornamento.Sparkline só com série real; indicador mensal mostra set e out em duas barras.

      Código de referência

      O primeiro bloco é o cartão de indicador que a demonstração usa, com os quatro elementos e a sparkline opcional; o segundo é a função que escreve a manchete a partir dos desvios contra a meta, com a unidade certa por tipo de indicador. Os dois são os mesmos do arquivo de exercício, sem biblioteca.

      <!-- Um cartão; o atributo data-principal marca o número principal (um por tela). -->
      <article class="db-kpi" data-ind="churn">
        <span class="db-kpi-label">Cancelamentos (churn): cancelados ÷ base ativa</span>
        <strong class="db-kpi-value">3,1%</strong>
        <span class="db-kpi-delta" data-direction="up">+0,5 p.p. sobre set</span>
        <span class="db-kpi-meta" data-direction="bad">+0,6 p.p. sobre a meta de 2,5%</span>
        <!-- Sem série semanal real, o cartão mostra set e out em duas barras;
             a sparkline de 5 pontos só entra quando a série existe. -->
        <svg class="db-spark" viewBox="0 0 120 28" aria-hidden="true" focusable="false">
          <rect x="10" y="7" width="40" height="18" rx="2" fill="var(--db-gray)"/>
          <rect x="60" y="4" width="40" height="21" rx="2" fill="var(--db-c1)"/>
        </svg>
      </article>
      // Cada indicador: valor de outubro, meta e o sentido bom ("up" ou "down").
      // tipo: "pct" (diferença em p.p.), "pts" (pontos) ou "num" (variação em %).
      const indicadores = [
        { id: "leads", rotulo: "Leads", verbo: "superaram", valor: 4612, meta: 4400, bom: "up", tipo: "num", fmt: (v) => v.toLocaleString("pt-BR") },
        { id: "churn", rotulo: "Churn", valor: 3.1, meta: 2.5, bom: "down", tipo: "pct", fmt: (v) => v.toLocaleString("pt-BR", { minimumFractionDigits: 1 }) + "%" },
        { id: "nps", rotulo: "NPS", valor: 47, meta: 55, bom: "up", tipo: "pts", fmt: String },
      ];
      
      // Desvio relativo contra a meta, com sinal: positivo quando está melhor que a meta.
      function desvio(ind) {
        const bruto = ind.tipo === "num" ? (ind.valor - ind.meta) / ind.meta : ind.valor - ind.meta;
        const sinal = ind.bom === "down" ? -1 : 1;
        const relativo = (ind.tipo === "num" ? bruto : (ind.valor - ind.meta) / ind.meta) * sinal;
        return { bruto, relativo };
      }
      
      function unidade(ind, bruto) {
        const abs = Math.abs(bruto);
        if (ind.tipo === "pct") return abs.toLocaleString("pt-BR", { maximumFractionDigits: 1 }) + " p.p.";
        if (ind.tipo === "pts") return abs.toLocaleString("pt-BR") + (abs === 1 ? " ponto" : " pontos");
        return (abs * 100).toLocaleString("pt-BR", { maximumFractionDigits: 1 }) + "%";
      }
      
      export function manchete(selecionados) {
        const comMeta = selecionados.filter((i) => i.meta != null).map((i) => ({ ...i, ...desvio(i) }));
        if (!comMeta.length) return "Nenhum indicador com meta na primeira tela: a manchete não tem o que comparar.";
        const pior = [...comMeta].sort((a, b) => a.relativo - b.relativo)[0];
        const melhor = [...comMeta].sort((a, b) => b.relativo - a.relativo)[0];
        const lado = (i) => (i.bruto > 0 ? "acima" : "abaixo");
        const frase1 = `${pior.rotulo} de ${pior.fmt(pior.valor)} ficou ${unidade(pior, pior.bruto)} ${lado(pior)} da meta`;
        if (melhor === pior || melhor.relativo <= 0) return frase1 + ".";
        return `${frase1}; ${melhor.rotulo.toLowerCase()} ${melhor.verbo || "superou"} a meta em ${unidade(melhor, melhor.bruto)}.`;
      }
      // manchete([...]) → "Churn de 3,1% ficou 0,6 p.p. acima da meta; leads superaram a meta em 4,8%."

      Critérios de aceite

      Levar para o projeto

      Em React, o cartão vira um componente Indicador com props rotulo, valor, anterior, meta, sentidoBom e serie opcional; a manchete é uma função pura sobre a lista de indicadores, testada com os números deste exercício (o teste fixa a frase do churn e dos leads), e a seleção da primeira tela mora na URL, como o guia de React recomenda para filtro e aba. No Power BI, o cartão novo (padrão desde novembro de 2025, dado de resultado de busca sem leitura da página primária) aceita reference labels, que são a variação contra setembro e contra a meta escritas abaixo do valor, e small multiples por cliente; a manchete vira uma medida DAX de texto, calculada do mesmo jeito. A regra que viaja intacta entre as três stacks é a dos 9 a 11 com denominador e meta, e a oficina de escala cuida do gráfico que fica ao lado dos cartões.

      Oficina: escolha a escala que não mente

      O mesmo número desenhado em duas escalas conta duas histórias. No painel antigo da agência Mirante (exemplo didático, agência fictícia), o churn (cancelamentos, parcela da base que saiu no mês) de 2,5% para 3,1%, entre setembro e outubro de 2026, aparecia numa linha com o eixo começando em 2,0, e a alta parecia o dobro do que era; na reunião seguinte, a mesma série foi mostrada a partir de zero e alguém disse que "não mudou nada". Nenhuma das duas telas mentiu nos dados, e as duas enganaram. Nesta oficina você desenha a série de churn de abril a outubro e a série semanal de leads de setembro e outubro em quatro escalas (zero, cortada com o início escrito, logarítmica e indexada), varia a altura do gráfico e compara quatro clientes em múltiplos pequenos na mesma escala ou em escalas livres; a saída diz, em cada caso, o que a escala exagera ou esconde.

      Cenário de exercício

      Quem
      Otávio, desenvolvedor da Mirante que trabalha com agente de código, e Helena, diretora de operações, que compara o churn do mês com a meta e com os meses anteriores toda segunda-feira.
      Tarefa
      Desenhar a série de churn (abr a out de 2026) e a de leads por semana (set e out) de modo que a variação visível corresponda à variação real, com o início do eixo escrito quando ele não for zero, e comparar quatro clientes sem que a escala de cada um esconda a diferença entre eles.
      Falha vista
      O agente de código entregou o gráfico com o eixo cortado em 2,0 por padrão da biblioteca, sem escrever o início; a alta de 0,6 p.p. de outubro de 2026 parecia uma alta de 120%. Na matriz de clientes, cada gráfico tinha a própria escala, e a Loja Marés em queda de 7% em outubro de 2026 parecia igual aos Cursos Bússola em alta de 15%.
      Resultado esperado
      Barra e área partindo de zero; linha pode cortar, com o início escrito no eixo e na legenda; logarítmica só quando os valores atravessam ordens de grandeza; indexada quando o que importa é o ritmo; múltiplos pequenos na mesma escala, com a escala comum escrita uma vez.

      Quatro escalas, quatro perguntas diferentes

      A escala é a resposta a uma pergunta, antes de ser uma opção estética. O eixo a partir de zero responde "quanto é": a altura da barra é proporcional ao valor, e é por isso que barra e área sempre partem de zero. O eixo cortado responde "quanto variou dentro de uma faixa": serve a linhas e dispersões, desde que o início esteja escrito no eixo e no título, como fixa o guia de gráficos. Franconeri e colegas, na revisão publicada em dezembro de 2021 na Psychological Science in the Public Interest, lembram que começar em zero nem sempre é melhor: pode esconder um padrão relevante ou criar a ilusão de estabilidade. O eixo logarítmico (cada divisão multiplica o valor por um fator fixo, como 10) responde "qual a taxa de crescimento" e só faz sentido quando a série atravessa ordens de grandeza; de 2,2% a 3,1%, a faixa do churn de 2026, ele achata tudo. O eixo indexado (todos os valores divididos pelo valor de um mês de referência, que vira 100) responde "quem cresce mais rápido" e é a alternativa certa ao eixo duplo, que a coleção proíbe.

      A altura do gráfico também é escala. Heer, Kong e Agrawala mostraram na CHI de 2009 que reduzir a altura de um gráfico de linha derruba a precisão da leitura a partir de certos pontos, e que em alturas pequenas vale trocar a forma (bandas em camadas) em vez de só encolher. A regra prática da Mirante: abaixo de 120 px, a série vira sparkline sem eixo ao lado do número, e nunca um gráfico completo com letras minúsculas.

      Mapa da oficina. A escala sai da pergunta, passa pelo tipo de marca, ganha o início escrito e é conferida contra a variação real.
      1. PerguntaQuanto é, quanto variou, qual a taxa ou quem cresce mais?
      2. MarcaBarra e área partem de zero; linha pode cortar.
      3. Início escritoEixo cortado declara o início no eixo e no título.
      4. ConferênciaA variação visível bate com a variação calculada.

      1. Helena quer saber se o churn de outubro está acima da meta e quanto: pergunta de variação dentro de uma faixa, que aceita linha cortada com o início escrito.
      2. Para a comparação de clientes, a pergunta é "quanto é": barras, que partem de zero, e a mesma escala para os quatro.
      3. O eixo cortado recebe o início no primeiro tique e no título ("eixo a partir de 2,0%"); a tabela gêmea traz os valores exatos.
      4. A conferência compara a altura visível com a variação calculada: 24% de alta real precisa de 24% a mais de altura; se der 2,2 vezes, a escala está mentindo.

      Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

      Demonstração: troque a escala, a altura e a comparação

      Dados de exercício: churn mensal da Mirante de abril a outubro de 2026 (2,2; 2,4; 2,3; 2,8; 2,5; 2,6 e 3,1%), com meta de 2,5%; leads por semana em setembro (790, 835, 870, 905 e 780) e outubro (860, 912, 1.004, 958 e 878), com meta mensal de 4.400 (880 por semana); e quatro clientes com leads de outubro e variação sobre setembro: Padaria Horizonte 612 (+9%), Loja Marés 498 (-7%), Cursos Bússola 296 (+15%) e Studio Fotográfico Prisma 274 (-11%). O gráfico inicial, com escala a partir de zero, já está no HTML.

      Série

      A partir de zero: a alta de 2,5% (set/2026) para 3,1% (out/2026) ocupa 24% a mais de altura, igual à variação real. Churn de outubro de 2026 0,6 p.p. acima da meta de 2,5%.

      Churn mensal da Mirante, abril a outubro de 2026, eixo a partir de zero Linha de sete meses: 2,2; 2,4; 2,3; 2,8; 2,5; 2,6 e 3,1%. Linha tracejada da meta em 2,5%. Eixo de 0 a 4%. Outubro é o maior valor e fica 0,6 p.p. acima da meta. Dados fictícios. Churn mensal · % da base · eixo a partir de 0 0 1 2 3 4 meta 2,5 3,1 abrmaijunjulagosetout Fonte: CRM da Mirante (dados fictícios) · meta mensal 2,5%
      Valores desenhados pela demonstração (dados fictícios de 2026)
      SérieValoresReferência
      Churn mensal (% da base)abr 2,2 · mai 2,4 · jun 2,3 · jul 2,8 · ago 2,5 · set 2,6 · out 3,1Meta 2,5%; índice =100 em abril
      Leads por semana, setembro790 · 835 · 870 · 905 · 780 (total 4.180)Índice =100 na S1 de cada mês
      Leads por semana, outubro860 · 912 · 1.004 · 958 · 878 (total 4.612)Meta 4.400 no mês (880 por semana)
      Quatro clientes, leads de outubroPadaria Horizonte 612 (set 561) · Loja Marés 498 (set 535) · Cursos Bússola 296 (set 257) · Studio Prisma 274 (set 308)Variação sobre set: +9%, -7%, +15%, -11%

      Execute a sequência que revela o problema

      1. Com o churn a partir de zero, troque para "Cortada". O eixo passa a começar em 2,0 e a leitura diz que a alta de set para out parece 2,2 vezes maior do que é; o título do gráfico escreve "eixo a partir de 2,0%".
      2. Troque para "Logarítmica". A curva quase some: entre 2,2 e 3,1 não há ordem de grandeza a atravessar, e a leitura explica por que essa escala não serve a esta série.
      3. Troque para "Indexada". Abril vira 100 e outubro vira 141: a figura passa a responder "quanto cresceu desde abril", e serve para colocar o churn e o NPS na mesma tela sem eixo duplo.
      4. Escolha a série de leads por semana, a partir de zero, contra setembro. As duas linhas mostram a S3 de outubro (1.004) 15% acima da S3 de setembro (870). Corte o eixo em 700 e a mesma diferença parece 1,8 vezes maior.
      5. Mude a altura para 80 px. A grade cai para duas linhas e a leitura avisa que a variação de 0,6 p.p. ocupa poucos pixels; a oficina recomenda a sparkline ao lado do número em vez do gráfico completo.
      6. Ligue "Múltiplos pequenos". Com a caixa marcada, os quatro clientes dividem a escala de 0 a 700 e a Padaria Horizonte aparece com mais do dobro do Studio Prisma. Desmarque: cada cliente ganha a própria escala, as quatro barras ficam da mesma altura e a queda da Loja Marés desaparece.

      Investigar as falhas

      Falhas de escala, com o sintoma que a gestora vê e a correção
      SintomaCausa provávelComo confirmarCorreção
      Alta de 0,6 p.p. parece o dobroEixo cortado pelo padrão da biblioteca, sem o início escrito.Divida a altura visível das duas marcas e compare com a razão dos valores (3,1 ÷ 2,5 = 1,24).Linha pode cortar com o início no eixo e no título; barra volta a zero.
      A barra mais alta não é o maior valorBarra em eixo cortado ou logarítmico.Meça duas barras com o inspetor e compare com os valores da tabela.Barra e área sempre a partir de zero (princípio da tinta proporcional, Wilke, cap. 17).
      A curva "não mudou nada"Logarítmica numa série de uma ordem de grandeza, ou eixo até 100 para uma série que vai a 3.Calcule máximo ÷ mínimo: abaixo de 10, a logarítmica achata.Escala linear com o máximo "nice" logo acima do maior valor; logarítmica só acima de uma ordem de grandeza.
      Duas séries com um eixo à esquerda e outro à direitaEixo duplo para comparar churn e MRR.Mude o máximo de um dos eixos: as linhas se cruzam em outro ponto, logo o cruzamento era arbitrário.Indexar as duas em =100 na referência, ou dois gráficos empilhados com o mesmo eixo de tempo.
      Quatro clientes parecem iguais na matrizCada múltiplo com a própria escala.Leia os valores na tabela: 612 contra 274 não podem ter barras da mesma altura.Escala comum escrita uma vez por linha; escala livre só com aviso no título e nunca para comparar entre painéis.
      Rótulo ilegível em gráfico de 80 pxGráfico completo encolhido em vez de trocar de forma.Meça a letra no inspetor: abaixo de 14 px no tamanho exibido reprova.Abaixo de 120 px, sparkline sem eixo ao lado do número; valor e variação escritos.

      Código de referência

      A função escala() devolve, para cada modo, o domínio (início e fim do eixo), a transformação de valor para posição e o rótulo que o título precisa escrever. O renderizador desenha o SVG com createElementNS, sem biblioteca; a mesma função alimenta a option do ECharts no bloco seguinte.

      // Modos: "zero", "cortada", "log", "indexada". Devolve dominio, posicao(v) e rotulo.
      export function escala(valores, modo, { inicio = null, referencia = valores[0], altura = 160 } = {}) {
        const limpos = valores.filter((v) => v != null);           // ausência continua ausência
        // Teto "nice": primeiro degrau acima do maior valor (3,1 → 4; 612 → 700; 1.004 → 1.200)
        const nice = (n) => { const p = 10 ** Math.floor(Math.log10(n)); const f = n / p;
          return [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 10].find((d) => d >= f) * p; };
        if (modo === "log") {
          const positivos = limpos.filter((v) => v > 0);            // log ignora zero e negativos
          const d = [Math.min(...positivos), Math.max(...positivos)];
          const l = (v) => Math.log10(v);
          return { dominio: d, rotulo: "escala logarítmica", avisoOrdem: d[1] / d[0] < 10,
            posicao: (v) => altura - ((l(v) - l(d[0])) / (l(d[1]) - l(d[0]))) * altura };
        }
        if (modo === "indexada") {
          const base = referencia, idx = limpos.map((v) => (v / base) * 100);
          const d = [Math.min(100, ...idx), nice(Math.max(...idx))];
          return { dominio: d, rotulo: "índice =100 na referência", valores: idx,
            posicao: (v) => altura - (((v / base) * 100 - d[0]) / (d[1] - d[0])) * altura };
        }
        const topo = nice(Math.max(...limpos));
        const base = modo === "cortada" ? (inicio ?? Math.floor(Math.min(...limpos))) : 0;
        return { dominio: [base, topo], rotulo: base === 0 ? "eixo a partir de 0" : `eixo a partir de ${base.toLocaleString("pt-BR")}`,
          posicao: (v) => altura - ((v - base) / (topo - base)) * altura };
      }
      
      // Conferência: a razão das alturas visíveis precisa bater com a razão dos valores.
      export function distorcao(e, a, b) {
        // real: razão entre os valores; visível: razão entre as alturas a partir do início do eixo
        return { real: b / a, visivel: (b - e.dominio[0]) / (a - e.dominio[0]) };
      }
      // escala([2.2,2.4,2.3,2.8,2.5,2.6,3.1], "cortada", { inicio: 2 }) → distorcao: real 1,24; visível 2,2
      // ECharts 6.1.0 (vendor próprio do portal): a mesma decisão escrita na option.
      const option = {
        title: { text: "Churn mensal · % da base · eixo a partir de 2,0", left: 24 },
        xAxis: { type: "category", data: ["abr", "mai", "jun", "jul", "ago", "set", "out"] },
        yAxis: {
          type: "value",            // "log" só quando máximo ÷ mínimo passa de 10
          min: 2, max: 4,           // eixo cortado: declarado, e escrito no título
          // 6.1.0: dataMin e dataMax pedem a extensão "nice" a partir dos dados
          // yAxis: { type: "value", min: "dataMin", max: "dataMax" }
          axisLabel: { formatter: (v) => v.toLocaleString("pt-BR") + "%" },
        },
        series: [
          { type: "line", data: [2.2, 2.4, 2.3, 2.8, 2.5, 2.6, 3.1], symbolSize: 8,
            markLine: { data: [{ yAxis: 2.5, name: "meta" }], label: { formatter: "meta 2,5" } } },
        ],
        aria: { enabled: true, decal: { show: true } },
      };
      // Para barras: yAxis.min fica em 0 e o título perde o "a partir de".

      Critérios de aceite

      Levar para o projeto

      No ECharts, a decisão mora em yAxis.min e yAxis.max (eixo cortado declarado), em type: 'log' (só acima de uma ordem de grandeza; a 6.1.0 ignora valores não positivos no log) e, a partir da 6.1.0, em min: 'dataMin' e max: 'dataMax', que pedem a extensão "nice" a partir dos dados; a matriz de múltiplos usa o sistema de coordenadas matrix da 6.0 com um eixo compartilhado, como a oficina de matriz mostra. No Observable Plot, y: { domain: [2, 4], label: "churn (%) · eixo a partir de 2" } e type: "log" fazem o mesmo, e a faceta (fy) compartilha a escala por padrão. No Power BI, o eixo Y do visual aceita início e fim manuais, e os small multiples trazem um eixo por linha e um por coluna, sincronizados, segundo a documentação do Microsoft Learn; a escala livre precisa ser desligada de propósito. Em React, a função escala() acima vira um hook puro e ganha um teste que fixa a distorção de 2,2 do exercício, como o guia de React recomenda para todo contrato de componente.

      Oficina: monte um carrossel de painéis que roda para os dois lados sem perder ninguém

      Quando a matriz de clientes não cabe na tela, a Mirante (agência fictícia deste guia) pagina os painéis em cartões que a pessoa roda para a esquerda e para a direita, como nas postagens do Instagram e do TikTok. O carrossel certo é o que a pessoa controla: ela vê que existe um próximo cartão, sabe em qual está, chega lá pelo dedo, pelo botão ou pelo teclado, e nada se move sozinho enquanto ela lê. Nesta oficina você varia cartões por vista, peek, tipo de encaixe e autoplay, lê o que cada combinação faz com a leitura e copia a stack nativa, com a biblioteca entrando só quando o problema crescer.

      Cenário de exercício

      Quem
      Otávio, desenvolvedor da Mirante que trabalha com agente de código, e Helena, diretora de operações, que lê o painel no notebook na segunda-feira e no celular entre reuniões.
      Tarefa
      Paginar os painéis semanais de outubro (um cartão por semana, com leads, comparação com setembro e meta) num carrossel que funcione com dedo, botão e teclado, nas larguras de 1280 e 390 px.
      Falha vista
      A primeira versão, gerada por um agente, usava uma biblioteca de 40 KB com autoplay de 4 segundos, sem botões visíveis e sem indicação de posição. Helena perdia o cartão que estava lendo quando ele avançava sozinho, e no celular o primeiro cartão ocupava 100% da largura (versão de outubro de 2026), então ninguém percebia que havia mais quatro.
      Resultado esperado
      Carrossel nativo com scroll-snap, próximo cartão aparecendo na borda, botões de 44 px, marcadores de página, "Cartão 2 de 5" anunciado ao leitor de tela, setas do teclado, sem autoplay, e movimento reduzido respeitado. Biblioteca só se surgir loop infinito, arrasto livre ou efeitos.

      Três sinais que o carrossel precisa dar o tempo todo

      Um carrossel esconde conteúdo por definição, e o NN/g mediu em 2016 que conteúdo escondido é menos descoberto. O que torna o padrão aceitável num painel são três sinais permanentes: a borda do próximo cartão visível (o peek, que diz "há mais"), a posição escrita e marcada ("3 de 12" mais os pontos) e controles que a pessoa alcança sem precisar descobrir um gesto. Autoplay quebra os três: move o conteúdo que a pessoa está lendo, muda a posição sem pedido e tira o controle. Em painel de decisão ele fica desligado; em vitrine de campanha, se existir, pausa ao passar o ponteiro e ao receber foco, e para de vez com movimento reduzido.

      Mapa da oficina. O carrossel nasce como lista rolável que já funciona sem script; cada camada seguinte acrescenta um sinal, e nenhuma delas substitui a anterior.
      1. Lista rolávelFlex com rolagem horizontal e snap: já funciona com o dedo e com a barra.
      2. SinaisPeek do próximo cartão, marcadores e "N de M" em região viva.
      3. ControlesBotões de 44 px, setas e Home/End no trilho, foco visível.
      4. SincroniaO gesto também atualiza a posição: scrollend ou atraso curto.

      1. O trilho é um contêiner com display:flex, overflow-x:auto e scroll-snap-type:x mandatory; cada cartão tem scroll-snap-align:start. Sem nenhum script, rolar com o dedo ou com a barra já para num cartão.
      2. Cada cartão ocupa 85% da largura a 390 px (o teste de celular da casa em 2026), para o seguinte aparecer na borda; os marcadores são botões numa lista e a posição vai para um parágrafo com role="status".
      3. Os botões chamam scrollIntoView no cartão alvo; no trilho, as setas e Home e End fazem o mesmo; o foco visível tem 3 px.
      4. Quando a pessoa rola com o dedo, scrollend (ou um atraso de 120 ms onde o evento não existe) descobre o cartão mais próximo do início visível e atualiza marcadores e posição.

      Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

      Demonstração: varie os parâmetros e leia o efeito

      Dados de exercício: cinco semanas de outubro, com os leads de setembro e a meta de 880 em cada cartão. Mude os controles e leia, na linha de saída, o que a combinação faz com a leitura. O carrossel abaixo usa exatamente o código da seção Carrossel de cartões.

      Semana 1 de outubro

      860

      790 leads na mesma semana de setembro; meta de 880.

      Semana 2 de outubro

      912

      835 leads na mesma semana de setembro; meta de 880.

      Semana 3 de outubro

      1.004

      870 leads na mesma semana de setembro; meta de 880.

      Semana 4 de outubro

      958

      905 leads na mesma semana de setembro; meta de 880.

      Semana 5 de outubro

      878

      780 leads na mesma semana de setembro; meta de 880.

      3 cartões por vista, com o próximo aparecendo na borda (peek). Snap obrigatório: a rolagem sempre para num cartão. Sem autoplay, como manda o padrão.

      Execute a sequência que revela o problema

      1. Deixe 3 por vista com peek e role com a barra ou o dedo: a rolagem para num cartão e os marcadores acompanham sem clique.
      2. Troque para 1 por vista e desligue o peek. O cartão ocupa toda a largura e nada diz que existem mais quatro; é o defeito da primeira versão da Mirante no celular.
      3. Escolha "Sem encaixe" e role devagar: o cartão para cortado ao meio, e a posição anunciada vira um chute.
      4. Ligue o autoplay e comece a ler o cartão 2. Ele vai embora antes de você terminar. Passe o ponteiro ou dê foco a um botão: a rotação pausa. Ative movimento reduzido no sistema ou no interruptor do portal: o autoplay nem começa.
      5. Com o foco no trilho (Tab até ele), use seta para a direita, End e Home. A posição é anunciada e o foco permanece onde estava.

      Investigar as falhas

      Falhas típicas de carrossel em painel e como confirmar cada uma
      SintomaCausa provávelComo confirmarCorreção
      Ninguém percebe que há mais cartõesCartão a 100% da largura, sem peek, sem marcadores.A 390 px, pergunte a alguém quantos cartões existem antes de tocar.Cartão a 85% da largura no celular, marcadores de página e "N de M" escrito.
      O cartão para cortado ao meioSem scroll-snap-type ou com proximity em cartões largos.Role devagar e solte no meio de um cartão.scroll-snap-type: x mandatory e scroll-snap-align: start em cada cartão; scroll-padding igual ao padding do trilho.
      Os marcadores não acompanham o dedoSó o clique nos botões atualiza a posição.Role com o dedo e observe os pontos.Ouvir scrollend (com atraso de 120 ms como reserva) e calcular o cartão mais próximo do início visível.
      O leitor de tela diz "botão" e nada maisBotões só com ícone, sem nome; trilho sem papel nem rótulo.Ative o leitor de tela e percorra os controles.aria-label nos botões ("Próximo cliente"), trilho com role="group", aria-roledescription="carrossel" e aria-label; cada cartão com "Cartão 3 de 12: nome".
      O conteúdo some enquanto a pessoa lêAutoplay ligado por padrão.Comece a ler um cartão e cronometre.Sem autoplay em painel; se existir, pausa em hover e foco e nunca roda com movimento reduzido (WCAG 2.2.2).
      A página inteira rola junto ao chegar no fimSem overscroll-behavior-x: contain.No celular, continue o gesto depois do último cartão.overscroll-behavior-x: contain no trilho; a página só rola na vertical.
      A rolagem suave continua com movimento reduzidoscroll-behavior: smooth no trilho sem condição.Ative movimento reduzido e clique em "Próximo".Suavidade só em @media (prefers-reduced-motion: no-preference) e fora do interruptor do portal; no script, behavior: "auto" quando reduzido.

      Código de referência

      O CSS é o mesmo do painel do topo; o script liga qualquer .db-carrossel e devolve um objeto com ir(n), para outro componente pedir um cartão.

      .db-carrossel-trilho {
        display: flex; gap: 12px;
        overflow-x: auto; overscroll-behavior-x: contain;
        scroll-snap-type: x mandatory; scroll-padding-inline: 4px;
        padding: 4px 4px 10px; scrollbar-width: thin;
      }
      .db-carrossel-trilho > * { flex: 0 0 min(85%, 300px); scroll-snap-align: start; scroll-snap-stop: always; }
      @container (min-width: 700px) { .db-carrossel-trilho > * { flex-basis: calc((100% - 24px) / 3); } }
      @media (prefers-reduced-motion: no-preference) {
        html:not([data-llx-motion="reduce"]) .db-carrossel-trilho { scroll-behavior: smooth; }
      }
      .db-carrossel-controles button { min-width: 44px; min-height: 44px; }
      .db-carrossel-marcadores button[aria-current="true"]::before { background: var(--db-accent); transform: scale(1.3); }
      function ligarCarrossel(raiz, { rotulo = "Cartão", nome = "Cartões" } = {}) {
        const trilho = raiz.querySelector(".db-carrossel-trilho");
        const cartoes = [...trilho.children];
        const anterior = raiz.querySelector("[data-db-anterior]"), proximo = raiz.querySelector("[data-db-proximo]");
        const marcadores = raiz.querySelector(".db-carrossel-marcadores"), pos = raiz.querySelector(".db-carrossel-pos");
        const reduzido = () => document.documentElement.dataset.llxMotion === "reduce" || matchMedia("(prefers-reduced-motion: reduce)").matches;
        let atual = 0;
        trilho.setAttribute("role", "group"); trilho.setAttribute("aria-roledescription", "carrossel");
        trilho.setAttribute("aria-label", nome); trilho.tabIndex = 0;
        cartoes.forEach((c, i) => { c.setAttribute("role", "group"); c.setAttribute("aria-label", `${rotulo} ${i + 1} de ${cartoes.length}: ${c.dataset.dbNome}`); });
        cartoes.forEach((c, i) => { const b = document.createElement("button"); b.type = "button";
          b.setAttribute("aria-label", `Ir ao ${rotulo.toLowerCase()} ${i + 1}: ${c.dataset.dbNome}`);
          b.addEventListener("click", () => ir(i)); const li = document.createElement("li"); li.append(b); marcadores.append(li); });
        const pintar = () => {
          marcadores.querySelectorAll("button").forEach((b, i) => b.toggleAttribute("aria-current", i === atual) || (i === atual && b.setAttribute("aria-current", "true")));
          pos.textContent = `${rotulo} ${atual + 1} de ${cartoes.length}: ${cartoes[atual].dataset.dbNome}`;
          anterior.disabled = atual === 0; proximo.disabled = atual === cartoes.length - 1;
        };
        const ir = (i) => { atual = Math.max(0, Math.min(cartoes.length - 1, i));
          cartoes[atual].scrollIntoView({ block: "nearest", inline: "start", behavior: reduzido() ? "auto" : "smooth" }); pintar(); };
        anterior.addEventListener("click", () => ir(atual - 1)); proximo.addEventListener("click", () => ir(atual + 1));
        trilho.addEventListener("keydown", (e) => { const mapa = { ArrowRight: atual + 1, ArrowLeft: atual - 1, Home: 0, End: cartoes.length - 1 };
          if (e.key in mapa) { e.preventDefault(); ir(mapa[e.key]); } });
        const sincronizar = () => { const x = trilho.scrollLeft; let melhor = 0, dist = Infinity;
          cartoes.forEach((c, i) => { const d = Math.abs(c.offsetLeft - trilho.offsetLeft - x); if (d < dist) { dist = d; melhor = i; } });
          if (melhor !== atual) { atual = melhor; pintar(); } };
        if ("onscrollend" in window) trilho.addEventListener("scrollend", sincronizar);
        else { let t; trilho.addEventListener("scroll", () => { clearTimeout(t); t = setTimeout(sincronizar, 120); }, { passive: true }); }
        pintar();
        return { ir, get atual() { return atual; } };
      }
      /* Melhoria progressiva: botões e marcadores gerados pelo próprio CSS (Chrome e Edge 135+).
         Entra dentro de @supports; o script acima continua sendo a retaguarda nos outros navegadores. */
      @supports (scroll-marker-group: after) {
        .db-carrossel-trilho { scroll-marker-group: after; }
        .db-carrossel-trilho::scroll-marker-group { display: flex; gap: 6px; justify-content: center; padding: 8px; }
        .db-carrossel-trilho > *::scroll-marker { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--db-line-soft); }
        .db-carrossel-trilho > *::scroll-marker:target-current { background: var(--db-accent); }
        .db-carrossel-trilho::scroll-button(inline-start) { content: "Anterior" / "Cartão anterior"; }
        .db-carrossel-trilho::scroll-button(inline-end) { content: "Próximo" / "Próximo cartão"; }
      }

      Critérios de aceite

      Levar para o projeto

      Em React, o mesmo contrato vira um componente Carrossel que recebe a lista de cartões e expõe irPara(indice) por ref; o estado "cartão atual" pode ir para a URL com nuqs quando o cartão é uma página de painel que vale compartilhar. Se o produto pedir loop infinito, arrasto livre com inércia ou sincronia entre dois carrosséis, o Embla Carousel 8.6.0 (cerca de 7 KB em gzip, dado aproximado a conferir no PR) implementa isso sobre o mesmo DOM, sem trocar a marcação; o Swiper 14.4.0 entra só por efeitos visuais, com o peso declarado. No Power BI, o equivalente é o botão de página com bookmarks que alternam entre visões, e a lição é a mesma: a pessoa precisa ver em que página está e como voltar. O guia de Navegação cobre gaveta, abas e paginação, que são as alternativas quando os cartões viram mais de 12.

      Oficina: uma matriz de múltiplos pequenos que compara sem enganar

      Doze clientes, cinco semanas cada: sessenta números que Helena, da agência Mirante (fictícia), precisa varrer em segundos para saber quem subiu, quem caiu e quem ficou parado. A matriz de múltiplos pequenos (small multiples, no nome que o Power BI usa) resolve isso com uma célula por cliente e o mesmo desenho repetido. O que decide se a matriz informa ou engana são três escolhas que esta oficina deixa você virar ao vivo: a escala (a mesma para todas as células ou livre por célula), a ordem das células e a notação de comparação com o período anterior.

      Cenário de exercício

      Quem
      Camila, designer da Mirante, e Helena, que lê a matriz na segunda-feira antes de escolher com quais três clientes vai conversar na semana.
      Tarefa
      Mostrar os leads por semana de outubro dos 12 clientes numa grade que caiba na primeira tela a 1280 px e que vire carrossel a 390 px, com a variação sobre setembro escrita em cada célula.
      Falha vista
      A matriz anterior usava escala livre por célula e ordem alfabética. Café Ladeira, com variação de 0% em outubro de 2026, parecia o cliente mais agitado da grade, porque a linha dele ocupava toda a altura da célula. Studio Prisma, que caiu 11% em outubro de 2026, ficava no canto inferior direito, onde ninguém chegava.
      Resultado esperado
      Mesma escala em todas as células, ordem por variação (ou por volume, conforme a pergunta), notação de realizado contra período anterior quando a pergunta é "mudou?", destaque de um cliente sem repintar os outros e tabela gêmea com os sessenta números.

      Escala compartilhada é o que faz a repetição valer

      A força da matriz vem da repetição: o olho aprende o desenho uma vez e passa a comparar posições numa escala comum, a tarefa mais precisa do ranking de Cleveland e McGill (1984). Isso só é verdade se a escala for a mesma. Com escala livre, cada célula estica a própria série até o teto, e a inclinação deixa de significar alguma coisa entre células. O Power BI sincroniza os eixos por padrão nas small multiples (um eixo Y por linha e um eixo X por coluna, segundo o Microsoft Learn), e a web precisa fazer o mesmo à mão: calcular o máximo global antes de desenhar a primeira célula. Escala livre tem um uso legítimo, quando a pergunta é a forma de cada série e não a magnitude, e nesse caso o eixo vai escrito em cada célula.

      Mapa da oficina. A matriz é construída de fora para dentro: primeiro a pergunta e a escala global, depois a ordem, depois cada célula, por último o destaque.
      1. Pergunta"Quem mudou?" pede ordem por variação e notação de comparação.
      2. Escala globalMáximo de todas as séries calculado antes de desenhar.
      3. CélulasUm desenho repetido, rótulo e variação escrita em cada uma.
      4. DestaqueUm cliente em evidência, os outros atenuados, cores intactas.

      1. Helena quer saber quem mudou, então a ordem é por variação sobre setembro e cada célula compara outubro com setembro.
      2. O script percorre as 12 séries, acha o maior valor (134, da Padaria Horizonte na semana 3) e fixa o eixo de 0 a 134 para todas as células.
      3. Cada célula recebe nome, o desenho (linha ou barras AC contra PY), o total e a variação; o SVG tem aria-label com os cinco valores.
      4. Escolher um cliente marca a célula dele e atenua as demais por opacidade; nenhuma cor muda, porque cor segue a entidade e não o filtro.

      Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

      Demonstração: vire a escala, a ordem e a notação

      Dados de exercício: leads por semana de outubro dos 12 clientes e a variação sobre setembro. A linha de saída diz o que cada combinação faz com a leitura.

      Cursos Bússola296 leads · +15% sobre set
      Escola Compasso431 leads · +12% sobre set
      Padaria Horizonte612 leads · +9% sobre set
      Oficina Trilho341 leads · +6% sobre set
      Dental Aurora268 leads · +5% sobre set
      Clínica Vale Verde540 leads · +4% sobre set
      Academia Pulso402 leads · +2% sobre set
      Pet Serena318 leads · +1% sobre set
      Café Ladeira255 leads · 0% sobre set
      Imobiliária Lume377 leads · -3% sobre set
      Loja Marés498 leads · -7% sobre set
      Studio Fotográfico Prisma274 leads · -11% sobre set

      Mesma escala em todas as células (0 a 134): a altura compara clientes entre si; Café Ladeira parece quase plano porque é pequeno, e isso é verdade. Ordem por variação: os que mais cresceram vêm primeiro e os que mais caíram ficam no fim, onde Helena procura problemas.

      Leads por semana de outubro de 2026 por cliente (unidade: leads) e variação sobre setembro
      ClienteS1S2S3S4S5TotalSobre set
      Padaria Horizonte108118134130122612+9%
      Clínica Vale Verde102106116110106540+4%
      Loja Marés112104989490498-7%
      Escola Compasso7280949689431+12%
      Academia Pulso7882868076402+2%
      Imobiliária Lume8076747572377-3%
      Oficina Trilho6266727269341+6%
      Pet Serena6364666362318+1%
      Cursos Bússola4654646864296+15%
      Studio Fotográfico Prisma6658545046274-11%
      Dental Aurora5052565654268+5%
      Café Ladeira51505251512550%

      Execute a sequência que revela o problema

      1. Troque a escala para "Livre por célula". Café Ladeira (0%) passa a parecer tão agitado quanto Cursos Bússola (+15%), porque cada linha estica até o teto da própria célula. Leia a saída: ela avisa que o eixo precisa ir escrito em cada célula.
      2. Volte à mesma escala e mude a ordem para alfabética. Studio Prisma, que mais caiu, vai para o fim da grade; com ordem por variação, ele fica na última célula com um motivo, e com ordem por volume, Padaria Horizonte abre a leitura.
      3. Escolha a notação de barras: cada semana mostra outubro (barra escura) ao lado de setembro (barra cinza), a mesma convenção de realizado contra período anterior da notação IBCS. A pergunta "mudou?" fica respondida célula a célula.
      4. Destaque Loja Marés. A célula ganha borda e as outras perdem opacidade; nenhuma cor muda. Troque o destaque para Padaria Horizonte e confira que Loja Marés voltou ao estado normal sem repintura.
      5. Reduza a janela para 390 px: a grade passa a uma coluna com rolagem vertical. Quando a matriz é a peça central do celular, a oficina de carrossel mostra como paginar as células.

      Investigar as falhas

      Falhas típicas de matriz de múltiplos pequenos e como confirmar cada uma
      SintomaCausa provávelComo confirmarCorreção
      Cliente parado parece agitadoEscala livre por célula.Compare a inclinação de Café Ladeira (0%) com a de Cursos Bússola (+15%).Máximo global antes de desenhar; escala livre só com o eixo escrito em cada célula e um aviso na legenda.
      Quem caiu fica escondidoOrdem alfabética ou ordem de cadastro.Peça a alguém para apontar os três que mais caíram em 5 segundos.Ordem pela pergunta: variação quando a pergunta é "mudou?", volume quando é "quem pesa mais?".
      Cores mudam ao filtrarCor atribuída pela posição (rank) em vez da entidade.Destaque um cliente e observe se outro trocou de cor.Cor segue a entidade; destaque por borda e opacidade, nunca por repintura.
      Leitor de tela lê sessenta números seguidosSVG sem nome por célula; ou tabela sem cabeçalho.Percorra a grade com o leitor de tela.aria-label curto por célula (nome e cinco valores) e tabela gêmea com th scope; a matriz como role="list".
      Texto abaixo de 14 px no celularRótulo dentro do SVG com viewBox largo.Meça a letra renderizada a 390 px.Nome e variação em HTML fora do SVG; o SVG só desenha a série.
      A matriz não cabe na primeira telaMais de 12 células ou célula alta demais.Conte quantas células aparecem a 1280 por 720 px sem rolar.Até 12 células de 48 px de gráfico; acima disso, 10 mais "outros" ou carrossel por grupo.

      Código de referência

      // Matriz de múltiplos pequenos sem biblioteca: escala global, ordem pela pergunta, destaque sem repintura.
      const NS = "http://www.w3.org/2000/svg";
      function matriz(clientes, { escala = "mesma", ordem = "variacao", destaque = null } = {}) {
        const lista = [...clientes];
        if (ordem === "variacao") lista.sort((a, b) => b.var - a.var);
        else if (ordem === "valor") lista.sort((a, b) => b.leads - a.leads);
        else lista.sort((a, b) => a.nome.localeCompare(b.nome, "pt-BR"));
        const maxGlobal = Math.max(...clientes.flatMap((c) => c.serie));   // passo 2: antes de desenhar
        const grade = document.createElement("div");
        grade.className = "db-matriz"; grade.setAttribute("role", "list");
        for (const c of lista) {
          const max = escala === "mesma" ? maxGlobal : Math.max(...c.serie);
          const y = (v) => 44 - (v / max) * 40;
          const cel = document.createElement("div");
          cel.className = "db-celula"; cel.setAttribute("role", "listitem");
          if (destaque !== null) cel.toggleAttribute(destaque === c.nome ? "data-destaque" : "data-apagado", true);
          const nome = document.createElement("strong"); nome.textContent = c.nome;
          const svg = document.createElementNS(NS, "svg");
          svg.setAttribute("viewBox", "0 0 160 48"); svg.setAttribute("role", "img");
          svg.setAttribute("aria-label", `${c.nome}: ${c.serie.map((v, i) => `S${i + 1} ${v}`).join(", ")}`);
          const path = document.createElementNS(NS, "path");
          path.setAttribute("d", c.serie.map((v, i) => `${i ? "L" : "M"}${8 + i * 36},${y(v).toFixed(1)}`).join(" "));
          path.setAttribute("fill", "none"); path.setAttribute("stroke", "var(--db-accent)"); path.setAttribute("stroke-width", "2");
          svg.append(path);
          const meta = document.createElement("span"); meta.className = "db-kpi-meta";
          meta.textContent = `${c.leads.toLocaleString("pt-BR")} leads · ${c.var > 0 ? "+" : ""}${c.var}% sobre set`;
          cel.append(nome, svg, meta); grade.append(cel);
        }
        return grade;
      }
      // A mesma matriz no Apache ECharts 6.1 com o sistema de coordenadas em matriz: uma célula por cliente,
      // cada célula com o próprio grid, e o máximo global fixado em todos os eixos.
      const maxGlobal = Math.max(...clientes.flatMap((c) => c.serie));
      const option = {
        matrix: { x: { data: [1, 2, 3, 4].map(String) }, y: { data: [1, 2, 3].map(String) }, top: 8, left: 8, right: 8, bottom: 8 },
        grid: clientes.map((c, i) => ({ coordinateSystem: "matrix", coord: [String((i % 4) + 1), String(Math.floor(i / 4) + 1)], left: 6, right: 6, top: 22, bottom: 6 })),
        xAxis: clientes.map((c, i) => ({ gridIndex: i, type: "category", data: ["S1", "S2", "S3", "S4", "S5"], show: false })),
        yAxis: clientes.map((c, i) => ({ gridIndex: i, type: "value", min: 0, max: maxGlobal, show: false })),
        title: clientes.map((c, i) => ({ text: `${c.nome} · ${c.var > 0 ? "+" : ""}${c.var}%`, textStyle: { fontSize: 14 } })),
        series: clientes.map((c, i) => ({ type: "line", xAxisIndex: i, yAxisIndex: i, data: c.serie, symbol: "none", lineStyle: { width: 2, color: token("--db-accent") }, emphasis: { focus: "self" } })),
        aria: { enabled: true },
      };

      Critérios de aceite

      Levar para o projeto

      No Power BI, arraste o campo cliente para o poço de small multiples de um gráfico de linha e deixe os eixos sincronizados como vêm; a grade vai de 2×2 a 6×6, e a ordem das células segue a ordenação do visual, que você muda para a medida de variação. Em React, a função matriz vira um componente que recebe clientes, escala, ordem e destaque por props e devolve a grade; a lista usa key pelo nome do cliente, nunca pelo índice, para a célula manter a identidade quando a ordem muda, como o guia de React cobra. No Observable Plot, facet com fx e fy produz a mesma matriz em uma chamada e já compartilha a escala. A seção Empilhar, sobrepor, matriz traz as regras de composição que esta oficina pratica.

      Peça o painel ao agente de código um elemento por vez, com aceite e armadilha escritos

      Otávio, desenvolvedor da agência Mirante (exemplo didático, agência fictícia), pediu ao agente de código "um dashboard moderno com os indicadores da operação" e recebeu 23 gráficos, dois velocímetros, uma pizza de nove fatias e um eixo duplo, tudo em cores que trocavam a cada filtro. O pedido descreveu um visual e nunca uma decisão. Os cinco prompts desta seção seguem o molde de toda a coleção: quem usa, qual tarefa, que arquivos reutilizar, aceite numerado, uma linha "Proibido" e uma armadilha com nome, um elemento por pedido e a estrutura antes do visual. Cada prompt pode ser colado como está no Claude Code, no Codex ou no Cursor, com os nomes da Mirante trocados pelos do seu projeto.

      O ciclo de seis passos e os três portões

      O ciclo é o mesmo do guia de gráficos, aplicado ao painel inteiro. Primeiro, o mapa em texto: a frase de decisão, a lista de indicadores com fonte, dono e denominador, e a manchete calculada, tudo antes de qualquer linha de código. Segundo, os tokens no repositório: cores, tipografia e espaçamento num arquivo que o agente lê (o DESIGN.md ou o @theme do guia de Tailwind), para que nenhum gráfico nasça com hexadecimal solto. Terceiro, um elemento por pedido: a linha de indicadores, depois a matriz, depois o carrossel, nunca "o painel". Quarto, revisão medida em 1280 e 390 px, com o detector da skill impeccable, o axe e o teclado, corrigindo só contraste, tamanho de texto e foco. Quinto, um teste que fixa o contrato (a manchete do churn, a escala comum da matriz, a URL do filtro). Sexto, medir depois de publicar: quanto tempo a gestora leva até a primeira ação, e quantas vezes abre o detalhe.

      Todo gráfico gerado por agente passa por três portões antes de entrar no painel. Executa: a option ou o SVG renderiza sem erro no navegador, nos dois temas. É fiel à base: total, mínimo, máximo e número de linhas batem com a tabela de origem, e toda lacuna continua lacuna, sem virar zero. É legível e acessível: letra de 14 px ou mais no tamanho exibido, contraste de 4,5:1 no texto e 3:1 nas formas, título e descrição no SVG, tabela gêmea e nenhuma informação só por cor. O agente é bom em passar no primeiro portão e ruim nos outros dois; os prompts abaixo cobram os três.

      Cada prompt convoca, pelo nome e quando existirem na máquina da vez, as skills dataviz (forma antes da cor e validador de paleta), design-leadlovers (os guias da coleção e os scripts de gráfico e conferência), icones-leadlovers (ícones Lucide com rótulo), impeccable (detector em duas larguras) e frontend-design (plano de tokens antes do código). Sem elas, o prompt continua válido: as regras estão escritas no próprio pedido, e a skill só encurta o caminho.

      Prompt 1: inventário e manchete em texto, antes de qualquer código

      O primeiro pedido não produz tela. Ele produz a lista que a tela vai obedecer, e a pessoa aprova a lista antes do segundo pedido. Na Mirante, esse passo cortou 23 gráficos para 11 indicadores e descobriu que "conversão" tinha duas definições na agência.

      Pessoa: Helena, diretora de operações da agência Mirante, lê o painel toda segunda-feira
      para decidir quais gestores de conta chamar e sobre o quê.
      Tarefa: produza, EM TEXTO e sem código, o inventário do Painel de operação de outubro de 2026
      e a manchete calculada. Não desenhe nada neste pedido.
      Arquivos a reutilizar: dados/operacao-2026-10.csv (uma linha por cliente e mês, colunas
      leads, custo_midia, reunioes, mrr, cancelamentos, base_ativa, nps, tempo_resposta_h, campanhas_ativas);
      metas/2026-10.json. Quando existirem na máquina da vez, use as skills dataviz (escolha da
      forma antes da cor) e design-leadlovers (regras do guia de gráficos: 9 a 11 indicadores, denominador
      em toda porcentagem, regra dos dois cliques).
      Aceite:
      1. Uma frase de decisão, na primeira linha, com quem decide e o quê.
      2. Tabela com 9 a 11 indicadores: nome, fórmula com denominador, unidade, fonte (arquivo e coluna),
         dono, valor de outubro, valor de setembro, meta e sentido bom (maior ou menor).
      3. Um indicador marcado como número principal, com a justificativa em uma frase.
      4. A manchete calculada: maior desvio ruim contra a meta e maior desvio bom, com unidade certa
         (p.p. entre percentuais, pontos para NPS, % para o resto), e os números que a sustentam.
      5. Lista do que ficou de fora e por quê (sem meta, sem dono, sem denominador, redundante).
      6. Toda lacuna do CSV aparece como "sem informação", nunca como zero.
      Proibido: inventar indicador que não esteja no CSV; arredondar valores da fonte; sugerir tipo de
      gráfico neste pedido; usar as palavras "moderno", "intuitivo" ou "insight".
      Armadilha: "a conversão". Existem duas no CSV (lead para reunião e proposta para fechamento);
      nomeie as duas com o denominador e escolha uma para a primeira tela, dizendo por quê.

      Prompt 2: a primeira tela com 9 a 11 indicadores, bullet e variância

      Com o inventário aprovado, o segundo pedido monta a primeira tela e nada além dela. A oficina de primeira tela é o lugar para testar o resultado antes de aprovar.

      Pessoa: Helena, diretora de operações da Mirante, no computador (1280 px) e no celular (390 px).
      Tarefa: implemente SÓ a primeira tela do Painel de operação: manchete, linha de indicadores com o
      número principal em destaque, um gráfico de marcador (bullet) para cada indicador com meta e uma
      tabela de variância (realizado, meta, diferença absoluta e relativa) abaixo da dobra.
      Arquivos a reutilizar: inventario/painel-operacao.md (aprovado no pedido anterior; a lista de
      indicadores e a manchete vêm dele e não mudam); estilos/tokens.css (todas as cores por token; o
      arquivo tem --db-c1 a --db-c6, --db-ok, --db-warn, --db-bad, --db-gray); componentes/Indicador.jsx
      se existir. Quando existirem na máquina da vez: frontend-design (plano de tokens e tipografia antes
      do código), dataviz (marcas finas, barra de até 24 px, vão de 2 px, texto nunca na cor da série),
      icones-leadlovers (ícone Lucide com rótulo em todo botão), design-leadlovers (script de conferência
      em 1280 e 390 px).
      Aceite:
      1. Manchete calculada em texto no topo, igual à do inventário, com fonte e data de atualização ao lado.
      2. De 9 a 11 cartões; o principal com valor de 48 px ou mais; os outros com valor, variação contra o
         mês anterior e contra a meta, com sinal, unidade e palavra; sem depender de cor.
      3. Bullet por indicador com meta: barra da medida, marca da meta, faixas em um só matiz; nenhum velocímetro.
      4. Tabela de variância com <caption>, th scope, algarismos tabulares alinhados à direita e CSV com ";".
      5. Zero hexadecimal fora de tokens.css; contraste medido 4,5:1 no texto e 3:1 nas formas nos dois temas.
      6. A 390 px: duas colunas, os três principais primeiro, sem rolagem horizontal da página.
      7. Tudo legível sem JavaScript; movimento só em transform e opacity, até 500 ms, e nenhum com
         prefers-reduced-motion.
      Proibido: velocímetro, pizza, rosca, 3D, eixo duplo, sparkline sem série real, número em todo ponto,
      cor por posição no ranking, biblioteca de gráficos para esta tela (SVG estático basta).
      Armadilha: "o cartão que mais cresceu". O número principal é o indicador ligado à decisão, com meta,
      e não o de maior variação; se o agente escolher pelo brilho do número, recuse.

      Prompt 3: matriz de múltiplos pequenos com ECharts na mesma escala

      A matriz de doze clientes é o segundo elemento. O sistema de coordenadas em matriz do ECharts 6.0 (que a documentação descreve como combinar tipos de gráfico e componentes "como uma tabela") entrega a grade; a escala comum e a tabela gêmea entram pelo aceite, porque a biblioteca não as impõe.

      Pessoa: Helena compara os 12 clientes da Mirante por leads de setembro e outubro para decidir
      quem chamar; Otávio implementa com o ECharts 6.1.0 do vendor próprio do portal (mesma origem).
      Tarefa: implemente SÓ a matriz de múltiplos pequenos: 12 gráficos de barras (set em cinza, out em
      cor), 4 por linha no computador e carrossel no celular (o carrossel é o próximo pedido; aqui basta
      empilhar), com a mesma escala de 0 a 700 em todos, ordenados do pior para o melhor por variação, e a
      tabela gêmea abaixo.
      Arquivos a reutilizar: dados/operacao-2026-10.csv; estilos/tokens.css (cores lidas por
      getComputedStyle antes de montar a option, e relidas ao trocar o tema); componentes/Figura.jsx
      (moldura com título, descrição, tabela gêmea e carregamento adiado por IntersectionObserver).
      Quando existirem na máquina da vez: dataviz (paleta validada com validate_palette nos dois fundos),
      design-leadlovers (guia de gráficos: múltiplos na mesma escala, até 15 categorias, rótulo direto).
      Aceite:
      1. Uma única option com o sistema de coordenadas matrix (6.0) ou grids alinhados, com a escala de
         0 a 700 escrita uma vez por linha e nunca por célula; nenhum eixo livre.
      2. Cada célula com o nome do cliente, os dois valores escritos na ponta das barras e a variação
         com sinal; barras de até 24 px, vão de 2 px entre set e out.
      3. aria: { enabled: true, decal: { show: true } } ligado; o contêiner tem role="img", aria-label
         com a conclusão ("Padaria Horizonte lidera com 612; Loja Marés cai 7%") e aria-describedby
         apontando para a tabela gêmea.
      4. SVG estático da matriz no HTML servido (gerado no build) até o ECharts carregar; a biblioteca
         entra por import() dinâmico só quando a matriz chega à tela.
      5. A tabela gêmea tem 12 linhas, as mesmas colunas e o total 4.612 no rodapé; o total, o mínimo
         (255) e o máximo (612) batem com o CSV.
      6. Tema escuro troca as cores da option sem recarregar; contraste 3:1 das barras nos dois fundos.
      7. Conferido em 1280 e 390 px com o script de conferência; nenhuma letra abaixo de 14 px exibidos.
      Proibido: escala por célula; cor por cliente (12 cores); legenda de 12 itens; tooltip como único
      lugar do valor; carregar o ECharts na primeira tela; jsDelivr ou qualquer CDN (a CSP bloqueia).
      Armadilha: "renderer svg para tudo". Acima de cerca de 1.000 elementos o canvas vence; a matriz
      tem 24 barras, então svg é a escolha certa aqui, e o agente precisa dizer por quê, e não só escolher.

      Prompt 4: carrossel de cartões com scroll-snap, sem biblioteca

      O carrossel é o elemento que leva a matriz ao celular e que pagina os doze clientes um a um. A seção de carrossel explica a mecânica e a oficina confere o resultado; o prompt pede a versão nativa primeiro, e a biblioteca só entra num pedido seguinte, com o peso medido.

      Pessoa: Helena, no celular, passa os 12 cartões de cliente com o polegar; no computador, com as
      setas e com os botões; com leitor de tela, pelos títulos de cada cartão.
      Tarefa: implemente SÓ o carrossel dos cartões de cliente, com HTML e CSS nativos e um JavaScript
      curto, sem biblioteca: rolagem horizontal com scroll-snap, botões "Anterior" e "Próximo",
      marcadores de página com a posição escrita ("3 de 12"), teclado e movimento reduzido.
      Arquivos a reutilizar: componentes/CartaoCliente.jsx (o cartão já existe; o carrossel só o
      envolve); estilos/tokens.css. Quando existirem na máquina da vez: design-leadlovers (guia de
      navegação: alvo de 44 px em painel, z-index até 10, carrossel só CSS sem retaguarda é proibido),
      icones-leadlovers (setas Lucide com o texto "Anterior" e "Próximo" visível ou em aria-label),
      impeccable (detector em 1280 e 390x844 depois de pronto).
      Aceite:
      1. Uma lista <ul> com um <li> por cartão, cada cartão com um título (h3) com o nome do cliente;
         sem JavaScript, a lista rola com o dedo e com a barra de rolagem, e todo cartão continua no DOM.
      2. scroll-snap-type: x mandatory no trilho e scroll-snap-align: start em cada item; um cartão
         por tela a 390 px e três a 1280 px, por @container e não por media query.
      3. Botões "Anterior" e "Próximo" de 44 px, com aria-label e aria-disabled nas pontas; marcadores
         como botões de 24 px com aria-pressed e o texto "3 de 12" em role="status" aria-live="polite".
      4. Setas esquerda e direita trocam de cartão quando o foco está no trilho (tabindex="0",
         role="region", aria-label="Clientes, do pior para o melhor"); Home e End vão às pontas.
      5. O cartão atual é detectado por IntersectionObserver (ou scrollend quando existir), nunca por
         cálculo de posição em setInterval; a posição vai para a URL (?cliente=loja-mares) e Voltar restaura.
      6. Com prefers-reduced-motion ou o interruptor do portal, scroll-behavior: auto e nenhuma
         animação; sem a preferência, a rolagem suave dura no máximo 500 ms.
      7. Melhoria opcional em @supports (selector(::scroll-button(*))): ::scroll-button e
         ::scroll-marker do carrossel CSS (Chrome e Edge 135), com os botões em HTML continuando como
         retaguarda nos outros navegadores.
      Proibido: biblioteca neste pedido; carrossel que avança sozinho; esconder cartões com display:none;
      capturar o gesto de pinça; z-index acima de 10; scroll-behavior: smooth no html.
      Armadilha: "o marcador que some". Doze marcadores de 24 px cabem em 1280 px e não cabem em 390;
      a 390 px os marcadores viram o texto "3 de 12" com os dois botões, e nunca pontos de 6 px.

      Prompt 5: auditoria de um painel existente, com achados por impacto

      O quinto prompt não constrói: ele revisa um painel pronto, da Mirante ou de qualquer cliente, e devolve uma tabela de achados que a equipe prioriza. É o pedido que Camila usa antes de qualquer apresentação ao cliente.

      Pessoa: Camila, designer da Mirante, revisa o painel de um cliente antes da apresentação; a
      leitora final é a diretora do cliente, que vai decidir orçamento com base nele.
      Tarefa: audite a página do painel (URL ou arquivo informado) e devolva UMA tabela de achados,
      ordenada por impacto na decisão, sem corrigir nada neste pedido.
      Arquivos a reutilizar: o HTML ou a option de cada gráfico; a tabela de origem de cada figura;
      estilos/tokens.css. Quando existirem na máquina da vez: impeccable (npx impeccable detect --json em
      1280 e 390x844, separando defeito medido, escolha de estilo e leitura errada do detector), dataviz
      (lista de antipadrões e validate_palette), design-leadlovers (regras do guia de gráficos e o script
      de conferência com axe e movimento reduzido).
      Aceite: a tabela tem as colunas "Figura", "Achado", "Como confirmei", "Impacto na decisão" (alto,
      médio, baixo, com a razão em uma frase) e "Correção proposta", e cobre, para cada figura:
      1. Eixo truncado sem o início escrito; eixo duplo; eixo invertido; tempo irregular.
      2. Pizza ou rosca acima de 3 fatias; velocímetro; 3D; ordem inadequada em barras.
      3. Cor por posição no ranking; mais de 8 cores; série distinguível só por cor; status reutilizado como série.
      4. Número em todo ponto; tooltip como único lugar do valor; figura sem tabela gêmea; sem título,
         descrição e aria no SVG ou no canvas.
      5. Ausência de dado desenhada como zero; total, mínimo, máximo e número de linhas conferidos contra
         a tabela de origem, com o número que não bateu.
      6. Contraste medido no estilo calculado nos dois temas (texto 4,5:1, formas 3:1), letra abaixo de
         14 px no tamanho exibido, alvo abaixo de 24 px, rolagem horizontal da página a 390 px.
      7. Primeira tela: há manchete? há número principal? quantos indicadores (fora de 9 a 11 é achado)?
         fonte, definição, data e dono a dois cliques?
      Proibido: opinião sem medição ("parece poluído"); corrigir o código neste pedido; achado sem a coluna
      "Como confirmei"; omitir figuras que passaram (liste-as numa linha final "sem achados").
      Armadilha: "o detector tem razão". O detector lista defeito medido, escolha de estilo e leitura
      errada na mesma lista; classifique cada item do JSON antes de copiá-lo para a tabela.

      Como cobrar o resultado

      O aceite é cobrado item a item, no navegador, antes de aprovar; um item reprovado volta ao agente com o número do item e o que foi visto, sem novas instruções gerais. A Mirante guarda os cinco prompts no repositório, ao lado do DESIGN.md, e cada mudança no painel começa pelo prompt correspondente, com os nomes atualizados. Quando o agente pede uma biblioteca, a resposta é a árvore de decisão da seção Bibliotecas: nativo primeiro, biblioteca com peso medido e informado no pedido seguinte, e nunca duas bibliotecas de gráfico no mesmo projeto sem justificativa escrita.

      Prepare o painel para publicar e confira com medição, e não com gosto

      O painel da Mirante (agência fictícia) só foi ao ar depois de passar por uma lista que qualquer pessoa do time repete e cujos resultados ficam no PR. A ordem importa: primeiro o que mede (contraste, texto, foco, peso), depois o que se observa (teclado, leitor de tela, celular, impressão), por último o que se decide (densidade, manchete, ordem). Esta seção é a lista, com o número que cada item precisa atingir e a ferramenta que o mede, citada pelo nome e usada quando existir na máquina da vez.

      Medir nas duas larguras

      Observar com teclado, leitor de tela, celular e impressora

      1. Só com Tab e setas: filtro, botões, carrossel, marcadores, figuras com navegação por setas, tabelas em <details>. O foco nunca some atrás de barra fixa e volta ao gatilho ao fechar uma gaveta.
      2. Com o leitor de tela: a manchete é anunciada ao mudar o filtro, cada cartão de indicador lê rótulo, valor, variação e meta, e cada figura tem título e descrição com os extremos. O number-flow fica com aria-hidden e o valor vai por aria-label.
      3. A 390 px: nada de rolagem horizontal na página, cartões empilhados, carrossel com peek, dica abre por toque e fecha com Esc e botão, alvos de 44 px.
      4. Impressão: o carrossel vira grade, as tabelas abrem, o fundo escuro não vai para o papel.
      5. Sem JavaScript e sem movimento: os números, as figuras estáticas e as tabelas estão lá; nada começa invisível.

      Decidir: a lista de triagem do painel

      Doze perguntas que bloqueiam a publicação quando a resposta é não
      PerguntaNúmero ou regraOnde este guia explica
      A primeira tela diz o que mudou, onde e o que fazer?Manchete calculada do maior desvio; teste de 5 segundos com alguém de foraA decisão primeiro
      Há de 9 a 11 indicadores, um deles como número principal?Denominador e meta escritos em cada um; um hero por telaAnatomia da tela
      Cada gráfico responde a uma pergunta com a tarefa perceptiva mais precisa?Posição numa escala comum antes de comprimento, ângulo e áreaPergunta → gráfico
      Barras e áreas partem do zero; linhas cortadas dizem onde começam?Sem eixo duplo; escala indexada ou log quando as magnitudes diferemEscalas e eixos
      Uma cor de acento, cinza de contexto e status reservado?Paleta validada nos dois temas; texto nunca na cor da sérieEstilo e notação
      Múltiplos pequenos na mesma escala e ordenados pela pergunta?Máximo global antes de desenhar; até 12 célulasEmpilhar, sobrepor, matriz
      Filtro e período vivem na URL e uma linha de filtros escopa tudo?Copiar o link reproduz a seleçãoFiltro, detalhe e estado
      O carrossel mostra que há mais, diz onde está e nunca anda sozinho?Peek, "N de M", 44 px, sem autoplayCarrossel de cartões
      O movimento cabe em 800 ms e desaparece com movimento reduzido?Só transform e opacity; estado final no CSS baseMovimento que explica
      Toda figura tem tabela gêmea, título, descrição e teclado?4,5:1 e 3:1 nos dois temas; alvo 24 e 44 pxAcessibilidade
      A 390 px a leitura continua sem rolagem horizontal?Cartões empilhados, matriz em carrossel, 14 px mínimoCelular e toque
      Fonte, definição, data e dono de cada número estão a dois cliques?Rodapé do painel com os quatro itensAnatomia da tela

      O que vai no PR

      O PR do painel da Mirante levou: o endereço de prévia, as duas saídas do detector com a triagem (medido, estilo, leitura errada), o contraste mínimo medido por tema, a soma dos bytes novos e o peso de cada biblioteca, a saída do validador de paleta, a lista de aceite das oficinas marcada e três capturas (1280 claro, 1280 escuro, 390). Sem esses itens, a revisão vira opinião, e opinião não converge.

      Por onde começar hoje, com um painel que já existe

      Pegue o painel mais usado do projeto, o que alguém abre toda segunda-feira, e faça com ele o que a Mirante (exemplo fictício) fez com o seu em três semanas. Nenhum passo exige refazer o painel inteiro; cada um deixa algo medido no lugar de algo suposto.

      1. Escreva a decisão. Uma frase: qual decisão esta tela alimenta e quem decide. Se a frase não sair, o painel está servindo a várias pessoas com perguntas diferentes, e o primeiro trabalho é separá-las em telas.
      2. Faça o teste de cinco segundos. Mostre a primeira tela a alguém que não a conhece, por cinco segundos, e pergunte o que mudou. Anote a resposta ao lado da manchete que você gostaria que tivesse sido dita.
      3. Conte e corte. Quantos indicadores, quantos gráficos, quantas cores, quantas telas de rolagem. Leve a primeira tela a 9 a 11 indicadores, um número principal e três gráficos; o resto vai para o carrossel, a gaveta ou a tabela.
      4. Troque os gráficos errados. Velocímetro vira bullet, pizza vira barra ordenada, eixo duplo vira escala indexada, série colorida por rank vira cor por entidade. A tabela de pergunta → gráfico dá a troca para cada caso.
      5. Meça antes de publicar. Detector nas duas larguras, contraste nos dois temas, paleta validada, peso somado, tabela gêmea em toda figura. Os números vão no PR.
      6. Releia em D+7 e D+14. Pergunte de novo o que mudou, e compare com a manchete. Se a pessoa ainda precisa rolar para decidir, o erro está na densidade, nunca na pessoa.

      As quatro oficinas deste guia seguem essa ordem: primeira tela, escala certa, matriz de múltiplos e carrossel de painéis. Os prompts colocam cada passo na boca do agente de código, com aceite numerado e armadilha nomeada, para que a entrega volte medida.

      Fontes para conferir e aprofundar

      As recomendações combinam documentação oficial (Microsoft Learn, Apache ECharts, W3C), estudos de percepção gráfica com autor e data, os padrões IBCS e os critérios da WCAG 2.2, além das regras já publicadas nos seis guias anteriores desta coleção. As aplicações ao trabalho de agência são propostas deste material, identificadas como exemplos didáticos. Consulta em 3 de novembro de 2026; versões de pacotes lidas no registro do npm na mesma data. Fatos obtidos só em resultado de busca, sem leitura da página primária, estão marcados assim no texto.

      A decisão primeiro. Microsoft Learn: Dashboards for designers in the Power BI service (23 de julho de 2026); Stephen Few: Dashboard Confusion, Intelligent Enterprise, 20 de março de 2004 (citado por fontes secundárias); Stephen Few: Information Dashboard Design (O'Reilly, 2006); Franconeri, Padilla, Shah, Zacks e Hullman: The Science of Visual Data Communication: What Works, Psychological Science in the Public Interest, v. 22, n. 3 (dezembro de 2021); guia de Gráficos desta coleção.

      Anatomia da tela. Microsoft Learn: Tips and tricks for creating reports in Power BI Desktop; NN/g, Pernice: F-Shaped Pattern of Reading on the Web: Misunderstood, But Still Relevant (2017); Franconeri e colegas (2021); W3C: WCAG 2.2; skill dataviz (regras de número herói, algarismos e estados de recarregamento); guia de Tailwind desta coleção.

      Pergunta e gráfico. Cleveland e McGill: Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods, Journal of the American Statistical Association, v. 79, n. 387 (setembro de 1984); Heer e Bostock: Crowdsourcing Graphical Perception, CHI 2010; Franconeri e colegas (2021); Apache ECharts: ECharts 6 Features (30 de julho de 2025); Microsoft Learn: Tips and tricks for creating reports; Rho e colegas: tempo irregular em gráficos de linha, CogSci 2024 (via guia de Gráficos); guia de Gráficos desta coleção.

      Escalas e eixos. Claus O. Wilke: Fundamentals of Data Visualization (O'Reilly; CC BY-NC-ND 4.0), capítulos 3, 16, 17 e 21; Franconeri e colegas (2021); Apache ECharts: releases 6.0.0 (30 de julho de 2025) e 6.1.0 (19 de maio de 2026); Apache ECharts: ECharts 6 Features; Microsoft Learn: Using visual calculations (RUNNINGSUM, MOVINGAVERAGE); Microsoft Learn: Small multiples; Talbot, Lin e Hanrahan: An Extension of Wilkinson's Algorithm for Positioning Tick Labels on Axes, InfoVis 2010 (dado de resultado de busca); MDN: Intl.NumberFormat; IBGE: sinais convencionais nas tabelas (via guia de Gráficos).

      Estilo e notação. IBCS Association: IBCS Standards 2.0 (aprovados em 11 de junho de 2026); IBCS Association: página dos Standards e licença Creative Commons; Masataka Okabe e Kei Ito: Color Universal Design, paleta de 8 cores (2008); Paul Tol: Colour Schemes; Edward Tufte: Beautiful Evidence, capítulo "Sparklines: Intense, Simple, Word-Sized Graphics" (2006); Stephen Few: Information Dashboard Design (O'Reilly, 2006) e a especificação do bullet graph; Heer, Kong e Agrawala: Sizing the Horizon, CHI 2009; W3C: WCAG 2.2, critérios 1.4.1, 1.4.3 e 1.4.11; skill dataviz: validate_palette.js e referências de marcas e paleta (procedimento de seis checagens).

      Empilhar, sobrepor, matriz. Microsoft Learn: Create small multiples in Power BI; Apache ECharts: ECharts 6 Features (sistema de coordenadas em matriz, 30 de julho de 2025); Apache ECharts: releases (6.1.0, 19 de maio de 2026: matrix.x/y.length e triggerEvent); Claus Wilke: Fundamentals of Data Visualization, capítulo 21, Multi-panel figures; MDN: CSS container queries; guia de Gráficos deste portal (eixo duplo proibido, até 4 séries com legenda); guia de Tailwind (vão 16, padding 24, bento por contêiner).

      O que o Power BI ensina. Microsoft Learn: Dashboards for designers in the Power BI service (23 de julho de 2026); Microsoft Learn: Tips and tricks for creating reports in Power BI Desktop; Microsoft Learn: Using visual calculations; Microsoft Learn: Set up drillthrough in Power BI reports; Microsoft Learn: Create report bookmarks in Power BI; Microsoft Learn: Small multiples; Microsoft Learn: Design Power BI reports for accessibility; Microsoft Learn: What's new in the October 2026 Power BI update (31 de outubro de 2026); resumo de setembro de 2026 do Power BI e novo visual de cartão: resultados de busca de 3 de novembro de 2026, sem leitura da página primária.

      Acessibilidade do painel. W3C: WCAG 2.2 (1.1.1, 1.3.3, 1.4.3, 1.4.10, 1.4.11, 1.4.13, 2.1.1, 2.3.3, 2.4.11, 2.5.8, 4.1.2); Frank Elavsky: Chartability (princípios de auditoria); Lundgard e Satyanarayan: Accessible Visualization via Natural Language Descriptions, IEEE VIS 2021 (quatro níveis de conteúdo semântico); Microsoft Learn: acessibilidade para autores de relatório (Alt+Shift+F11, ordem de Tab, alt de 250 caracteres); MDN: forced-colors; Playwright: Accessibility testing com @axe-core/playwright; guia de Ícones deste portal (medição de contraste após a transição de tema).

      Filtro, detalhe e estado. Microsoft Learn: Set up drillthrough in Power BI reports; Microsoft Learn: Create report bookmarks in Power BI; Microsoft Learn: Dashboards for designers in the Power BI service (23/07/2026); Microsoft Learn: Design Power BI reports for accessibility; W3C: Understanding 1.4.13 Content on Hover or Focus (WCAG 2.2); MDN: History.pushState(); MDN: URLSearchParams; MDN: elemento dialog.

      Carrossel de cartões. W3C ARIA Authoring Practices Guide: Carousel Pattern; MDN: CSS scroll snap; MDN: evento scrollend; Chrome for Developers: Carousels with CSS (2025); Web Features Explorer (Baseline, lido em 3 de novembro de 2026); Embla Carousel: documentação; npm: embla-carousel 8.6.0; Swiper: documentação; npm: swiper 14.4.0; MDN: content-visibility.

      Movimento que explica. Web Features Explorer: Scroll-driven animations (Newly Available em 27/10/2026); Web Features Explorer: View transitions (Newly Available em 14/10/2025); MDN: @starting-style; MDN: prefers-reduced-motion; number-flow: documentação; GitHub: barvian/number-flow; Motion: documentação; GSAP: documentação; Apache ECharts: opções de animação; Apache ECharts: Universal Transition (handbook).

      Stack, bibliotecas e skills na prática. Apache ECharts: ECharts 6 Features (handbook); GitHub: releases do apache/echarts (6.1.0 em 19 de maio de 2026); Apache ECharts: acessibilidade e decal (handbook); Observable Plot: documentação; Observable Plot: facets; Vega-Lite: documentação; GitHub: Vega-Lite 6.5.0 (8 de outubro de 2026); D3: documentação; D3: scaleLinear().nice(); GitHub: uPlot; TradingView: Lightweight Charts; Recharts; MUI X Charts; ApexCharts; Tailwind CSS: @theme; Lucide; GitHub: Impeccable (Paul Bakaus); Playwright: accessibility testing com axe; Playwright: page.emulateMedia().

      Versões e licenças. npm: echarts; npm: @observablehq/plot; npm: d3; npm: vega-lite; npm: number-flow; npm: motion; npm: gsap; npm: uplot; npm: lightweight-charts; npm: recharts; npm: @mui/x-charts; npm: apexcharts; npm: tailwindcss; npm: lucide; Apache License 2.0; GSAP: licença gratuita desde a 3.13; MUI X: licenciamento; Highcharts: licença comercial (página de licenças da Highsoft).

      Celular e toque. W3C: WCAG 2.2, critérios 1.4.13 (conteúdo em hover ou foco), 2.5.8 (tamanho do alvo, mínimo) e 1.4.11 (contraste não textual); Microsoft Learn: Tips and tricks for creating reports in Power BI Desktop ("keep it to one screen"); MDN: @container; MDN: content-visibility; NN/g, Pernice e Budiu: Hamburger Menus and Hidden Navigation Hurt UX Metrics (2016); guia de navegação da coleção (390 px, gaveta com Voltar, tabela longa no celular); guia de gráficos da coleção (390 contra 1280 px, letra de 14 px ou mais).

      Oficina: primeira tela. guia de gráficos da coleção (9 a 11 indicadores, denominador explícito, regra dos dois cliques, manchete calculada); Stephen Few, "Dashboard Confusion", Intelligent Enterprise, 20 de março de 2004 (definição de dashboard em uma tela; citação por fontes secundárias); Microsoft Learn: Tips and tricks for creating reports ("make the most important information biggest", "upper corner", 3 ou 4 algarismos); novo visual de cartão do Power BI com reference labels e small multiples (dado de resultado de busca, sem leitura da página primária); Franconeri, Padilla, Shah, Zacks e Hullman: The Science of Visual Data Communication: What Works, Psychological Science in the Public Interest, v. 22, n. 3, 2021 (estatísticas globais em fração de segundo; comparar subconjuntos é lento).

      Oficina: escala certa. Franconeri e colegas (2021) (eixo em zero nem sempre é melhor); Heer, Kong e Agrawala: Sizing the Horizon, CHI 2009 (altura do gráfico e precisão; bandas em camadas em alturas pequenas); Cleveland e McGill: Graphical Perception, JASA, 1984 (posição numa escala comum como a tarefa mais precisa); Claus Wilke: Fundamentals of Data Visualization, capítulos 3 (eixos não lineares), 17 (tinta proporcional e eixo logarítmico) e 21 (múltiplos pequenos); Apache ECharts: ECharts 6 Features e releases do ECharts (6.1.0: dataMin e dataMax, log ignora valores não positivos); Microsoft Learn: Small multiples (eixos sincronizados por linha e coluna); Observable Plot: Scales; guia de gráficos da coleção (barra e área partem do zero; linha pode cortar com o início escrito; eixo duplo proibido).

      Prompts para o agente. guia de gráficos da coleção (molde de prompt com pessoa, tarefa, arquivos, aceite, proibido e armadilha; três portões para gráfico gerado por IA; ciclo de seis passos); guia de React da coleção (roteiro de vibecoding e prova de aceite com Playwright e axe); guia de Tailwind e tokens (DESIGN.md e @theme); Apache ECharts: ECharts 6 Features (sistema de coordenadas em matriz); Chrome for Developers: Carousels with CSS (::scroll-button e ::scroll-marker no Chrome e Edge 135; Firefox e Safari sem suporte, dado de resultado de busca); MDN: scroll-snap-type; W3C: WCAG 2.2; Microsoft Learn: Design Power BI reports for accessibility ("Don't use tooltips to convey important information").

      Voltar aos materiais de Design