'use client' import Link from 'next/link' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useCallback, useMemo, useRef, useState } from 'react' import { BahiaMap, type BahiaMapFeatureInfo } from '@/components/campaign/BahiaMap' import { ChoroplethLegend } from '@/components/campaign/ChoroplethLegend' import { MapFeatureReadout } from '@/components/campaign/MapFeatureReadout' import { useMunicipalityEstimateScenarioOptional } from '@/components/campaign/MunicipalityEstimateScenarioContext' import { VOTE_ESTIMATE_SCENARIO_MAP_HINT, VoteEstimateScenarioField, } from '@/components/campaign/VoteEstimateScenarioField' import { Field, FieldLabel } from '@/components/ui/field' import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select' import { computeChoroplethMax } from '@/lib/bahiaMapStyle' import { computeValidVoteShares, divergingGradientCss } from '@/lib/choroplethColorScale' import { formatElectionNumber } from '@/lib/electionInsights' import type { FederalCandidateOption } from '@/utilities/electionCandidateOptions' import { PLAZA_MAP_SCALE_MODES, PLAZA_MAP_YEARS, municipalityMapScaleModeLabels, municipalityMapYearLabels, type MunicipalityMapBundle, type MunicipalityMapScaleMode, type MunicipalityMapYear, } from '@/utilities/municipalityMapData' import { resolveMunicipalityMapNavigation } from '@/utilities/municipalityMapNavigation' import { DEFAULT_VOTE_ESTIMATE_SCENARIO, voteEstimateScenarioLabels, type VoteEstimateScenario, } from '@/utilities/voteEstimate' type MunicipalityMapPanelProps = { bundle: MunicipalityMapBundle candidateOptions: FederalCandidateOption[] defaultYear?: MunicipalityMapYear } export const MunicipalityMapPanel = ({ bundle, candidateOptions, defaultYear = 2022, }: MunicipalityMapPanelProps) => { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const [year, setYear] = useState(defaultYear) const [scaleMode, setScaleMode] = useState('percentValid') const scenarioContext = useMunicipalityEstimateScenarioOptional() const [localEstimateScenario, setLocalEstimateScenario] = useState( DEFAULT_VOTE_ESTIMATE_SCENARIO, ) const estimateScenario = scenarioContext?.scenario ?? localEstimateScenario const setEstimateScenario = scenarioContext?.setScenario ?? setLocalEstimateScenario const [selectedFeature, setSelectedFeature] = useState(null) const selectedKeyRef = useRef(null) const comparison = bundle.comparison const comparisonActive = comparison !== null && year !== 2026 const percentScaleActive = !comparisonActive && scaleMode === 'percentValid' const rawValues = useMemo(() => { if (comparisonActive && comparison) { return comparison.diffByYear[String(year)] ?? {} } if (year === 2026) { return bundle.values2026ByScenario[estimateScenario] ?? {} } return bundle.valuesByYear[String(year)] ?? {} }, [bundle, comparison, comparisonActive, estimateScenario, year]) const validVotesForYear = bundle.validVotesByYear[String(year)] ?? {} const displayValues = useMemo(() => { if (percentScaleActive) { return computeValidVoteShares(rawValues, validVotesForYear) } return rawValues }, [percentScaleActive, rawValues, validVotesForYear]) const displayMax = useMemo(() => { const fillMode = comparisonActive ? 'diverging' : 'sequential' return computeChoroplethMax(displayValues, fillMode, percentScaleActive ? 1 : undefined) }, [comparisonActive, displayValues, percentScaleActive]) const scopedKeys = useMemo(() => Object.keys(bundle.municipalitiesByIbgeCode), [bundle.municipalitiesByIbgeCode]) const metricLabel = useMemo(() => { if (comparisonActive) { return `diferença de votos em ${year}` } if (percentScaleActive) { if (year === 2026) { return 'participação nos válidos 2022 (estimativas 2026)' } return `participação nos válidos (${year})` } if (year === 2026) return `votos estimados 2026 (${voteEstimateScenarioLabels[estimateScenario].toLowerCase()})` return `votos de ${bundle.candidateName} em ${year}` }, [bundle.candidateName, comparisonActive, estimateScenario, percentScaleActive, year]) const setCompare = (candidateNumber: string) => { const params = new URLSearchParams(searchParams.toString()) if (candidateNumber) params.set('compare', candidateNumber) else params.delete('compare') router.replace(`${pathname}${params.size ? `?${params.toString()}` : ''}`, { scroll: false }) } const handleFeatureSelect = useCallback((info: BahiaMapFeatureInfo | null) => { selectedKeyRef.current = info?.key ?? null setSelectedFeature(info) }, []) const handleFeatureActivate = useCallback( (key: string) => { if (selectedKeyRef.current !== key) return const navigation = resolveMunicipalityMapNavigation(key, bundle.municipalitiesByIbgeCode) if (navigation.kind === 'navigate') { router.push(`/campanha/municipios/${navigation.slug}`) return } if (navigation.kind === 'zones') { document.getElementById('municipality-zone-breakdown')?.scrollIntoView({ behavior: 'smooth', block: 'nearest', }) } }, [bundle.municipalitiesByIbgeCode, router], ) const selectedMetricValue = selectedFeature && selectedFeature.key in displayValues ? displayValues[selectedFeature.key] : undefined const selectedRawMetricValue = selectedFeature && selectedFeature.key in rawValues ? rawValues[selectedFeature.key] : undefined const selectedNavigation = useMemo( () => selectedFeature ? resolveMunicipalityMapNavigation(selectedFeature.key, bundle.municipalitiesByIbgeCode) : null, [bundle.municipalitiesByIbgeCode, selectedFeature], ) return (

