Merge branch 'cursor/ba211341'

This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-21 18:11:51 -03:00
commit 3adc8eacc3
8 changed files with 119 additions and 31 deletions

View file

@ -7,6 +7,7 @@ alwaysApply: false
## Status
- **B12 entregue em código (2026-07-21; deploy pendente com remodelagem):** `fitToKeys` + `interactiveKeys` no Mapa das Praças (`plazasByIbgeCode`); `canonicalMapKeysKey`; hover stroke-only (fill inalterado); clear síncrono no mouseout; fit ao footprint filtrado sem re-zoom em Ano/Escala. Plano: [`aproximar-mapa-pracas.md`](../../../docs/plans/aproximar-mapa-pracas.md).
- **B6 entregue em código (2026-07-21; deploy pendente com remodelagem):** `BahiaMap` setStyle incremental — layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers puros em `bahiaMapStyle.ts` + unit `bahiaMapStyle.unit.spec.ts`. Plano: [`escala-dry-pos-b3.md`](../../../docs/plans/escala-dry-pos-b3.md). Fecha débitos de perf de **B10** e **B11**.
- **B8 F1 entregue em código (2026-07-21; deploy pendente com remodelagem):** catálogo estático `plazaZoneNeighborhoods` (21 Praças-zona SSA/CMS; Salvador = TRE-BA RA 02/2017; Camaçari = locais de votação + curadoria) + fixture `plaza-zone-neighborhoods.official.json` + int `plazaZoneNeighborhoods.int.spec.ts`; card **Bairros desta Praça** na aba Visão geral de `/campanha/pracas/[slug]` quando `kind === 'zona'` (staff e líder). Plano: [`poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md). **F2 pendente** (dissolve IBGE → TopoJSON + mapa).
- **C8 F4 entregue em código (2026-07-21; deploy pendente com remodelagem):** DRY plaza staff form actions — `plazaStaffFormActions.ts` (votos estimados, tendência, assessores) compartilhado entre lista e `/editar`; `editar/formActions.ts` mantém só `updatePlazaStrategy`. Plano: [`escala-dry-pos-c6.md`](../../../docs/plans/escala-dry-pos-c6.md).

View file

@ -51,6 +51,8 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
**Recently resolved (2026-07-21):** **B11** Escala % dos válidos no Mapa das Praças — `validVotesByYear` in `PlazaMapBundle`, `scaleMode` selector (`absolute` | `percentValid`, default %), `computeValidVoteShares`, readout em % com votos absolutos secundários; `/simplify` fix `scaleMax` (legenda 0–100% alinhada ao fill). Plano: `docs/plans/escala-percentual-mapa-pracas.md`.
**Recently resolved (2026-07-21):** **B12** Aproximar mapa ao footprint filtrado + correção hover — `fitToKeys` / `interactiveKeys` em `BahiaMap` (`plazasByIbgeCode`); `canonicalMapKeysKey`; hover stroke-only; clear síncrono no mouseout; fit só quando footprint muda. Plano: `docs/plans/aproximar-mapa-pracas.md`.
## Posts & Tags (news / publications)
The public news/publications system is backed by two collections (both in the `Publicações` admin group) plus the helpers in `src/utilities/posts.ts`.

View file

@ -1,12 +1,14 @@
# Aproximar o Mapa das Praças à região filtrada
Status: rascunho
Status: entregue 2026-07-21 (branch; deploy pendente com remodelagem)
Atualizado em: 2026-07-21
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B12)
Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` (sem rota nova)
Appetite: ~0,5 dia eng; `fitToKeys` no Leaflet + chaves estáveis do bundle + teste/checklist
Responsável: —
_Revisão 2026-07-21 (implementação): escopo ampliado com correção de três bugs de hover reportados em campo — (1) ênfase agressiva (fillOpacity + stroke grosso), (2) hover em municípios fora do filtro B7, (3) ênfase sticky no mouseout (`setHoveredKey` restyleava antes de limpar `hoveredKey` + `selectedKey` espelhava hover). Solução: `interactiveKeys` compartilhando chaves com `fitToKeys` (`plazasByIbgeCode`); hover stroke-only (fill inalterado, weight 2); clear síncrono de `hoveredKey`/`selectedKey` no mouseout desktop; touch pinned inalterado._
## Design (Impeccable)
Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`PlazaMapPanel`, `BahiaMap`).
@ -63,8 +65,8 @@ flowchart LR
Componentes:
- **`BahiaMap`** (`src/components/campaign/BahiaMap.tsx`): nova prop opcional `fitToKeys?: string[]` (default `[]`). Efeito (ou ramo) que, quando a chave canônica de `fitToKeys` muda: resolve features via `getMunicipalityFeature` / `getTerritoryFeature` (mesmo path de `highlightKeys`), `fitBounds` com padding/`maxZoom` atuais; se vazio → `BAHIA_BOUNDS`. **Não** alterar o estilo das layers. Depth check: não criar `BahiaMapViewport.ts` pass-through; reusar o bloco já existente de highlight bounds.
- **`PlazaMapPanel`** (`src/components/campaign/PlazaMapPanel.tsx`): `fitToKeys={Object.keys(bundle.plazasByIbgeCode)}` (memo estável se útil). Não passar as mesmas chaves em `highlightKeys`.
- **`BahiaMap`** (`src/components/campaign/BahiaMap.tsx`): props `fitToKeys?: string[]` e `interactiveKeys?: string[]` (default `[]` = fit Bahia / tudo interativo). Efeito quando `canonicalMapKeysKey(fitToKeys)` muda → `fitMapToHighlights` (sem alterar estilo). `interactiveKeys` com gate em handlers + `interactive: false` nos paths fora do set. Hover stroke-only + clear síncrono no mouseout. Reusa `fitMapToHighlights` e `canonicalMapKeysKey` em `bahiaMapStyle.ts`.
- **`PlazaMapPanel`** (`src/components/campaign/PlazaMapPanel.tsx`): memo `scopedKeys = Object.keys(bundle.plazasByIbgeCode)` → `fitToKeys` + `interactiveKeys`. Não passar as mesmas chaves em `highlightKeys`.
- **Teste:** unit do helper de chave canônica / “quando fitKey muda”; checklist manual: filtrar TI → aproxima; limpar filtros → Bahia; trocar Ano/Escala → viewport estável; assessor com poucas Praças → aproxima sem filtro URL.
- **Migration:** Sem migration, sem collection, sem server action.
@ -93,6 +95,7 @@ Componentes:
- **Controle “Ver Bahia toda”.** Revisitar se, após B12, usuários pedirem reset sem limpar filtros.
- **Threshold N→`BAHIA_BOUNDS`.** Revisitar se fit em ~400 `codarea` causar jank perceptível.
- **Restyle diff só nos paths cujo `interactive` mudou.** Revisitar se troca frequente de filtro causar jank no O(n) `restyleAllPaths` (hoje full pass quando `interactiveKey` muda).
- **`flyTo` / `animate: true`.** Revisitar se critique pedir transição; default Leaflet `fitBounds` basta no v1.
## Referências

