mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
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.
120 lines
4.4 KiB
TypeScript
120 lines
4.4 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'
|
||
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>
|
||
)
|
||
}
|