teqo/src/lib/municipalityVoteRank.ts
Francisco Ignácio Fontoura Solla 66feecd66b simplify(B13): corrige classe agregada, bolhas em pane proprio e registra debitos
O /simplify da escala relativa do mapa achou um bug semantico real em
computeAggregateTerritorialClass — somava o headroom dos slugs e comparava
contra a mediana de UMA unidade, classificando um grupo grande como expansao
quando ele e marginal. A mediana agora escala por slugs.length, com teste.

As bolhas sairam do centro do bounding box para o centroide real e ganharam
pane proprio (z-index 450), porque o bringToFront do hover as escondia atras
do poligono. Legenda e pintura passaram a compartilhar o mesmo predicado,
a copy dos quantis parou de prometer faixas de igual largura, e o subtitulo
do painel parou de dizer "em 2026" com 2022 selecionado (defeito do B11).

Fonte unica onde havia duas: NO_DATA_FILL, o raio das bolhas e
formatPlacementOrdinal (ex-formatMunicipalityVoteRank, agora client-safe em
electionFormat.ts).

Os quatro debitos maiores que o cleanup viraram o fill-in B13+
(docs/plans/escala-dry-pos-b13.md); a flakiness dos e2e, reproduzida em main
limpo, foi para o ledger com a causa afinada.
2026-07-26 05:12:37 -03:00

85 lines
2.5 KiB
TypeScript

import {
federalBaselineMunicipalitySlugs,
getMunicipalityFederalBaseline,
getStatewideFederalTotals,
} from '@/lib/bahiaElectionAggregates'
/** Year the mesa uses as the current priority lens (A11 list sort + default readout). */
export const DEFAULT_VOTE_RANK_YEAR = 2022
export type MunicipalityVoteRankEntry = {
rank: number
votes: number
/** Fraction of the candidate's statewide (artifact) total for the year — 0..1. */
share: number
totalUnits: number
}
const rankByYearCache = new Map<number, ReadonlyMap<string, MunicipalityVoteRankEntry>>()
/**
* Dense rank + share of own statewide vote for every catalog municipality slug
* in the committed federal baseline artifact. Ties share a rank; the next
* distinct total gets the next consecutive rank (no gaps).
*
* Cached at module scope — the artifact is immutable for the process lifetime.
*/
export const computeVoteRankByYear = (
year: number,
): ReadonlyMap<string, MunicipalityVoteRankEntry> => {
const cached = rankByYearCache.get(year)
if (cached) return cached
const yearKey = String(year)
const slugs = federalBaselineMunicipalitySlugs()
const rows = slugs.map((slug) => ({
slug,
votes: getMunicipalityFederalBaseline(slug).votesByYear[yearKey] ?? 0,
}))
const candidateTotal = getStatewideFederalTotals(year).ownVotes
rows.sort((a, b) => {
if (b.votes !== a.votes) return b.votes - a.votes
return a.slug.localeCompare(b.slug)
})
const totalUnits = rows.length
const bySlug = new Map<string, MunicipalityVoteRankEntry>()
let denseRank = 0
let previousVotes: number | undefined
for (const row of rows) {
if (row.votes !== previousVotes) {
denseRank += 1
previousVotes = row.votes
}
bySlug.set(row.slug, {
rank: denseRank,
votes: row.votes,
share: candidateTotal > 0 ? row.votes / candidateTotal : 0,
totalUnits,
})
}
rankByYearCache.set(year, bySlug)
return bySlug
}
export const getMunicipalityVoteRank = (
slug: string,
year: number = DEFAULT_VOTE_RANK_YEAR,
): MunicipalityVoteRankEntry | null => computeVoteRankByYear(year).get(slug) ?? null
/** Compare for list sort=votos. Zero-vote rows always sort last (any dir). */
export const compareMunicipalityVotesForSort = (
aVotes: number,
bVotes: number,
dir: 'asc' | 'desc',
): number => {
const aZero = aVotes === 0
const bZero = bVotes === 0
if (aZero !== bZero) return aZero ? 1 : -1
const cmp = aVotes - bVotes
return dir === 'asc' ? cmp : -cmp
}