Mapa dos 5570 municípios brasileiros que mostra, ao lado do resultado, como as cores foram divididas.
O mesmo indicador, os mesmos números, cinco divisões diferentes — e a mancha do mapa muda em cada uma:
Garantias · Como rodar · Alternativas consideradas
Um mapa colorido por faixas não mostra dados: mostra dados agrupados por alguém. A escolha de onde cortar as faixas muda a conclusão mais do que qualquer outra decisão do mapa, e quase nenhum mapa publicado revela qual escolha foi feita. Malha faz o oposto — desenha o mesmo indicador pelos cinco métodos de classificação ao mesmo tempo, com as métricas de cada um à vista, para que a divergência entre eles seja visível em vez de teórica.
Dois exemplos do próprio aplicativo. Com o PIB municipal em valor absoluto e cinco classes por quebras naturais, 97,2% dos municípios caem na primeira faixa: o mapa fica quase uniforme e o que resta visível são as capitais — um mapa de onde mora mais gente, apresentado como mapa de economia. São Paulo tem o maior PIB do país e é o 323º em PIB por habitante; as duas afirmações saem do mesmo dado, e um mapa de valor absoluto só conta a primeira.
O documento abre pelas garantias porque a afirmação central do projeto — a classificação por quebras naturais é o ótimo global exato, obtido em O(k·n·log n) em vez de O(k·n²) — é fácil de escrever e difícil de verificar. A implementação rápida poderia estar devolvendo um ótimo local sem que nada aparecesse no mapa. Por isso a versão quadrática permanece no repositório sem servir à aplicação: ela existe para provar que a rápida está certa.
Cada linha é uma propriedade que o sistema mantém, com o comando que a verifica. Os comandos rodam
após npm install.
| garantia | prova |
|---|---|
| Jenks devolve o ótimo global exato, conferido contra enumeração exaustiva de todas as partições contíguas | npx vitest run src/lib/classification/jenks.test.ts |
| A versão log-linear produz exatamente o mesmo custo da quadrática de referência | npx vitest run src/lib/classification/jenks.test.ts |
| Somas prefixas deslocadas pela mediana concordam com o cálculo em duas passadas | npx vitest run src/lib/classification/jenks.test.ts |
| A média dos I locais é igual ao I de Moran global | npx vitest run src/lib/spatial/moran.test.ts |
| Reaplicar a correção reproduz o cálculo feito desde o início, nas três correções e três níveis de alfa | npx vitest run src/lib/spatial/moran.test.ts |
| A contiguidade extraída da topologia é simétrica | npx vitest run src/lib/spatial/moran.test.ts |
| Classes vizinhas nunca ficam abaixo do limiar de distinção, em todo k e nos dois temas | npx vitest run src/lib/theme.test.ts |
| Toda tinta alcança 4,5:1 de contraste sobre toda superfície, nos dois temas | npx vitest run src/lib/tokens.test.ts |
| O estado da URL sobrevive à ida e volta, e entrada inválida cai no padrão | npx vitest run src/lib/url-state.test.ts |
| Área medida da malha e somas municipais conferem com os valores publicados pelo IBGE | /validacao, no navegador, sobre o dado em produção |
A última linha não é um teste gravado: /validacao reexecuta as
provas no navegador de quem abre a página, sobre o dado que está em produção naquele momento, com os
números à vista.
npm install
npm run devA malha territorial e a última captura íntegra do dado são artefatos versionados — a aplicação sobe sem depender do IBGE estar no ar. Os dois scripts que os regeneram existem para quando houver atualização territorial ou novo período publicado:
npm run geodata # baixa e processa a malha municipal
npm run snapshot # captura o dado atual como reservaVerificação:
npm test # 151 testes em 10 arquivos
npm run coverage # cobertura sobre src/lib
npm run bench # medição sobre os 5570 municípios
npm run typecheck
npm run lint
npm run buildOs mesmos comandos rodam a cada push e pull request em
.github/workflows/ci.yml.
O caminho do dado tem três trechos independentes. Um script local baixa a malha do IBGE, simplifica a topologia e grava os artefatos no repositório. Em execução, o navegador pede a série a uma rota própria, que consulta o SIDRA com cache de borda e degrada para a captura versionada quando a fonte falha. Todo o cálculo — classificação, permutação, métricas — acontece num Web Worker no navegador, sem passar pelo servidor.
flowchart LR
subgraph offline["Preparação, fora do ciclo de request"]
ibge_malha[("API de malhas<br/>do IBGE")] --> geodata["scripts/build-geodata.ts<br/>simplifica topologia"]
geodata --> artefatos[/"public/geo/<br/>malha · pesos · atlas"/]
ibge_sidra[("SIDRA")] --> snapshot["scripts/build-snapshot.ts"]
snapshot --> reserva[/"data/snapshot/<br/>última captura íntegra"/]
end
subgraph servidor["Servidor — Vercel"]
rota["/api/serie/[key]<br/>lista de permissão<br/>cache de borda 6 h"]
saude["/api/saude"]
fonte["/api/fonte/[key]<br/>metadados e calendário"]
end
subgraph navegador["Navegador"]
ui["Interface<br/>mapa · legenda · painéis"]
worker["Web Worker<br/>Jenks · Moran · verificação"]
end
ui -->|"pede a série"| rota
rota -->|"consulta"| ibge_sidra
rota -.->|"fonte fora do ar"| reserva
rota -->|"valores + procedência"| ui
artefatos --> ui
ui <-->|"valores / classificação"| worker
ui --> saude
ui --> fonte
Classificação: Jenks exato em O(k·n·log n). Quebras naturais são o particionamento de dados univariados em k grupos contíguos que minimiza a soma de desvios quadrados intragrupo, com estrutura de programação dinâmica:
D[q][i] = min sobre j de ( D[q-1][j-1] + deviance(j, i) )
Avaliada diretamente, a recorrência custa O(k·n²) — cerca de 155 milhões de avaliações para 5570
municípios. A função de custo satisfaz a desigualdade quadrangular de Monge, o que torna o argmin
monótono em i e permite resolver cada linha por divisão e conquista: decide-se o i do meio, e o
j ótimo encontrado limita a busca das duas metades. O custo cai para O(k·n·log n), cerca de 500 mil
avaliações — a mesma redução do Ckmeans.1d.dp de Wang e Song (2011). Continua sendo o ótimo global
exato: nenhuma heurística, nenhum chute inicial, nenhuma dependência de semente.
Estabilidade numérica. A soma de desvios quadrados de qualquer intervalo sai em O(1) por somas
prefixas, o que o DP executa milhões de vezes. A forma direta sum(x²) - sum(x)²/m sofre
cancelamento catastrófico nas magnitudes deste dado: o PIB municipal chega a 1e9, o quadrado a 1e18 e
a soma de 5570 termos a 1e21 — muito além dos ~9e15 que um double representa como inteiro exato. As
somas são deslocadas pela mediana; como a soma de desvios quadrados é invariante a translação, o
resultado exato não muda, mas as magnitudes intermediárias caem para uma faixa onde o double é
preciso.
Autocorrelação espacial. O I de Moran local de Anselin (1995) responde por unidade a pergunta que o coroplético não responde: onde o valor destoa da vizinhança. A significância vem de permutação condicional — fixa-se o valor da unidade e sorteiam-se os dos vizinhos entre as demais —, sem supor normalidade. A vizinhança é contiguidade real extraída da topologia: arcos compartilhados em TopoJSON são literalmente a mesma linha de divisa. A correção por múltiplas comparações não é opcional: sem ela, 5570 testes a 5% produzem cerca de 278 unidades "significativas" apenas por sorteio.
Cálculo no cliente. Classificação e permutação rodam em Web Worker. O dado já está na máquina de quem abre e não precisa de ida e volta; e 999 permutações sobre 5570 municípios em função serverless seriam cobradas a cada interação — sendo que a interação é o produto. As únicas funções de servidor são o proxy do IBGE com cache de borda e a verificação de saúde.
Cor gerada, não escolhida. A escala sequencial é gerada em OKLCH porque o número de classes é do usuário: uma paleta de passos fixos calibrada para sete classes fica com degraus quase iguais quando se pedem quatro. O teto de sete classes é consequência medida — a separação mínima entre classes vizinhas é 0,06 de luminosidade, e a faixa utilizável de cada tema comporta sete degraus acima desse limiar. As duas escalas, clara e escura, foram geradas e verificadas de forma independente contra a superfície em que cada uma é desenhada.
Stack. Next.js 16 (App Router), React 19, TypeScript 6, Tailwind CSS 4, MapLibre GL 6, Zod 4, Vitest 4. Sem mapa de fundo: nenhuma requisição a servidor de tile, nenhuma chave de terceiro, política de conteúdo restrita à origem própria.
Por que não usar uma biblioteca de classificação pronta? As implementações difundidas de quebras
naturais em JavaScript são heurísticas iterativas que dependem de inicialização e podem parar num
ótimo local. Como a afirmação do projeto é que a classificação é ótima, uma dependência que não
garante isso invalidaria a afirmação — e a alternativa exata publicada, Ckmeans.1d.dp, é o
algoritmo aqui implementado com o oráculo quadrático ao lado para prová-lo.
Por que não k vizinhos mais próximos entre centroides? É o caminho comum para montar vizinhança e erra em município comprido, em município encravado e em toda a faixa litorânea. A contiguidade sai da topologia da malha, onde a divisa entre dois municípios é um único arco compartilhado, sem tolerância a calibrar.
Por que não calcular no servidor? Ver Cálculo no cliente: o custo recorrente de permutação em função serverless recai exatamente sobre a interação que o aplicativo existe para permitir.
Por que não simplificar os polígonos, e sim a topologia? Simplificar cada polígono isoladamente abre fendas e sobreposições ao longo de todas as divisas do país. Sobre a topologia, remover um vértice o remove dos dois lados ao mesmo tempo. A quantização vem depois da simplificação — arredondar as coordenadas antes destruiria as áreas de triângulo que decidem quais vértices ficam.
Por que Bonferroni fica disponível se é inviável aqui? Sobre 5570 unidades a α=0,05 o limiar exigido é p ≤ 9e-6, e 999 permutações não produzem p abaixo de 1e-3. O mapa sairia vazio por falta de resolução do teste, não por ausência de padrão. A opção permanece porque o contraste entre os três critérios é a informação; o que o aplicativo não faz é apresentar o mapa vazio sem dizer a causa.
Por que Mercator, num projeto sobre honestidade cartográfica? Mercator distorce área. A escolha se sustenta porque o coroplético codifica valor em cor, não em área — nenhuma leitura depende do tamanho aparente do município — e porque o território brasileiro fica entre 34° sul e 5° norte, faixa de distorção modesta. A limitação está declarada abaixo em vez de omitida.
Por que um title nativo não serve para explicar os termos? Não aparece para quem navega por
teclado, não existe em toque, demora cerca de um segundo e some antes de ser lido. As definições do
glossário abrem no ponteiro e no foco, sobrevivem ao ponteiro entrando nelas e fecham com Escape.
npm run benchMedido em Intel Core i7-6700 3,40 GHz (8 threads), 16 GiB, Windows 10, Node v24.18.0, sobre o dado real dos 5570 municípios — PIB por habitante, lido da reserva versionada para que o resultado não dependa da rede. Mediana de repetições, porque a primeira execução de cada trecho carrega compilação do motor.
| operação | mediana |
|---|---|
| Jenks por divisão e conquista, k=5 | 2,7 ms |
| Jenks quadrático, o oráculo, k=5 | 354,8 ms |
| I de Moran local, 999 permutações | 829,7 ms |
| reaplicar a correção sobre os mesmos p | 1,7 ms |
Os valores são de uma execução; entre execuções a razão entre as duas implementações de Jenks fica em torno de 130× a 145×. O que não varia é o custo da partição: as duas chegam ao mesmo número, e o script aborta se divergirem — velocidade sem a mesma resposta não mede nada.
Os dois últimos são a razão de o corte de significância ter saído de dentro da permutação. Alfa e correção não mudam um único pseudo p-valor, mudam apenas onde se corta, e ainda assim cada troca refazia milhões de sorteios para chegar aos mesmos p. A thread de cálculo guarda a última análise: quando valores, semente e número de permutações coincidem, só o corte é refeito. Medido no navegador, a troca de correção caiu de 923 ms – 2,3 s para 10 – 12 ms.
A prova de otimalidade em /validacao compara as duas implementações sobre os 5570 municípios e
reporta diferença exata de custo zero. Os tempos variam entre execuções; a diferença de custo não: é
zero por construção, e a prova falha se deixar de ser.
151 testes em 10 arquivos, executados por npm test.
| camada | o que cobre |
|---|---|
| classificação | otimalidade contra enumeração exaustiva, equivalência entre as duas implementações, monotonicidade do custo em k, casos degenerados (menos valores que classes, valores repetidos, variação mínima em magnitude grande) |
| momentos | somas prefixas contra cálculo em duas passadas, ausência de desvio negativo |
| espacial | identidade entre I global e média dos locais, aproximação de −1/(n−1) sob independência, sinal em gradiente e em tabuleiro alternado, simetria da contiguidade, equivalência do corte reaplicado |
| geometria | área geodésica e centroides contra valores oficiais |
| IBGE | contrato da resposta, marcadores tipográficos do SIDRA, auditoria do cruzamento, saneamento da chave ecoada em erro |
| cor e tema | separação entre classes vizinhas em todo k, direção da escala em cada tema, contraste de toda tinta sobre toda superfície, paridade entre tokens CSS e paleta JavaScript |
| estado | ida e volta da URL, entrada inválida caindo no padrão |
| glossário | toda entrada com definição, sem repetir o próprio termo, dentro do tamanho do balão |
Cobertura por npm run coverage, medida sobre src/lib: 52,8% das instruções. O número global é
menos informativo que sua distribuição. O núcleo que sustenta as afirmações do projeto está entre 90%
e 100% — jenks.ts 98,7%, moran.ts 97,0%, weights.ts 97,6%, measure.ts 98,6%, join.ts 98,4%,
schema.ts, url-state.ts e theme.ts em 100%.
O que fica descoberto é adaptação a I/O e a desenho: cliente HTTP do IBGE, leitura de metadados,
carga dos artefatos geográficos, projeção para canvas, composição do PNG e formatação. Esses caminhos
dependem de rede ou de DOM, que a suíte em ambiente Node não tem — e são exercitados no navegador
pela página /validacao, sobre o dado em produção.
Projeção de Mercator distorce área. Justificativa em Alternativas consideradas; a distorção não afeta a leitura porque o valor está na cor.
Duas ilhas ficam fora dos testes de contiguidade. Fernando de Noronha e Ilhabela não confinam com nenhum município, não têm vizinhança e não entram em nenhum teste espacial. A exclusão é da geografia, não do método, e o painel de saúde a declara.
O poder do teste de permutação depende do grau de vizinhança. Com poucos vizinhos e dado próximo de binário, o menor p alcançável já fica acima de 5% e nenhuma correção detecta nada. A suíte documenta esse limite verificando o pseudo p-valor contra a probabilidade hipergeométrica exata.
Nenhuma das pesquisas exibidas tem divulgação futura agendada. O calendário do IBGE cobre pesquisas conjunturais; em pesquisa estrutural anual a data entra poucas semanas antes. O painel mostra a última divulgação ocorrida e diz que não há próxima agendada, em vez de estimar uma.
Escopo deliberadamente estreito. Seis indicadores, um recorte territorial, sem série temporal e sem setor censitário. Acrescentar um indicador exige período verificado, valor nacional publicado para conferência e decisão explícita de aditividade; sem os três, a lista cresceria e o projeto pioraria.
Não há limite de taxa nas rotas. A lista de permissão e o cache de borda contêm o uso normal — existem sete agregados consultáveis, servidos do CDN —, e uma chave inválida custa 15 ms sem consulta externa. Conter abuso por volume é trabalho de firewall, não do código da rota.
Todos do IBGE, dados abertos:
| indicador | fonte | período |
|---|---|---|
| Produto interno bruto | PIB dos Municípios (tabela 5938) | 2023 |
| População residente | Censo Demográfico (tabela 4714) | 2022 |
| Nascidos vivos | Estatísticas do Registro Civil (tabela 2612) | 2024 |
| Pessoal ocupado assalariado | Cadastro Central de Empresas (tabela 1685) | 2021 |
| Índice de envelhecimento | Censo Demográfico (tabela 9515) | 2022 |
| Idade mediana | Censo Demográfico (tabela 9515) | 2022 |
| Área territorial | Censo Demográfico (tabela 4714) | 2022 |
| Malha municipal | API de malhas territoriais | 2022 |
Cada indicador declara como se agrega, e a auditoria muda com essa declaração: uma contagem soma, e
a soma municipal tem de reproduzir o total nacional publicado; um índice ou uma mediana não somam, e
a conferência passa a ser o valor nacional cair dentro da faixa municipal observada. A distinção não
pode ser deduzida dos metadados — o campo sumarizacao da API declara lista vazia para a população
do Censo 2022, que ainda assim soma exatamente os 203.080.756 publicados.
Copyright (c) 2026 Igor Bahia. Todos os direitos reservados. Ver LICENSE.
O código é público para leitura e avaliação; nenhum uso, cópia, modificação ou distribuição é autorizado sem permissão prévia por escrito. Os dados exibidos são do IBGE e seguem os termos da própria fonte.


