teqo/src/lib/municipalityVoteRank.ts
Francisco Ignácio Fontoura Solla 3c95bc6174 A11: show municipality 2022 vote rank/share and default list sort by votos.
Dense catalog rank plus share of statewide votes on the baseline card and list column (header label 2022), with URL sort/dir and Impeccable polish.
2026-07-24 18:40:56 -03:00

95 lines
2.8 KiB
TypeScript

import {
federalBaselineMunicipalitySlugs,
getMunicipalityFederalBaseline,
} from '@/lib/bahiaElectionAggregates'
import { formatElectionNumber } from '@/lib/electionInsights'
/** 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,
}))
let candidateTotal = 0
for (const row of rows) candidateTotal += row.votes
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
export const formatMunicipalityVoteRank = (rank: number): string =>
`${formatElectionNumber(rank)}º`
export const formatMunicipalityVoteShare = (share: number): string =>
`${(share * 100).toLocaleString('pt-BR', {
maximumFractionDigits: 1,
minimumFractionDigits: 0,
})}%`
/** 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
}