- Compile docs/roadmap.md into a next-steps document (277→206 lines): R0–R5/M1–M5 tables and the delivered map/list item paragraphs compressed into 'Já entregue'; janelas, grafo and cortes list only open IDs; C10/C11/C8 fill-ins keep their original triggers; C5 moved under 'A validar'. - Record the production deploy of the destructive remodel chain (2026-07-23 Vercel builds) across roadmap, AGENTS.md, the project notebook and plans — supersedes every 'deploy pendente'; smoke pós-deploy + onboarding remain the open blockers. - Fix AGENTS.md internal contradictions: Município locked-decisions bullet (435, Camaçari whole, candidate role), campaignUser role enum, deploy-checklist item 7 as historical, access/* structure note, election-aggregates artifact + election-tse revalidate runbook. - Sync open plans to real identifiers (plaza*/nucleus* → municipality*): the intelligence program (master + E8–E15/B13/C12 with artifact/caching context and satisfied deps), B8 rescoped to Salvador ZE 1–19 only, WhatsApp D3–D5, D2, A6 (now the insight layer over stateDeputy), RS+, onda-0 smoke step. - Close items verified done in code: C8 F4 (DRY de forms) complete, filtros-auto shared pending shell, petition generateStaticParams select/depth; ledger the surviving user-visible 'Praça' copy in src as new P2 debt. - Banner delivered praça-era plans (production note + rename pointer); mark remodelagem-pracas.md superseded and remodelagem-municipios.md delivered. - TECH-DEBT smell statuses, TESTING RBAC row, README quality gates, campanha UI rules updated to current component names.
14 KiB
Escala e DRY pós-B3 (Leaflet / coroplético)
Nota (2026-07-24): entregue na era Praças e em produção desde 2026-07-23. A remodelagem M1 renomeou os identificadores
plaza*→municipality*e as rotaspracas/→municipios/; os nomes neste plano são os da época. Nomes atuais: ver remodelagem-municipios.md.
Status: entregue (2026-07-21; em produção desde 2026-07-23)
Atualizado em: 2026-07-21 (B6 implementado + triage capture-review-debts pós-/simplify: gatilhos absorvidos; sem item novo no roadmap)
Item do roadmap: docs/roadmap.md (Trilha B, item B6)
Impeccable: A — N/A (sem superfície UI nova; otimização do renderer Leaflet existente)
Appetite: ~1–1,5 dia eng (Fase 1 única: métrica/ano + hover/select incremental; PR único)
Responsável: —
Contexto
O B3 (mapa-bahia-geometrias.md Fase 2) entrega BahiaMap + embeds (NucleusOverviewMap, NucleusDetailMap, DashboardMap), agregação servidor em nucleusChoropleth.ts / nucleusChoroplethPageData.ts, e lazy load de geometrias (B5 F1 ✓). Duas passagens /simplify no mesmo branch já limparam o que cabia em cleanup: createCampaignClientDynamic, resolveChoroplethNuclei / single-pass bundle, remoção de geographyForCityNames duplicado, toNucleusElectionGeographyInput no dashboard, highlight O(k) via getMunicipalityFeature / getTerritoryFeature, NucleusDetailMap useMemo, alinhamento de tipos com NucleusElectionGeographyInput.
Os revisores (performance) marcaram como importante e maior que simplify os follow-ups abaixo. Sem registro, cada troca de métrica no coroplético (overview/dashboard) refaz decode + L.geoJSON completo em 417 ou 27 polígonos; e, desde B10 (hover-mapa-pracas.md), cada hover/select no mapa de Praças chama applyLayerStyles com eachLayer em ~417 municípios. Desde B11 (escala-percentual-mapa-pracas.md), trocar scaleMode (absolute ↔ % dos válidos) também altera displayValues e dispara o mesmo rebuild — mesmo com scaleMax corrigindo legenda vs fill.
-
BahiaMaprebuild completo do layer GeoJSON quando sóvalues(métrica/escala),scaleMaxouhighlightKeysmudam. O effect emBahiaMap.tsxdepende de[highlightKey, mode, scaleMax, values]e, a cada mudança, remove o layer, recriaL.geoJSONcom todas as features e reaplica estilos — mesmo quemodee o módulo de geometria já estejam carregados. O caminho correto para troca de métrica/escala élayer.setStyle(...)(efitBoundssó quando o highlight muda). -
Hover/select no mapa de Praças (B10) ainda faz full-scan de estilo.
mouseover/selectedKeydisparamrefreshLayerStyles→applyLayerStyles→eachLayer+setStyleem todos os polígonos. O simplify pós-B10 já deduplica hover repetido e evita double-refresh quando hover “possui” o estilo, mas o hot path continua O(n) por movimento do cursor. O caminho correto é restyle incremental (2-path: anterior + atual, ouMap<featureKey, PathLayer>) — mesma refatoração estrutural da Fase 1.
Já resolvido no simplify/critique (não reabrir): B5 F1 lazy split mun/TI + dynamic(..., { ssr: false }); createCampaignClientDynamic; choropleth single-resolve (resolveChoroplethNuclei); DRY geographyForCityNames → resolveNucleusElectionGeography; dashboard toNucleusElectionGeographyInput; ChoroplethMapPanel passa max; detail map useMemo; highlight bounds O(k); type alignment NucleusChoroplethNucleus. Pós-B10 simplify: dedup de hover (styleContextRef.hoveredKey único); merge dos effects de callback-ref; emphasizeFeature compartilhado; skip de refresh quando hover inalterado ou hover possui estilo; formatElectionNumber na zone breakdown de PlazaMapPanel; useMemo de selectedNavigation. B6 entrega: split effects ([mode] mount vs [values, fillMode, scaleMax, highlightKey, mode] restyle); pathByKeyRef + hover/select 2-path; fitBounds só em mode/highlightKey; helpers puros em src/lib/bahiaMapStyle.ts + tests/unit/bahiaMapStyle.unit.spec.ts; PlazaMapPanel.displayMax via computeChoroplethMax. Pós-B6 /simplify: remoção de keyProperty morto em LayerStyleContext; DRY keyPropertyForMode / featureKeyFromProperties; fix selectedKey após mouseout com hover; syncMountStyleContext(); remoção de mode não usado em buildLayerStyleContext.
Explicitamente fora (revisores pediram skip ou triage descartou): colapsar shells finos DashboardMap / NucleusOverviewMap; CAMPAIGN_BRAND_PRIMARY para #c51414; codeForIdentityTerritory em bahiaTerritories.ts; catch vazio no BahiaMap (logging opcional); flake int campaignNucleusListOverview (totalFiltered — isolamento de DB pré-existente); métricas E1/E2 no coroplético v1 (decisão de produto B3); critique Impeccable formal nas três superfícies (polish leve sem P0–P3). Triage capture-review-debts 2026-07-21 pós-B10: extrair DivergingChoroplethLegend (1 consumer — polish opcional em R6); helper scrollIntoView (1 call site em #plaza-zone-breakdown); throttle/defer do setState do readout no hover (trade-off produto; sem evidência de perf); mudar contrato activate/select para remover selectedKeyRef (intencional para 2º tap mobile — gatilho: 3º consumidor do mapa precisar API diferente); migração campanha-wide Intl → formatElectionNumber (B10 zone list já corrigida; gatilho: E6 F4 em núcleos ou 3ª superfície Praça com formatter local). Triage capture-review-debts 2026-07-21 pós-B6: useReducer para contexto de estilo (1 call site — YAGNI); refactor highlightKeys string↔Set sem ganho medido; brand tokens / DivergingChoroplethLegend → R6 (inalterado).
Objetivos
- Trocar métrica (núcleos / estimativa / baseline 2022), escala (
absolute↔% dos válidosno mapa de Praças pós-B11) ou modo município/TI no coroplético não recria o layer GeoJSON inteiro — apenas atualizafillColor/ borda viasetStylequando a geometria já está montada. - Hover/select no mapa de Praças (B10) restyla só o polígono anterior + o ativo — não
eachLayerem ~417 paths pormouseover. fitBoundsroda só quandohighlightKeysoumodemudam, não a cada alteração devalues.- Guardrails: sem migration, sem collection, sem Consent, sem server action. Client-only; manter acessibilidade (
role="img",aria-busy,aria-label,aria-livedo readout B10 inalterado).
Decisões travadas
- Um item B6, uma fase. Mesmo racional de débitos pontuais pós-entrega (VR+, RS+): um ID, PR único.
- Dependência suave de B3. Só faz sentido com
BahiaMapno produto; não reabre escopo de coroplético, tiles ou agregação servidor. - Query duplicada de votos 2022 na lista de núcleos (
loadNucleusListElectionOverview+loadNucleusChoroplethBundle) não entra neste plano — absorvida como A7 Fase 4 em escala-dry-pos-a4.md (mesmo módulonucleusElectoralBaseline.ts). - Factory mun/TI near-duplicate → B5 Fase 3 em escala-dry-pos-b2.md.
- Cortável se o mapa for raramente usado ou se troca de métrica permanecer aceitável em campo; vira não-cortável quando coordenadores alternarem métricas com frequência no overview filtrado ou hover denso no mapa de Praças (B10 entregue).
- Dependência suave de B10. Hover/select já existe em
PlazaMapPanel+BahiaMap; Fase 1 unifica o caminho de estilo incremental — não reabre escopo de navegação/readout do B10. - Dependência suave de B11. Seletor
scaleModeemPlazaMapPaneljá alteradisplayValues/scaleMax; Fase 1 deve cobrir essa troca sem rebuild — não reabre escopo de % válidos nemvalidVotesByYear. - i18n e naming (AGENTS.md): identificadores em inglês; strings visíveis em pt-BR inalteradas.
Questões em aberto
- Split em dois effects ou ref de layer estável? Recomendação: effect 1 monta mapa + tiles + carrega geometria por
mode(deps[mode]); effect 2 aplica estilo quando[values, highlightKey]mudam — selayerRef.currentexiste,setStyle+fitBoundscondicional; senão aguarda effect 1. - Manter
valuescomo objeto novo a cada render do parent? Recomendação: não bloquear Fase 1 —ChoroplethMapPaneljá derivavaluesestáveis por métrica; se profiling mostrar churn, memoizar no parent num follow-up micro (fora de escopo). - Índice
Map<featureKey, layer>vs 2-path hover? Opções: (A) mapa estável no mount do GeoJSON; (B) guardarpreviousHoveredKey/previousSelectedKeye restylar só dois layers. Recomendação: (A) se o refactor já tocaonEachFeature— simplifica métrica + hover + highlight programático; (B) como fallback se o mapa de layers for inviável nos embeds de núcleo.
Abordagem proposta
flowchart TD
B3["B3 Leaflet + coroplético"] --> F1
B5F1["B5 F1 lazy geometrias ✓"] --> F1
B10["B10 Hover/tap Praças ✓"] --> F1
B11["B11 Escala % válidos ✓"] --> F1
F1["Fase 1 — BahiaMap setStyle<br/>(split effects + hover incremental)"]
F1 -.não bloqueia.-> B4["B4 Zonas TSE no mapa"]
Fase 1 — setStyle incremental no BahiaMap
src/components/campaign/BahiaMap.tsx:- Separar mount do mapa/OSM (
useEffectonce) do load de geometria pormode. - Guardar
geometryModuleem ref após primeiro load; recriar layer só quandomodemuda. - Manter
Map<featureKey, PathLayer>(ou equivalente) no mount do GeoJSON para restyle O(1) por chave. - Em updates de
values/highlightKey:setStylepor layer afetado — nãoeachLayerfull-scan salvo fallback de migração. - Hover/select (B10): restylar só polígono anterior + ativo (
hoveredKey,selectedKey); unificar callbackstyleinicial com o caminho deapplyLayerStyles/getFeatureStyle. fitBounds: highlight quandohighlightSetnão vazio; senãoBAHIA_BOUNDS— só quando highlight oumodemudam.
- Separar mount do mapa/OSM (
- Testes: unit leve com mock de
L.geoJSON/ spy emsetStylese viável; senão checklist manual (trocar métrica 3× no overview e hover 10× no mapa de Praças sem flicker perceptível). - Critério: DevTools Performance — troca de métrica não dispara novo
import()de topojson nem aloca 400+ layers; hover não dispara >2setStylepor evento.
Migration: nenhuma.
Dependências
- Suave: B3 Mapa Fase 2 (Leaflet) — implementado no branch; merge pendente.
- Suave: B5 F1 lazy geometrias — entregue com B3.
- Suave: B10 hover/tap no mapa de Praças — entregue 2026-07-21; Fase 1 consolida o hot path de estilo sem alterar navegação/readout.
- Suave: B11 escala % válidos — entregue 2026-07-21; troca
scaleModeé mais um gatilho de rebuild a eliminar viasetStyle. - Reusa:
choroplethFillColor/choroplethMaxValue(src/lib/choroplethColorScale.ts), loadersloadMunicipalityGeometryModule/loadTerritoryGeometryModule, tiposChoroplethValues,emphasizeFeature/getFeatureStyle(B10).
Não escopo
- Query duplicada overview + coroplético na lista → A7 F4 (escala-dry-pos-a4.md).
- Factory TopoJSON mun/TI → B5 F3 (escala-dry-pos-b2.md).
- Camada de zonas TSE, rota
/mapa, métricas tendência/prioridade no coroplético → B3/B4/E1/E2. react-leaflet— manterleafletdireto conforme B3.- Navegação/readout/2º tap do B10 — permanecem em hover-mapa-pracas.md.
Rabbit holes
- Refatorar todos os embeds de uma vez. Pós-remodelagem só
PlazaMapPanelconsomeBahiaMap— validar troca de ano/escala/compare + hover denso. Mitigação: um renderer, índice de layers compartilhado;territorymode mantido na API. - Separar componente só para Praças. Duplicar
BahiaMappara otimizar hover fragmenta o coroplético. Mitigação: um renderer, índice de layers compartilhado.
Adiado com gatilho
- Contrato activate/select sem
selectedKeyRef. Revisitar quando um 3º consumidor do mapa precisar semântica diferente de 2º tap mobile (hoje intencional em B10). formatElectionNumbercampanha-wide. Revisitar via E6 F4 (núcleos) ou quando uma 3ª superfície Praça reintroduzirIntl.NumberFormatlocal.- Remover
restyleAllPaths()pós-mount assíncrono. Hoje necessário para race entre mount do GeoJSON e props iniciais; revisitar se Leaflet expuser callback estável pós-addDataou se profiling mostrar custo irrelevante. - Restyle incremental só em mudança de
highlightKey(semrestyleAllPaths). Revisitar quando highlight programático (ex. deep-link futuro) gerar jank medido em DevTools. - Cache de bounds em
fitMapToHighlights. Revisitar com B12 (viewport) ou sefitBoundsrepetido aparecer no profiling após filtro URL estável.
Referências
docs/roadmap.md(Trilha B, item B6; B3/B5)docs/plans/mapa-bahia-geometrias.md— plano pai B3docs/plans/escala-dry-pos-b2.md— B5 F1/F3docs/plans/escala-dry-pos-a4.md— A7 F4 (query duplicada)src/components/campaign/BahiaMap.tsx— effect que rebuilda layer;applyLayerStyles/ hover B10src/components/campaign/PlazaMapPanel.tsx— consumidor B10/B11 comselectedKey+ readoutsrc/components/campaign/ChoroplethLegend.tsx— legenda do mapa de Praçasdocs/plans/hover-mapa-pracas.md— plano pai B10 (navegação fora de escopo B6)src/lib/bahiaGeometries.ts— lazy loaders- AGENTS.md — naming inglês; B3 sem migration