Mapa das Praças

{comparisonActive && comparison ? `Comparação ${bundle.candidateName} × ${comparison.candidateName} em ${year}.` : percentScaleActive ? `Cor pela participação nos votos válidos — em 2026, estimativas sobre válidos de 2022.` : `Cor pela votação de ${bundle.candidateName} — em 2026, pelas estimativas da campanha.`}

Ano setYear(Number(event.target.value) as MunicipalityMapYear)} className="min-h-11 w-full" > {PLAZA_MAP_YEARS.map((entry) => ( {municipalityMapYearLabels[entry]} ))} {!comparisonActive ? ( Escala setScaleMode(event.target.value as MunicipalityMapScaleMode)} className="min-h-11 w-full" > {PLAZA_MAP_SCALE_MODES.map((mode) => ( {municipalityMapScaleModeLabels[mode]} ))} ) : null} {year === 2026 && !comparisonActive ? ( ) : null} Comparar com setCompare(event.target.value)} className="min-h-11 w-full" > Sem comparação {candidateOptions.map((candidate) => ( {candidate.name} {candidate.party ? ` (${candidate.party})` : ''} — {candidate.candidateNumber} ))}
{comparisonActive && comparison ? (
+{comparison.candidateName}
+{bundle.candidateName}

Vermelho: {bundle.candidateName} na frente · Branco: empate · Azul:{' '} {comparison.candidateName} na frente. Comparação usa diferença absoluta.

) : displayMax > 0 ? ( '100%' : undefined} /> ) : (

Sem dados para este ano no seu escopo.

)} {comparison && year === 2026 ? (

A comparação usa os anos com dados TSE (2014, 2018, 2022). Escolha um destes anos para ver o mapa comparativo.

) : null} 0 ? displayMax : undefined} fillMode={comparisonActive ? 'diverging' : 'sequential'} fitToKeys={scopedKeys} interactiveKeys={scopedKeys} selectedKey={selectedFeature?.key ?? null} onFeatureSelect={handleFeatureSelect} onFeatureActivate={handleFeatureActivate} ariaLabel={ comparisonActive && comparison ? `Mapa comparativo entre ${bundle.candidateName} e ${comparison.candidateName} por município` : `Mapa da Bahia com ${metricLabel} por município` } /> {bundle.zoneBreakdown.length > 0 && !comparisonActive ? (

Praças por zona eleitoral

Zonas não têm polígono oficial — Salvador e Camaçari aparecem agregadas no mapa e detalhadas aqui.

    {bundle.zoneBreakdown.map((zone) => (
  • {zone.name} {formatElectionNumber( year === 2026 ? (zone.votes2026ByScenario[estimateScenario] ?? 0) : (zone.votesByYear[String(year)] ?? 0), )}
  • ))}
) : null}
) }