teqo-backup/src/components/campaign/map/MapFeatureReadout.tsx
Francisco Ignácio Fontoura Solla 84e796ab49 B8 F2: polígonos das ZE de Salvador e re-keying do coroplético por map key
Salvador era um polígono só carregando os votos da cidade inteira, então o
coroplético não conseguia responder "qual pedaço de Salvador está fraco / é
meu". As 19 ZE ganham polígono próprio, dissolvido da malha de bairros do IBGE
(Censo 2022) pela lista de circunscrição da RA 02/2017 do TRE-BA, e o mapa passa
a pintar por map key (slug para as zonas, ibgeCode para o resto) em vez de
codarea. O polígono municipal de Salvador continua desenhado por baixo, sem dado
e sem interação, para não abrir buraco no contorno do estado.

Aproximação declarada na copy e no cabeçalho do script: o TRE publica a
circunscrição como lista de bairros, então o bairro é o átomo geométrico —
Periperi, que a resolução corta na Rua das Pedrinhas, vai inteiro para a ZE 17.
competitiveRank segue por cidade (o artefato TSE só tem federalRankByIbgeCode).

Sem migration, sem collection, sem action, sem Consent.
2026-07-26 23:08:09 -03:00

115 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from 'next/link'
import type { BahiaMapFeatureInfo } from '@/components/campaign/map/BahiaMap'
import { Button } from '@/components/ui/button'
import { formatElectionNumber } from '@/lib/electionFormat'
import type { MunicipalityMapScaleMode } from '@/utilities/municipalityMapContract'
import type { MunicipalityMapNavigation } from '@/utilities/municipalityMapNavigation'
type MapFeatureReadoutProps = {
feature: BahiaMapFeatureInfo | null
metricValue: number | undefined
rawMetricValue: number | undefined
metricLabel: string
scaleMode: MunicipalityMapScaleMode
/**
* What the colour means for THIS município, spelled out — "4ª de 5 faixas",
* "1,8× o padrão estadual do candidato", "12º entre 663 candidatos". A class
* without its reason is a verdict the reader cannot check.
*/
relativeReading: string | null
/** What the bubble encodes for THIS município — only while the layer is on. */
bubbleReading: string | null
comparisonActive: boolean
navigation: MunicipalityMapNavigation | null
}
const percentFormatter = new Intl.NumberFormat('pt-BR', {
style: 'percent',
maximumFractionDigits: 1,
})
const formatMetricValue = (
value: number | undefined,
scaleMode: MunicipalityMapScaleMode,
comparisonActive: boolean,
): string => {
if (value === undefined) return 'Sem dados'
if (comparisonActive) {
const prefix = value > 0 ? '+' : ''
return `${prefix}${formatElectionNumber(value)}`
}
if (scaleMode === 'percentValid') return percentFormatter.format(value)
return formatElectionNumber(value)
}
export const MapFeatureReadout = ({
feature,
metricValue,
rawMetricValue,
metricLabel,
scaleMode,
relativeReading,
bubbleReading,
comparisonActive,
navigation,
}: MapFeatureReadoutProps) => {
if (!feature) {
return (
<p className="min-h-11 content-center text-sm text-muted-foreground" aria-live="polite">
Passe o cursor ou toque em um município para ver os votos.
</p>
)
}
const formattedValue = formatMetricValue(metricValue, scaleMode, comparisonActive)
// Every mode except the percentage prints the metric next to the number, so
// announcing it again would read it twice.
const showMetricLabel =
!comparisonActive && metricValue !== undefined && scaleMode !== 'percentValid'
const showPercentSecondary =
!comparisonActive &&
scaleMode === 'percentValid' &&
metricValue !== undefined &&
rawMetricValue !== undefined &&
rawMetricValue > 0
return (
<div
className="flex flex-col gap-2 rounded-lg border bg-muted/30 px-3 py-2 sm:flex-row sm:items-center sm:justify-between"
aria-live="polite"
>
<div className="flex min-w-0 flex-col gap-0.5">
<p className="truncate text-sm font-medium">{feature.name}</p>
<p className="text-sm text-muted-foreground">
{showMetricLabel ? null : <span className="sr-only">{metricLabel}: </span>}
<span className="tabular-nums">{formattedValue}</span>
{showMetricLabel ? <span className="text-muted-foreground"> {metricLabel}</span> : null}
{!comparisonActive && metricValue !== undefined && scaleMode === 'percentValid' ? (
<span className="text-muted-foreground"> dos válidos</span>
) : null}
{comparisonActive && metricValue !== undefined ? (
<span className="text-muted-foreground"> (diferença)</span>
) : null}
{relativeReading ? (
<span className="text-muted-foreground"> · {relativeReading}</span>
) : null}
</p>
{showPercentSecondary ? (
<p className="text-xs text-muted-foreground tabular-nums">
{formatElectionNumber(rawMetricValue)} votos
</p>
) : null}
{/* The bubble's own line: mixing "votes he got" and "votes at stake"
into one sentence is how a reader ends up quoting the wrong one. */}
{bubbleReading ? <p className="text-xs text-muted-foreground">{bubbleReading}</p> : null}
</div>
{navigation?.kind === 'navigate' ? (
<Button asChild className="min-h-11 shrink-0">
<Link href={`/campanha/municipios/${navigation.slug}`}>Abrir município</Link>
</Button>
) : null}
</div>
)
}