teqo-backup/src/utilities/plazaMapData.ts
Francisco Ignácio Fontoura Solla 9db80d815b Deliver B11 percent-of-valid-votes scale on Praças map.
Adds valid-votes denominators to the map bundle, a Total vs % dos válidos selector with fixed 0–100% coloring, and readout/legend alignment via scaleMax; updates roadmap and post-simplify debt docs.
2026-07-21 13:12:05 -03:00

257 lines
8.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 type { Payload } from 'payload'
import { BASELINE_TICKET_2022, HISTORICAL_SERIES_YEARS } from '@/lib/electionResults'
import { getPlazaCatalogEntry } from '@/lib/plazaCatalog'
import type { CampaignUser } from '@/payload-types'
import {
plazaElectionGeography,
type PlazaElectionGeography,
} from '@/utilities/plazaElectionGeography'
import {
loadCandidateVotesByCityZone,
loadValidVotesByCityZone,
sumVotesForGeography,
} from '@/utilities/plazaElectoralBaseline'
import { buildPlazasByIbgeCode, type PlazasByIbgeCode } from '@/utilities/plazaMapNavigation'
import {
buildPlazaListWhere,
parsePlazaListParams,
type PlazaListSearchParams,
type PlazaListState,
} from '@/utilities/plazaUi'
import {
aggregatePledgesByPlaza,
emptyPlazaPledgeAggregate,
resolvePlazaStaffVoteTotal,
} from '@/utilities/votePledgeData'
export type { PlazasByIbgeCode, PlazaMapSlugEntry } from '@/utilities/plazaMapNavigation'
export const PLAZA_MAP_YEARS = [...HISTORICAL_SERIES_YEARS, 2026] as const
export type PlazaMapYear = (typeof PLAZA_MAP_YEARS)[number]
export type PlazaMapScaleMode = 'absolute' | 'percentValid'
export const PLAZA_MAP_SCALE_MODES = ['percentValid', 'absolute'] as const satisfies readonly PlazaMapScaleMode[]
export const plazaMapScaleModeLabels: Record<PlazaMapScaleMode, string> = {
absolute: 'Total (votos)',
percentValid: '% dos válidos',
}
export const plazaMapYearLabels: Record<PlazaMapYear, string> = {
2014: '2014 (TSE)',
2018: '2018 (TSE)',
2022: '2022 (TSE)',
2026: '2026 (estimativas)',
}
export type PlazaZoneBreakdownRow = {
slug: string
name: string
votesByYear: Record<string, number>
}
export type PlazaMapComparison = {
candidateNumber: number
candidateName: string
/** TSE year (as string) → codarea → (sollaVotes − otherVotes). */
diffByYear: Record<string, Record<string, number>>
}
export type PlazaMapBundle = {
/** year (as string) → codarea → value. 2026 = expectedVotes ?? pledge effective total. */
valuesByYear: Record<string, Record<string, number>>
/** year (as string) → codarea → votosValidos (federal T1). 2026 reuses 2022. */
validVotesByYear: Record<string, Record<string, number>>
/** IBGE codarea → accessible plaza slugs for map click navigation. */
plazasByIbgeCode: PlazasByIbgeCode
/** Zone plazas in scope (Salvador/Camaçari) with per-year values. */
zoneBreakdown: PlazaZoneBreakdownRow[]
candidateName: string
comparison: PlazaMapComparison | null
}
type ScopedPlaza = {
id: number
slug: string
name: string
kind: 'municipio' | 'zona'
ibgeCode: string
expectedVotes: number | null
geography: PlazaElectionGeography
}
const loadScopedPlazas = async (
payload: Payload,
user: CampaignUser,
state: PlazaListState,
): Promise<ScopedPlaza[]> => {
const result = await payload.find({
collection: 'plaza',
depth: 0,
limit: 0,
pagination: false,
select: { slug: true, name: true, kind: true, ibgeCode: true, expectedVotes: true },
where: buildPlazaListWhere(state),
user,
overrideAccess: false,
})
return result.docs.flatMap((plaza) => {
const entry = getPlazaCatalogEntry(plaza.slug)
if (!entry) return []
return [
{
id: plaza.id,
slug: plaza.slug,
name: plaza.name,
kind: plaza.kind,
ibgeCode: plaza.ibgeCode,
expectedVotes: plaza.expectedVotes ?? null,
geography: plazaElectionGeography(entry),
},
]
})
}
/**
* Map data for the Praças overview: plazas matching the list URL filters (and
* role access) contribute their geography's votes (or 2026 pledge estimates)
* to municipality polygons. When `state.compare` is set, TSE years also carry
* a red↔white↔blue diff (Solla − other candidate).
*/
export const loadPlazaMapBundle = async (
payload: Payload,
user: CampaignUser,
searchParams: PlazaListSearchParams,
): Promise<PlazaMapBundle | null> => {
if (user.role === 'leader') return null
const state = parsePlazaListParams(searchParams)
const plazas = await loadScopedPlazas(payload, user, state)
if (plazas.length === 0) return null
const valuesByYear: Record<string, Record<string, number>> = {}
const validVotesByYear: Record<string, Record<string, number>> = {}
const zoneVotesBySlug = new Map<string, Record<string, number>>()
const sollaVotesByYear = new Map<number, Map<string, number>>()
const yearLoads = await Promise.all(
HISTORICAL_SERIES_YEARS.map(async (year) => {
const [votesByCityZone, validByCityZone] = await Promise.all([
loadCandidateVotesByCityZone(payload, user, {
year,
candidateNumber: BASELINE_TICKET_2022.candidate.candidateNumber,
}),
loadValidVotesByCityZone(payload, user, { year }),
])
return { year, votesByCityZone, validByCityZone }
}),
)
for (const { year, votesByCityZone, validByCityZone } of yearLoads) {
sollaVotesByYear.set(year, votesByCityZone)
const values: Record<string, number> = {}
const validValues: Record<string, number> = {}
for (const plaza of plazas) {
const votes = sumVotesForGeography(votesByCityZone, plaza.geography)
values[plaza.ibgeCode] = (values[plaza.ibgeCode] ?? 0) + votes
const valid = sumVotesForGeography(validByCityZone, plaza.geography)
validValues[plaza.ibgeCode] = (validValues[plaza.ibgeCode] ?? 0) + valid
if (plaza.kind === 'zona') {
const bySlug = zoneVotesBySlug.get(plaza.slug) ?? {}
bySlug[String(year)] = votes
zoneVotesBySlug.set(plaza.slug, bySlug)
}
}
valuesByYear[String(year)] = values
validVotesByYear[String(year)] = validValues
}
validVotesByYear['2026'] = validVotesByYear['2022'] ?? {}
const pledgeAggregates = await aggregatePledgesByPlaza(
payload,
plazas.map((plaza) => plaza.id),
)
const pledgeValues: Record<string, number> = {}
for (const plaza of plazas) {
const aggregate = pledgeAggregates.get(plaza.id) ?? emptyPlazaPledgeAggregate
const votes = resolvePlazaStaffVoteTotal(plaza.expectedVotes, aggregate.effectiveTotal)
if (votes > 0) pledgeValues[plaza.ibgeCode] = (pledgeValues[plaza.ibgeCode] ?? 0) + votes
if (plaza.kind === 'zona') {
const bySlug = zoneVotesBySlug.get(plaza.slug) ?? {}
bySlug['2026'] = votes
zoneVotesBySlug.set(plaza.slug, bySlug)
}
}
valuesByYear['2026'] = pledgeValues
const zoneBreakdown = plazas
.filter((plaza) => plaza.kind === 'zona')
.map((plaza) => ({
slug: plaza.slug,
name: plaza.name,
votesByYear: zoneVotesBySlug.get(plaza.slug) ?? {},
}))
.sort((left, right) => left.name.localeCompare(right.name, 'pt-BR'))
let comparison: PlazaMapComparison | null = null
if (state.compare) {
const candidates = await payload.find({
collection: 'electionCandidate',
where: {
and: [
{ candidateNumber: { equals: state.compare } },
{ office: { equals: 'deputado_federal' } },
{ turn: { equals: '1' } },
],
},
depth: 0,
limit: 1,
pagination: false,
sort: '-year',
select: { urnaName: true, party: true },
overrideAccess: true,
})
const candidate = candidates.docs[0]
if (candidate) {
const diffByYear: Record<string, Record<string, number>> = {}
const otherVotesByYear = await Promise.all(
HISTORICAL_SERIES_YEARS.map((year) =>
loadCandidateVotesByCityZone(payload, user, {
year,
candidateNumber: state.compare!,
}),
),
)
for (const [index, year] of HISTORICAL_SERIES_YEARS.entries()) {
const otherVotes = otherVotesByYear[index]!
const sollaVotes = sollaVotesByYear.get(year) ?? new Map<string, number>()
const values: Record<string, number> = {}
for (const plaza of plazas) {
const solla = sumVotesForGeography(sollaVotes, plaza.geography)
const other = sumVotesForGeography(otherVotes, plaza.geography)
values[plaza.ibgeCode] = (values[plaza.ibgeCode] ?? 0) + (solla - other)
}
diffByYear[String(year)] = values
}
comparison = {
candidateNumber: state.compare,
candidateName: `${candidate.urnaName}${candidate.party ? ` (${candidate.party})` : ''}`,
diffByYear,
}
}
}
return {
valuesByYear,
validVotesByYear,
plazasByIbgeCode: buildPlazasByIbgeCode(plazas),
zoneBreakdown,
candidateName: BASELINE_TICKET_2022.candidate.name,
comparison,
}
}