View file

@ -1,6 +1,6 @@
# Roadmap — Teqo
Atualizado em: 2026-07-21 (B6 + B8 F1 + fill-in **C8** F4 entregues; B12 registrado; A9+/B7/B9–B11/filtros-auto entregues)
Atualizado em: 2026-07-21 (B12 entregue; B6 + B8 F1 + fill-in **C8** F4 entregues; A9+/B7/B9–B11/filtros-auto entregues)
Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc).
@ -72,7 +72,7 @@ flowchart TD
A9 --> A9plus["A9+ Loader lista ✓"]
A9plus --> B9["B9 Edição rápida lista ✓"]
R2 -.-> B7["B7 Mapa filtrado ✓"]
B7 --> B12["B12 Aproximar mapa<br/>ao filtro"]
B7 --> B12["B12 Aproximar mapa<br/>ao filtro ✓"]
R2 -.-> B8["B8 Polígonos Praças-zona<br/>(SSA/CMS)"]
R2 -.-> B10["B10 Hover/tap mapa ✓"]
R2 -.-> B11["B11 Escala % válidos<br/>no mapa ✓"]
@ -84,11 +84,11 @@ flowchart TD
C2prod --> C5["C5 GOTV (validar)"]
```
Paralelizáveis agora: **B12** (viewport do mapa; B7 já entregue), fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / **B7** / **B10** / **B11** / **B6** / **filtros-auto** / **C8** F4~~ entregues 2026-07-21.
Paralelizáveis agora: fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / **B7** / **B10** / **B11** / **B6** / **B12** / **filtros-auto** / **C8** F4~~ entregues 2026-07-21.
### Sequência por janela
**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ / ~~**B11** escala % dos válidos~~ / ~~**filtros-auto** lista de Praças~~ entregues; **B12** aproximar mapa ao footprint filtrado (quick win, ~0,5 dia); **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; R6 critique/polish; Onda 0 jurídica em paralelo.
**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ / ~~**B11** escala % dos válidos~~ / ~~**B12** aproximar mapa ao footprint filtrado~~ / ~~**filtros-auto** lista de Praças~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; R6 critique/polish; Onda 0 jurídica em paralelo.
**Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; D2 se sobrar folga.
@ -100,7 +100,7 @@ Paralelizáveis agora: **B12** (viewport do mapa; B7 já entregue), fill-ins (O0
**Não cortáveis:** Onda 0 (jurídico/Consent); R1–R2 (sem eles a vertical não reflete a operação real); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**A9**~~ (total esperado da Praça — entregue 2026-07-21).
**Cortes seguros** (se o prazo apertar, nesta ordem): R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); **B12** (mapa continua usável com pan/zoom manual); débitos/fill-ins. ~~**B9** / **B10** / **B11** / **B6**~~ (entregues — não cortar).
**Cortes seguros** (se o prazo apertar, nesta ordem): R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); débitos/fill-ins. ~~**B9** / **B10** / **B11** / **B6** / **B12**~~ (entregues — não cortar).
## Já entregue (resumo)
@ -113,6 +113,7 @@ Paralelizáveis agora: **B12** (viewport do mapa; B7 já entregue), fill-ins (O0
- **B9 (2026-07-21)** — **Edição rápida na lista de Praças** (Assessores / Tendência / `expectedVotes` via Popovers em `PlazaList*Control`; `plazaStaffFormActions`; sem migration) — [plano](plans/edicao-rapida-lista-pracas.md).
- **B10 (2026-07-21)** — **Hover/tap no Mapa das Praças** (destaque + `MapFeatureReadout`; desktop click navega; mobile 2º tap; SSA/CMS N>1 → `zoneBreakdown`; `plazasByIbgeCode` / `resolvePlazaMapNavigation`) — [plano](plans/hover-mapa-pracas.md).
- **B11 (2026-07-21)** — **Escala % dos válidos no Mapa das Praças** (`validVotesByYear` no bundle; seletor `Total (votos)` / `% dos válidos`; domínio fixo 0–100%; 2026 usa válidos 2022; compare desliga %; readout em %) — [plano](plans/escala-percentual-mapa-pracas.md).
- **B12 (2026-07-21)** — **Aproximar mapa ao footprint filtrado + correção hover** (`fitToKeys` + `interactiveKeys` em `BahiaMap` a partir de `plazasByIbgeCode`; `canonicalMapKeysKey`; hover stroke-only sem alterar fill; clear síncrono no mouseout; fit só quando footprint muda — sem re-zoom em Ano/Escala) — [plano](plans/aproximar-mapa-pracas.md).
- **B6 (2026-07-21)** — **`BahiaMap` setStyle incremental** (layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers em `bahiaMapStyle.ts`) — [plano](plans/escala-dry-pos-b3.md).
- **B8 F1 (2026-07-21)** — **Catálogo bairros das Praças-zona** (`plazaZoneNeighborhoods`: Salvador TRE-BA RA 02/2017 + Camaçari curado; fixture+int; card **Bairros desta Praça** no overview de Praças `kind=zona`; sem migration) — [plano](plans/poligonos-pracas-zona.md). **F2 pendente** (F2 prep catálogo ~½ dia + TopoJSON + mapa). Débitos pós-`/simplify`: hidratação via `plazaCatalog`, canônico JSON→TS, teste ordem slugs → F2 prep no plano (S6 CSS compartilhado adiado até F2/R6).
- **Fill-in filtros-auto (2026-07-21)** — **Filtros auto-aplicados na lista de Praças** (`PlazaFilters`: debounce 1s no `q`, Enter imediato, selects imediatos, remove Buscar; `useTransition` + pending a11y; `shouldUpdatePlazaSearchUrl` + no-op via `buildPlazaFiltersKey`; sem migration) — [plano](plans/filtros-auto-pracas.md). Débitos pós-`/simplify`: sync back/forward `search`↔`state.q` e shell pending compartilhado — gatilhos no plano.
@ -139,7 +140,6 @@ Paralelizáveis agora: **B12** (viewport do mapa; B7 já entregue), fill-ins (O0
- **A6** dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · reenquadrar para Praça · [plano](plans/insight-dobradinha-2026.md)
- **B5 F2–F3** cache CLI compartilhado + factory mun/TI (scripts continuam) · [plano](plans/escala-dry-pos-b2.md)
- **B8** Polígonos das Praças-zona (Salvador ZE 1–19 / Camaçari ZE 170–171): ~~F1 catálogo zona→bairros + UI na Praça~~ (entregue 2026-07-21); F2 prep catálogo (~½ dia) + dissolve IBGE/malha → TopoJSON no mapa · Janela 3 · cortável (F2) · [plano](plans/poligonos-pracas-zona.md)
- **B12** Aproximar o Mapa das Praças ao footprint filtrado (`fitToKeys` no `BahiaMap`; sem re-zoom em Ano/Escala) · Janela 1 · cortável · gatilho do B7 · [plano](plans/aproximar-mapa-pracas.md)
- **C5** operação dia D / GOTV _(validar com produto)_ · design [`Dia-D-GOTV`](design-refs/latest/Dia-D-GOTV.png) · depende de C2 dados reais
- **D2** push + sino in-app · soft: chave `campanha-notificacoes-push` (Onda 0) · [plano](plans/notifications.md)
- **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · gatilho: antes de 16/08 · (opcional: mover filtros acima do mapa)

View file

@ -15,6 +15,7 @@ import {
import {
buildHighlightSet,
buildLayerStyleContext,
canonicalMapKeysKey,
featureKeyFromProperties,
keyPropertyForMode,
resolvePathStyle,
@ -46,6 +47,10 @@ type BahiaMapProps = {
/** When set, overrides auto max from `values` (e.g. fixed 1 for 0–100% shares). */
scaleMax?: number
highlightKeys?: string[]
/** Fit viewport to these keys without highlighting them. */
fitToKeys?: string[]
/** When non-empty, only these keys receive hover/click. */
interactiveKeys?: string[]
selectedKey?: string | null
onFeatureSelect?: (info: BahiaMapFeatureInfo | null) => void
onFeatureActivate?: (key: string) => void
@ -96,6 +101,8 @@ export const BahiaMap = ({
fillMode = 'sequential',
scaleMax,
highlightKeys = [],
fitToKeys = [],
interactiveKeys = [],
selectedKey = null,
onFeatureSelect,
onFeatureActivate,
@ -113,6 +120,8 @@ export const BahiaMap = ({
const touchPinnedRef = useRef(false)
const onFeatureSelectRef = useRef(onFeatureSelect)
const onFeatureActivateRef = useRef(onFeatureActivate)
const interactiveSetRef = useRef<Set<string> | null>(null)
const lastFittedViewportKeyRef = useRef<string | null>(null)
const stylePropsRef = useRef({
values,
fillMode,
@ -121,7 +130,16 @@ export const BahiaMap = ({
selectedKey,
})
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
const highlightKey = useMemo(() => highlightKeys.slice().sort().join(','), [highlightKeys])
const highlightKey = useMemo(() => canonicalMapKeysKey(highlightKeys), [highlightKeys])
const fitKey = useMemo(() => canonicalMapKeysKey(fitToKeys), [fitToKeys])
const interactiveKey = useMemo(() => canonicalMapKeysKey(interactiveKeys), [interactiveKeys])
const viewportKey = useMemo(() => fitKey || highlightKey, [fitKey, highlightKey])
const interactiveSet = useMemo(
() => (interactiveKey ? buildHighlightSet(interactiveKey) : null),
[interactiveKey],
)
interactiveSetRef.current = interactiveSet
stylePropsRef.current = {
values,
@ -136,11 +154,21 @@ export const BahiaMap = ({
onFeatureActivateRef.current = onFeatureActivate
}, [onFeatureActivate, onFeatureSelect])
const isKeyInteractive = (key: string) => {
const set = interactiveSetRef.current
return set === null || set.has(key)
}
const resolvePathStyleForFeature = (context: LayerStyleContext, key: string) => ({
...resolvePathStyle(context, key),
interactive: isKeyInteractive(key),
})
const restyleFeature = (key: string) => {
const context = styleContextRef.current
const path = pathByKeyRef.current.get(key)
if (!context || !path) return
path.setStyle(resolvePathStyle(context, key))
path.setStyle(resolvePathStyleForFeature(context, key))
}
const restyleAllPaths = () => {
@ -148,23 +176,31 @@ export const BahiaMap = ({
if (!context) return
for (const [key, path] of pathByKeyRef.current) {
path.setStyle(resolvePathStyle(context, key))
path.setStyle(resolvePathStyleForFeature(context, key))
}
}
const setHoveredKey = (nextKey: string | null, bringToFrontLayer?: L.Path) => {
const setHoveredKey = (
nextKey: string | null,
bringToFrontLayer?: L.Path,
clearSelection = false,
) => {
const context = styleContextRef.current
if (!context) return
const previousKey = context.hoveredKey
if (previousKey === nextKey) return
context.hoveredKey = nextKey
if (clearSelection && previousKey && context.selectedKey === previousKey) {
context.selectedKey = null
}
if (previousKey) {
restyleFeature(previousKey)
}
context.hoveredKey = nextKey
if (nextKey) {
restyleFeature(nextKey)
;(bringToFrontLayer ?? pathByKeyRef.current.get(nextKey))?.bringToFront()
@ -221,6 +257,7 @@ export const BahiaMap = ({
let cancelled = false
setStatus('loading')
geometryReadyRef.current = false
lastFittedViewportKeyRef.current = null
const renderLayer = async () => {
try {
@ -252,7 +289,7 @@ export const BahiaMap = ({
const properties = feature?.properties as Record<string, string> | undefined
const key = featureKeyFromProperties(properties, keyProperty)
const context = styleContextRef.current ?? mountContext
return resolvePathStyle(context, key ?? '')
return resolvePathStyleForFeature(context, key ?? '')
},
onEachFeature: (feature, featureLayer) => {
if (!(featureLayer instanceof L.Path)) return
@ -265,6 +302,8 @@ export const BahiaMap = ({
pathByKeyRef.current.set(key, featureLayer)
featureLayer.on('mouseover', (event) => {
if (!isKeyInteractive(key)) return
const pointerType = getPointerType(event)
if (pointerType === 'mouse') {
touchPinnedRef.current = false
@ -280,11 +319,15 @@ export const BahiaMap = ({
featureLayer.on('mouseout', () => {
if (touchPinnedRef.current) return
setHoveredKey(null)
if (!isKeyInteractive(key)) return
setHoveredKey(null, undefined, true)
onFeatureSelectRef.current?.(null)
})
featureLayer.on('click', (event) => {
if (!isKeyInteractive(key)) return
touchPinnedRef.current = getPointerType(event, 'mouse') === 'touch'
setHoveredKey(key, featureLayer)
@ -301,12 +344,9 @@ export const BahiaMap = ({
syncMountStyleContext()
restyleAllPaths()
fitMapToHighlights(
map,
mode,
styleContextRef.current!.highlightSet,
geometryModule,
)
const fitIdentity = `${mode}:${viewportKey}`
fitMapToHighlights(map, mode, buildHighlightSet(viewportKey), geometryModule)
lastFittedViewportKeyRef.current = fitIdentity
geometryReadyRef.current = true
setStatus('ready')
@ -332,12 +372,21 @@ export const BahiaMap = ({
useEffect(() => {
if (!geometryReadyRef.current) return
const fitIdentity = `${mode}:${viewportKey}`
if (lastFittedViewportKeyRef.current === fitIdentity) return
const map = mapRef.current
const geometryModule = geometryModuleRef.current
if (!map || !geometryModule || !layerRef.current) return
fitMapToHighlights(map, mode, buildHighlightSet(highlightKey), geometryModule)
}, [highlightKey, mode])
fitMapToHighlights(map, mode, buildHighlightSet(viewportKey), geometryModule)
lastFittedViewportKeyRef.current = fitIdentity
}, [viewportKey, mode])
useEffect(() => {
if (!interactiveKey || pathByKeyRef.current.size === 0) return
restyleAllPaths()
}, [interactiveKey])
useEffect(() => {
const previousContext = styleContextRef.current

View file

@ -75,6 +75,11 @@ export const PlazaMapPanel = ({
)
}, [comparisonActive, displayValues, percentScaleActive])
const scopedKeys = useMemo(
() => Object.keys(bundle.plazasByIbgeCode),
[bundle.plazasByIbgeCode],
)
const metricLabel = useMemo(() => {
if (comparisonActive) {
return `diferença de votos em ${year}`
@ -252,6 +257,8 @@ export const PlazaMapPanel = ({
values={displayValues}
scaleMax={displayMax > 0 ? displayMax : undefined}
fillMode={comparisonActive ? 'diverging' : 'sequential'}
fitToKeys={scopedKeys}
interactiveKeys={scopedKeys}
selectedKey={selectedFeature?.key ?? null}
onFeatureSelect={handleFeatureSelect}
onFeatureActivate={handleFeatureActivate}

View file

@ -35,13 +35,13 @@ export const getFeatureStyle = ({
fillMode,
max,
}: FeatureStyleInput): PathOptions => ({
weight: highlighted ? 2.5 : 1,
weight: highlighted ? 2 : 1,
color: highlighted ? '#c51414' : '#a8a29e',
fillColor:
fillMode === 'diverging'
? divergingFillColor(metric, max)
: choroplethFillColor(metric, max),
fillOpacity: highlighted ? 0.92 : metric !== 0 ? 0.78 : 0.35,
fillOpacity: metric !== 0 ? 0.78 : 0.35,
})
export const computeChoroplethMax = (
@ -55,6 +55,9 @@ export const computeChoroplethMax = (
export const buildHighlightSet = (highlightKey: string): Set<string> =>
new Set(highlightKey.length > 0 ? highlightKey.split(',').filter(Boolean) : [])
export const canonicalMapKeysKey = (keys: string[]): string =>
keys.length > 0 ? [...keys].sort().join(',') : ''
export const keyPropertyForMode = (mode: 'municipality' | 'territory'): FeatureKeyProperty =>
mode === 'municipality' ? 'codarea' : 'code'

View file

@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
buildLayerStyleContext,
canonicalMapKeysKey,
computeChoroplethMax,
resolvePathStyle,
} from '@/lib/bahiaMapStyle'
@ -28,6 +29,17 @@ describe('bahiaMapStyle', () => {
})
})
describe('canonicalMapKeysKey', () => {
it('sorts keys for stable identity', () => {
expect(canonicalMapKeysKey(['2927408', '2905701'])).toBe('2905701,2927408')
expect(canonicalMapKeysKey(['2905701', '2927408'])).toBe('2905701,2927408')
})
it('returns empty string for empty input', () => {
expect(canonicalMapKeysKey([])).toBe('')
})
})
describe('resolvePathStyle', () => {
it('highlights hovered feature over selected', () => {
const context = {
@ -36,7 +48,7 @@ describe('bahiaMapStyle', () => {
hoveredKey: '2927408',
}
expect(resolvePathStyle(context, '2927408').weight).toBe(2.5)
expect(resolvePathStyle(context, '2927408').weight).toBe(2)
expect(resolvePathStyle(context, '2905701').weight).toBe(1)
})
@ -47,7 +59,7 @@ describe('bahiaMapStyle', () => {
hoveredKey: null,
}
expect(resolvePathStyle(context, '2905701').weight).toBe(2.5)
expect(resolvePathStyle(context, '2905701').weight).toBe(2)
})
it('reveals selected highlight after hover clears on a different feature', () => {
@ -58,7 +70,7 @@ describe('bahiaMapStyle', () => {
}
expect(resolvePathStyle(context, '2927408').weight).toBe(1)
expect(resolvePathStyle(context, '2905701').weight).toBe(2.5)
expect(resolvePathStyle(context, '2905701').weight).toBe(2)
})
it('highlights keys in highlightSet', () => {
@ -67,7 +79,18 @@ describe('bahiaMapStyle', () => {
highlightSet: new Set(['2927408']),
}
expect(resolvePathStyle(context, '2927408').weight).toBe(2.5)
expect(resolvePathStyle(context, '2927408').weight).toBe(2)
})
it('keeps fillOpacity when highlighted', () => {
const base = resolvePathStyle(baseContext, '2927408')
const highlighted = resolvePathStyle(
{ ...baseContext, hoveredKey: '2927408' },
'2927408',
)
expect(highlighted.fillOpacity).toBe(base.fillOpacity)
expect(highlighted.color).toBe('#c51414')
})
it('uses diverging fill for negative metrics', () => {