mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
Merge branch 'cursor/ba211341'
This commit is contained in:
commit
3adc8eacc3
8 changed files with 119 additions and 31 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue