16 KiB
Mapa da Bahia na campanha — geometrias estáticas + Leaflet
Status: Fase 1 entregue (B2); Fase 2 (B3 Leaflet) em implementação Atualizado em: 2026-07-19 Item do roadmap: docs/roadmap.md — B2 (Fase 1) / B3 (Fase 2) / B4 (camada de zonas); lazy load e DRY de scripts → B5 Responsável: —
Contexto
O roadmap (trilha B) prevê o mapa da Bahia na vertical /campanha: preparar geometrias estáticas (municípios + Territórios de Identidade) e usá-las para visualizar agregados da campanha. A âncora /mapa do design-ux permanece adiada.
O electoralNucleus indexa território por texto (regions/cities/neighborhoods/tseZones — arrays hasMany desde A1) e as agregações saem de queries Payload por essas chaves. Não há query espacial. A decisão de produto (2026-07-17) é adotar geometrias estáticas versionadas no repo (mesmo padrão de src/lib/bahiaTerritories.ts), sem PostGIS, e integrar Leaflet nas superfícies já existentes.
Decisões travadas (confirmadas com o usuário)
- GeoJSON/TopoJSON estático versionado no repo, sem PostGIS. Mesmo padrão de
src/lib/bahiaTerritories.ts(dado versionado com proveniência + fixture + teste int). - v1 cobre municípios (IBGE) + Territórios de Identidade. Zonas TSE como camada ficam para B4 (dependem de A2 ✓ + B3).
- Territórios de Identidade por dissolução IBGE (decisão 2026-07-18): polígonos dos TIs saem de
topojson.mergedos municípios IBGE conforme a composição oficial embahiaIdentityTerritoryRecords— não do shapefile IDE Bahia. A IDE Bahia fica como referência de validação na proveniência. Garante coerência mapa ↔ composição do app e antecipa a abordagem A do B4. - Extensão
*.topo.json(não.topojson) para import JSON nativo no Next/TS sem config extra. /mapadedicada do design-ux permanece adiada. v1 (B3) incorpora o mapa em superfícies já existentes: overview da lista de núcleos, detalhe do núcleo e dashboard/campanha.- Zona segue como granularidade eleitoral de referência do núcleo — não mapear núcleo a seções (ver "Granularidade seção vs. zona").
- Sem migration, sem collection, sem Consent, sem server action nova. Só dado estático + componentes cliente + agregações por chave (
codarea/ código TI) vianucleusChoropleth.ts. - Alocação multi-município (B3): estimativa confirmada dividida igualmente entre
cities[]; contagem de núcleos +1 por cidade coberta; baseline 2022 por voto TSE por cidade (sem split, sem dupla contagem na união do conjunto filtrado). - Métricas v1 do coroplético: nº de núcleos, estimativa confirmada, votos Solla 2022 (quando seed A3 existir). Tendência E2 e prioridade E1 ficam fora desta entrega.
- Tiles: OpenStreetMap raster com atribuição; altura ~280–360px; tokens
data-theme='campaign'.
Por que estático e não PostGIS
O electoralNucleus já indexa território por texto e as agregações saem de queries Payload por essas chaves — não há query espacial (point-in-polygon, raio, geocodificação) no horizonte curto. PostGIS adicionaria imagem Docker PostGIS local, extensão no Neon, colunas geometry + migrations e um endpoint de serving, sem benefício imediato. PostGIS só volta à mesa se surgir query espacial real (ex.: geocodificar endereços de apoiadores, busca por raio). Movido para "Fora de escopo por enquanto" no roadmap.
Fontes oficiais (pesquisa)
- Municípios (BA): IBGE Malhas API v3 (
qualidade=intermediaria) + Localidades API v1 paraid/nome.codarea= código IBGE de 7 dígitos. CRS SIRGAS 2000 (EPSG:4674) ≈ WGS84 para web. - Territórios de Identidade: composição oficial em
bahiaTerritories.ts(SECULT/SEPLAN); polígonos = dissolução dos municípios IBGE. IDE Bahia / SEI (referência de validação):https://metadados.ide.ba.gov.br/geonetwork/srv/api/records/90b140bf-17df-496f-b048-5783fdf02864. - Zonas TSE: o TSE não publica polígonos — só o cadastro tabular. Polígonos só por construção derivada. Fica fora da v1 (B4).
Granularidade seção vs. zona
Hierarquia TSE: município → (1+) zona → (muitas) seção; a relação município↔zona é muitos-para-muitos. Decisão (2026-07-17): manter zona como granularidade eleitoral de referência do núcleo, não seções, porque:
- O núcleo é unidade operacional da campanha, não eleitoral — o coordenador pensa em bairro/município/território.
- O cruzamento com a baseline TSE 2022 (
baseline-eleitoral-tse) é por zona. - Não existe mapeamento oficial seção↔bairro — não dá para auto-derivar as seções do bairro do núcleo; seria manual e frágil, e números de seção mudam entre eleições.
- Caso sub-zona (núcleo menor que a zona) é o único em que seção traria precisão; hoje a baseline da zona sobre-cobre o núcleo (aproximação aceitável, documentada como limitação).
Modelagem de dados
- Arquivos de geometria (TopoJSON) commitados no repo:
src/lib/geometries/bahia-municipalities.topo.json— uma Feature por município,properties: { codarea, name }(name = canônico do app).src/lib/geometries/bahia-identity-territories.topo.json— uma Feature por território,properties: { code, name }(code= "01".."27").
- Join por chave estável:
- Municípios: o núcleo guarda
cities[](array de nomes, desde A1). Tabelasrc/lib/bahiaMunicipalityCodes.ts(nome canônico → código IBGE de 7 dígitos) + fixturetests/fixtures/bahia-municipality-codes.official.json+ testetests/int/bahiaMunicipalityCodes.int.spec.ts. Reconciliação IBGE→canônico viacanonicalizeMunicipalityName(src/lib/electionResults.ts). - Territórios: join por
code(01–27) já presente embahiaIdentityTerritoryRecords; sem tabela extra.
- Municípios: o núcleo guarda
- Helpers:
src/lib/bahiaGeometries.ts—getMunicipalityFeature(codarea),getTerritoryFeature(code), arraysbahiaMunicipalityFeatures/bahiaTerritoryFeatures, topologias tipadas (bahiaMunicipalitiesTopology/bahiaIdentityTerritoriesTopology; objectsmunicipalities/territories). Framework-free para B3. Lookups de código:codeForMunicipality/municipalityForCodeembahiaMunicipalityCodes.ts. - Proveniência e versionamento: cabeçalho em
bahiaMunicipalityCodes.ts+ script (URL + SHA-256 dos downloads IBGE), no estilo debahiaTerritories.ts/bahiaTseZones.ts.
Script de geração (one-off, re-executável)
scripts/build-bahia-geometries.mjs(pnpm build:geometries): baixa IBGE Malhas + Localidades (viadownloadToBufferdeelectionResultsZip.ts), reconcilia nomes comcanonicalizeMunicipalityName(sóUnknownMunicipalityError), constrói topologia comtopojson-server+topojson-simplify(SIMPLIFY_QUANTILE = 0.35) +quantize(1e4), dissolve territórios comtopojson-clientmerge, emite os*.topo.json,bahiaMunicipalityCodes.tse o fixture. Cache emdata/geometries/(gitignored;GEOMETRIES_CACHE_DIR). Não toca banco — semassertLocalDatabase. Não roda em build/dev — só quando se atualiza a versão de origem.
Leaflet na campanha (B3 — em implementação)
- Dependência:
leaflet(+topojson-clientjá instalado na Fase 1).react-leafleté opcional; preferir wrapper cliente fino comleafletdireto edynamic(() => import(...), { ssr: false })para evitar SSR. - Lazy load (B5 F1): não importar
bahiaGeometriesde forma eager no path default de/campanha. Preferir split município/TI +import()dinâmico no mount do mapa — ver escala-dry-pos-b2.md. Se B3 aplicar isso no mesmo PR, fecha B5 Fase 1. - Base tiles: OpenStreetMap raster (ou CartoDB Positron) com atribuição obrigatória; ou fundo neutro sem tiles (só coroplético) como opção mais leve. Decidir na implementação; default OSM.
- Componentes novos (todos
src/components/campaign/, PascalCase):BahiaMap.tsx— cliente, recebegeometry(TopoJSON) +values: Record<string, number>(chave→métrica) +mode: 'municipality' | 'territory'+colorScale; renderiza coroplético Leaflet.NucleusOverviewMap.tsx— mapa no overview de/campanha/nucleos, escopado pelos mesmos filtros da lista, com toggle município/território e escolha da métrica (estimativa confirmada, nº de núcleos, baseline 2022 Solla quando disponível).NucleusDetailMap.tsx— mapa pequeno no detalhe do núcleo destacando seus municípios/território (+ marcadores ponto das zonas TSE, quando a camada de zonas existir).DashboardMap.tsx— coroplético por território no dashboard/campanha.
- Agregação: servidor continua a fonte.
nucleusChoropleth.tsexpõebuildNucleusChoroplethBundle(métricas porcodareae código TI);loadNucleusListOverviewDataegetCampaignDashboardPageData(geral) anexam o bundle ao view model. O cliente junta com a geometria lazy-loaded. Sem query espacial. - Soft deps entregues (2026-07-19): A4 baseline no produto, E1 metas/prioridade, E2 tendência — o coroplético v1 usa baseline 2022; tendência/prioridade ficam para ciclo seguinte.
Phasing
- Fase 1 — Fundação de geometrias (B2) ✓ entregue 2026-07-18:
bahiaMunicipalityCodes.ts+ fixture + teste;*.topo.jsonde municípios e territórios; scriptpnpm build:geometries; helpers embahiaGeometries.ts. - Fase 2 — Leaflet nas superfícies (B3):
BahiaMap+NucleusOverviewMap+NucleusDetailMap+DashboardMap, comnucleusChoropleth. Fecha B5 F1 (lazy geometrias) no mesmo PR.
Sequenciamento: a Fase 1 é independente e paralelizável. A Fase 2 rende mais depois de B1 ✓ (overview); A4 ✓, E1 ✓ e E2 ✓ já ampliam as métricas disponíveis. B5 F1 fecha no mesmo PR que B3.
Entrega as-built (B2, 2026-07-18)
| Artefato | Caminho | Notas |
|---|---|---|
| TopoJSON municípios | src/lib/geometries/bahia-municipalities.topo.json |
~132 KB; 417 features; object municipalities |
| TopoJSON TIs | src/lib/geometries/bahia-identity-territories.topo.json |
~15 KB; 27 features; object territories (dissolução IBGE) |
Tabela nome→codarea |
src/lib/bahiaMunicipalityCodes.ts |
+ codeForMunicipality / municipalityForCode |
| Helpers runtime | src/lib/bahiaGeometries.ts |
topojson-client feature() + Maps no load do módulo |
| Fixture oficial | tests/fixtures/bahia-municipality-codes.official.json |
evidence SHA + bijeção |
| Testes int | tests/int/bahiaMunicipalityCodes.int.spec.ts, tests/int/bahiaGeometries.int.spec.ts |
cobertura 417/27, teto de bytes, nomes de objects via módulo |
| Script | scripts/build-bahia-geometries.mjs |
pnpm build:geometries; deps: topojson-client (runtime), topojson-server / topojson-simplify (dev) |
| Cache | data/geometries/ |
gitignored |
Cleanup /simplify já aplicado no PR da Fase 1: reuso de downloadToBuffer; catch só de UnknownMunicipalityError; remoção de dead code de codegen (stableStringifyTopo enganoso, tipo BahiaMunicipalityCode não usado). Não aplicado (→ B5): lazy load split mun/TI; helper CLI compartilhado ensureCachedDownload.
Ciclo seguinte — Zonas TSE como camada (B4)
Após A2 ✓ + B3. Polígono por dissolução dos municípios membros (abordagem A, default — já validada na Fase 1 para TIs) ou por geocodificação dos endereços das seções → casco (abordagem B, questão em aberto). Ponto representativo por centroide da zona.
- (A) Dissolução dos municípios membros: o TSE publica quais municípios compõem cada zona (
bahiaTseZones); com os polígonos municipais IBGE, basta dissolver. Limpo, alinhado às divisas oficiais. - (B) Geocodificar endereços das seções/locais de votação → casco: milhares de endereços, casco irregular em áreas rurais, pode divergir da composição oficial.
Não escopo
- PostGIS (adiado; só se surgir query espacial real).
/mapadedicada do design-ux (adiada).- Camada de Zonas TSE — polígono ou ponto (B4; depende de A2 ✓ + B3).
- Mapear núcleo a seções eleitorais (zona segue como referência).
- Geocodificação de endereços de apoiadores.
- Outros estados (todo núcleo é BA).
Dependências
- Reusa
bahiaIdentityTerritoryRecords/bahiaMunicipalities(src/lib/bahiaTerritories.ts),canonicalizeMunicipalityName(src/lib/electionResults.ts),CitiesByState.BA(src/lib/cities),campaignDashboardPageData/nucleusViewModels/nucleusListOverviewPageData(agregados para B3), e o padrão de fixture/teste debahiaTerritories. - Camada de Zonas (B4) depende de A2 (
docs/plans/zonas-por-municipio.md) + B3. - Lazy load / DRY CLI (B5): débitos do
/simplifypós-B2 — escala-dry-pos-b2.md. F1 preferencialmente com B3; F2 (ensureCachedDownload) independente.
Revisões
- 2026-07-18 (auditoria pré-B2 + entrega Fase 1): territórios por dissolução IBGE (não shapefile IDE Bahia); extensão
*.topo.json; reuso decanonicalizeMunicipalityName; núcleo usacities[](A1); script sem guard de banco; referências de linha do roadmap antigas removidas; Fase 1 implementada e marcada entregue. - 2026-07-18 (B5 registrado): follow-ups do
/simplifyque não entraram no cleanup (lazy geometrias + cache CLI compartilhado) viraram item B5; B3 deve preferir F1. - 2026-07-19 (auditoria pré-B3): overview/dashboard não tinham
Record<codarea|code, number>— corrigido comnucleusChoropleth.ts; soft deps A4/E1/E2 marcadas entregues; alocação multi-município e métricas v1 documentadas; B5 F1 no mesmo PR.
Referências
docs/roadmap.md(B2 / B3 / B4 / B5 na trilha B)docs/plans/escala-dry-pos-b2.md(follow-ups pós-/simplify)- AGENTS.md — seção "Campaign map geometries (B2)"
src/lib/bahiaTerritories.ts+tests/fixtures/bahia-identity-territories.official.json+tests/int/bahiaTerritories.int.spec.ts(padrão a espelhar)src/lib/bahiaMunicipalityCodes.ts,src/lib/bahiaGeometries.ts,src/lib/geometries/*.topo.jsonsrc/lib/electionResults.ts(canonicalizeMunicipalityName),src/lib/electionResultsZip.ts(downloadToBuffer)scripts/build-bahia-geometries.mjstests/int/bahiaMunicipalityCodes.int.spec.ts,tests/int/bahiaGeometries.int.spec.tssrc/collections/ElectoralNucleus.ts,src/utilities/campaignDashboardPageData.ts,src/utilities/nucleusViewModels.ts,src/utilities/nucleusListOverviewPageData.ts(agregados a reusar no B3)docs/plans/zonas-por-municipio.md(dependência da camada de Zonas)docs/plans/baseline-eleitoral-tse.md(baseline por zona, consumidor do mapa)- AGENTS.md (naming, "Bahia implícita", padrão de dado estático versionado)
- IBGE — API de Malhas: https://servicodados.ibge.gov.br/api/docs/malhas
- IDE Bahia / SEI — Territórios de Identidade 1:100.000 (referência): https://metadados.ide.ba.gov.br/geonetwork/srv/api/records/90b140bf-17df-496f-b048-5783fdf02864