F1 — a tolerancia de simplificacao e os digitos de quantizacao viraram politica em unidades de solo em scripts/lib/topology.mjs, consumida pelos dois scripts de geometria. Os quantis 0.35 copiados cortavam as 19 ZE de Salvador a ~0.6 m2 e os 417 municipios a ~1.78 km2; com tolerancia absoluta e a grade de ~103 m do estado, a malha das zonas caiu de 480 para 30.2 pontos/feature. bahia-municipalities segue byte-identico e o tripwire de bytes deu lugar a pontos/feature. F2 — featureMapKey passou a ser asserido sobre features reais das tres malhas comprometidas (417 codarea, 27 code, 19 slug), e o comentario de MAP_KEY_PROPERTIES agora diz o que o codigo faz: as propriedades sao mutuamente exclusivas, nao ordenadas por especificidade. F3 — a malha das zonas e um detalhe sobre o mapa, nao o mapa: se o chunk falha, Salvador cai no poligono municipal que ja esta desenhado embaixo e o resto da Bahia continua funcionando, com o motivo logado. A ressalva de aproximacao saiu do gate de comparacao e passou a depender de bundle.hasZoneMunicipalities, derivado no servidor de municipality.kind. F4 — orfaos deletados: os dois acessores sem chamador, isZoneMapKey (o servidor ja sabe a regra), o wrapper simplifyTopology e o helper polygonCount com um branch inalcancavel. MunicipalityMapNavigation colapsou em string | undefined e MunicipalitiesByMapKey em Record<string, string>.
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
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'
|
||
|
||
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
|
||
/** The município the painted feature opens, when it is one the actor can read. */
|
||
municipalitySlug: string | undefined
|
||
}
|
||
|
||
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,
|
||
municipalitySlug,
|
||
}: 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>
|
||
|
||
{municipalitySlug ? (
|
||
<Button asChild className="min-h-11 shrink-0">
|
||
<Link href={`/campanha/municipios/${municipalitySlug}`}>Abrir município</Link>
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|