teqo-backup/src/components/campaign/map/MapFeatureReadout.tsx
Francisco Ignácio Fontoura Solla 29f45e093a B13: escala relativa no mapa dos Municipios (quantis/LQ/posicao) + artefato TSE v3
O coropleto do Inicio era quase monocromatico porque 0-100% dos validos comprime
uma disputa cujo topo estadual e 4,71%. Tres escalas relativas ao proprio
candidato, todas em classes discretas: quantis (novo default), padrao proprio
(LQ) e posicao no municipio, ao lado de "% dos validos" (B11) e "Total (votos)".

Artefato v3: federalRankByIbgeCode com a colocacao entre todos os candidatos com
votos > 0 na geografia (2014/2018/2022), chaveado por ibgeCode porque e o que o
mapa desenha. Fica no artefato commitado, nao em query de request, que varreria
~100 mil linhas por ano para um dado imutavel. 534 -> 623 KB (orcamento 700 KB).

LQ virou 5 classes nas ancoras do E10, nao diverging: o plano pedia as duas
coisas e se contradizia. Vermelho x azul continua significando "Solla x
adversario" e segue exclusivo do compare.

computeAggregateTerritorialClass classifica um grupo de slugs como um territorio
so, somando insumos e rodando o mesmo classificador (LQ e razao; a media de
razoes nao e a razao das somas) - o E12 herda o helper. TERRITORIAL_CLASS_ANCHORS
mudou para src/lib/territorialClassAnchors.ts para nao arrastar o artefato TSE
ao bundle do cliente.

Quantis sobre o escopo de acesso do usuario, degradando para 3 classes abaixo de
10 municipios com a legenda declarando a contagem efetiva. Rank indisponivel em
2026 (dado TSE); LQ em 2026 reusa o padrao estadual de 2022.

Bolhas opcionais por validos projetados (area proporcional ao valor, cor da
classe E10, interactive: false). 417 marcadores medidos antes de otimizar - nao
derrubaram o frame, entao o corte para top-K nao foi feito.

Sem migration, sem collection, sem server action; bundle client de /campanha
byte a byte igual.
2026-07-26 04:22:56 -03:00

120 lines
4.4 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}
{navigation?.kind === 'zones' ? (
<p className="text-xs text-muted-foreground">
Toque de novo no mapa ou role até os Municípios por zona abaixo.
</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>
)
}