diff --git a/scripts/build-election-aggregates.mjs b/scripts/build-election-aggregates.mjs new file mode 100644 index 00000000..1000fe3d --- /dev/null +++ b/scripts/build-election-aggregates.mjs @@ -0,0 +1,111 @@ +/** + * Builds the committed per-municipality election baseline artifact consumed by + * the campaign map (src/lib/electionAggregates/bahia-federal-baseline.json). + * + * Why an artifact instead of runtime queries: the 2014/2018/2022 TSE results + * are immutable, but the map used to re-scan statewide city×zone slices on + * every dashboard visit. Precomputing the per-municipality sums (Solla nominal + * votes + federal T1 valid votes, cut by the same `municipalityCatalog` + * geography used at runtime — Salvador split into zone municipalities, every + * other municipality whole) removes all historical-year DB work. + * + * Why not compute during `pnpm build`: Vercel builds must not depend on the + * production database CONTENT (TSE seeds are local-only by policy), and the + * artifact should only change when the electoral scope deliberately changes. + * + * Prerequisite: `pnpm db:seed:tse` (2022) + `--year=2018` + `--year=2014` on + * the LOCAL database. + * + * Usage: + * pnpm build:election-aggregates + * + * Safety: read-only against the local database (assertLocalDatabase guard); + * writes only src/lib/electionAggregates/. + */ + +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { config as loadEnv } from 'dotenv' +import { getPayload } from 'payload' + +import { assertLocalDatabase } from './assert-local-database.mjs' + +loadEnv({ path: '.env.local' }) +loadEnv({ path: '.env' }) + +const payloadConfig = (await import('../src/payload.config.ts')).default +const { BASELINE_TICKET_2022, HISTORICAL_SERIES_YEARS } = await import( + '../src/lib/electionResults.ts' +) +const { municipalityCatalog } = await import('../src/lib/municipalityCatalog.ts') +const { municipalityElectionGeography } = await import( + '../src/utilities/municipalityElectionGeography.ts' +) +const { + loadCandidateVotesByCityZone, + loadValidVotesByCityZone, + sumVotesForGeography, +} = await import('../src/utilities/municipalityElectoralBaseline.ts') + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const OUTPUT_DIR = join(ROOT, 'src', 'lib', 'electionAggregates') +const OUTPUT_FILE = join(OUTPUT_DIR, 'bahia-federal-baseline.json') + +assertLocalDatabase( + 'build:election-aggregates', + 'This script only reads the LOCAL seeded TSE data (pnpm db:seed:tse).', +) + +const payload = await getPayload({ config: payloadConfig }) + +/** CLI reader — satisfies the election-data read assertion (admin realm). */ +const cliReader = { collection: 'users' } + +const candidateNumber = BASELINE_TICKET_2022.candidate.candidateNumber +const municipalities = {} +for (const entry of municipalityCatalog) { + municipalities[entry.slug] = { votesByYear: {}, validVotesByYear: {} } +} + +for (const year of HISTORICAL_SERIES_YEARS) { + const [candidateVotes, validVotes] = await Promise.all([ + loadCandidateVotesByCityZone(payload, cliReader, { year, candidateNumber }), + loadValidVotesByCityZone(payload, cliReader, { year }), + ]) + + if (candidateVotes.size === 0) { + console.error( + `[build:election-aggregates] No nominal votes found for ${year}. ` + + `Seed the local database first: pnpm db:seed:tse -- --year=${year}`, + ) + process.exit(1) + } + + let yearTotal = 0 + for (const entry of municipalityCatalog) { + const geography = municipalityElectionGeography(entry) + const votes = sumVotesForGeography(candidateVotes, geography) + const valid = sumVotesForGeography(validVotes, geography) + municipalities[entry.slug].votesByYear[String(year)] = votes + municipalities[entry.slug].validVotesByYear[String(year)] = valid + yearTotal += votes + } + console.log(`[build:election-aggregates] ${year}: ${yearTotal} votes for #${candidateNumber}.`) +} + +const artifact = { + provenance: + 'Derived from TSE open data seeded by pnpm db:seed:tse; regenerate with pnpm build:election-aggregates.', + candidateNumber, + candidateName: BASELINE_TICKET_2022.candidate.name, + years: [...HISTORICAL_SERIES_YEARS], + municipalities, +} + +await mkdir(OUTPUT_DIR, { recursive: true }) +await writeFile(OUTPUT_FILE, `${JSON.stringify(artifact, null, 2)}\n`) +console.log( + `[build:election-aggregates] Wrote ${OUTPUT_FILE} (${municipalityCatalog.length} municipalities × ${HISTORICAL_SERIES_YEARS.length} years).`, +) +process.exit(0) diff --git a/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx index 4dec6cc8..6a64d454 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx @@ -196,7 +196,7 @@ const OverviewTab = async ({ const ElectionsTab = async ({ slug, rawSearchParams, - payloadUser: { payload, user }, + payloadUser: { user }, }: { slug: string rawSearchParams: MunicipalityDetailSearchParams @@ -213,11 +213,11 @@ const ElectionsTab = async ({ ].slice(0, MAX_COMPARISON_CANDIDATES) const [baseline, comparisonRows, candidateOptions] = await Promise.all([ - geography ? loadMunicipalityElectoralBaseline(payload, user, geography) : null, + geography ? loadMunicipalityElectoralBaseline(user, geography) : null, geography - ? loadMunicipalityCandidateComparison(payload, user, geography, compareNumbers) + ? loadMunicipalityCandidateComparison(user, geography, compareNumbers) : ([] as Awaited>), - loadFederalCandidateOptions(payload, user), + loadFederalCandidateOptions(user), ]) return ( diff --git a/src/app/(campaign)/campanha/(app)/page.tsx b/src/app/(campaign)/campanha/(app)/page.tsx index 5456a58d..89852d6b 100644 --- a/src/app/(campaign)/campanha/(app)/page.tsx +++ b/src/app/(campaign)/campanha/(app)/page.tsx @@ -42,7 +42,7 @@ export default async function CampaignHomePage({ searchParams }: CampaignHomePag isStaff ? loadMunicipalityMapBundle(payload, user, { compare: rawSearchParams.compare }) : Promise.resolve(null), - isStaff ? loadFederalCandidateOptions(payload, user) : Promise.resolve([]), + isStaff ? loadFederalCandidateOptions(user) : Promise.resolve([]), ]) return ( diff --git a/src/app/(frontend)/abaixo-assinado/[id]/page.tsx b/src/app/(frontend)/abaixo-assinado/[id]/page.tsx index 31f961c7..30af3074 100644 --- a/src/app/(frontend)/abaixo-assinado/[id]/page.tsx +++ b/src/app/(frontend)/abaixo-assinado/[id]/page.tsx @@ -3,7 +3,7 @@ import { MetaPixel } from '@/components/MetaPixel' import { SignatureCounter } from '@/components/SignatureCounter' import { getSignatureCount } from '@/app/(frontend)/actions/getSignatureCount' import { normalizeFacebookPixelId } from '@/lib/facebookPixel' -import { getCachedDocumentById, getDocuments } from '@/utilities/documents' +import { getCachedDocumentById, getPetitionIds } from '@/utilities/documents' import { getCachedGlobal } from '@/utilities/globals' import { extractFirstImageFromLexical } from '@/utilities/extractFirstImageFromLexical' import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' @@ -26,11 +26,10 @@ const toAbsoluteUrl = (url: string, siteUrl: string) => : `${stripTrailingSlash(siteUrl)}${url.startsWith('/') ? '' : '/'}${url}` export async function generateStaticParams() { - const payload = await getDocuments('petition') + // Build-time enumeration only needs the ids — skip relationship population. + const ids = await getPetitionIds() - return payload.docs.map((doc) => ({ - id: doc.id, - })) + return ids.map((id) => ({ id })) } export async function generateMetadata({ diff --git a/src/components/campaign/CampaignDashboard.tsx b/src/components/campaign/CampaignDashboard.tsx index bc3d2510..f6cfff94 100644 --- a/src/components/campaign/CampaignDashboard.tsx +++ b/src/components/campaign/CampaignDashboard.tsx @@ -14,7 +14,7 @@ import { Button } from '@/components/ui/button' import { formatElectionNumber } from '@/lib/electionInsights' import type { StaffDashboardView } from '@/utilities/campaignDashboardData' import type { FederalCandidateOption } from '@/utilities/electionCandidateOptions' -import type { MunicipalityMapBundle } from '@/utilities/municipalityMapData' +import type { MunicipalityMapBundle } from '@/utilities/municipalityMapContract' import { formatVoteEstimateEndpointsLabel, voteEstimateScenarioLabels, diff --git a/src/components/campaign/MapFeatureReadout.tsx b/src/components/campaign/MapFeatureReadout.tsx index 7301924f..fa9b0c47 100644 --- a/src/components/campaign/MapFeatureReadout.tsx +++ b/src/components/campaign/MapFeatureReadout.tsx @@ -4,7 +4,7 @@ import type { BahiaMapFeatureInfo } from '@/components/campaign/BahiaMap' import { Button } from '@/components/ui/button' import { formatElectionNumber } from '@/lib/electionInsights' import type { MunicipalityMapNavigation } from '@/utilities/municipalityMapNavigation' -import type { MunicipalityMapScaleMode } from '@/utilities/municipalityMapData' +import type { MunicipalityMapScaleMode } from '@/utilities/municipalityMapContract' type MapFeatureReadoutProps = { feature: BahiaMapFeatureInfo | null diff --git a/src/components/campaign/MunicipalityMapPanel.tsx b/src/components/campaign/MunicipalityMapPanel.tsx index 88e601c2..5b7d4b1b 100644 --- a/src/components/campaign/MunicipalityMapPanel.tsx +++ b/src/components/campaign/MunicipalityMapPanel.tsx @@ -19,14 +19,14 @@ import { computeValidVoteShares, divergingGradientCss } from '@/lib/choroplethCo import { formatElectionNumber } from '@/lib/electionInsights' import type { FederalCandidateOption } from '@/utilities/electionCandidateOptions' import { - PLAZA_MAP_SCALE_MODES, - PLAZA_MAP_YEARS, + MUNICIPALITY_MAP_SCALE_MODES, + MUNICIPALITY_MAP_YEARS, municipalityMapScaleModeLabels, municipalityMapYearLabels, type MunicipalityMapBundle, type MunicipalityMapScaleMode, type MunicipalityMapYear, -} from '@/utilities/municipalityMapData' +} from '@/utilities/municipalityMapContract' import { resolveMunicipalityMapNavigation } from '@/utilities/municipalityMapNavigation' import { DEFAULT_VOTE_ESTIMATE_SCENARIO, @@ -73,7 +73,10 @@ export const MunicipalityMapPanel = ({ return bundle.valuesByYear[String(year)] ?? {} }, [bundle, comparison, comparisonActive, estimateScenario, year]) - const validVotesForYear = bundle.validVotesByYear[String(year)] ?? {} + const validVotesForYear = useMemo( + () => bundle.validVotesByYear[String(year)] ?? {}, + [bundle.validVotesByYear, year], + ) const displayValues = useMemo(() => { if (percentScaleActive) { @@ -179,7 +182,7 @@ export const MunicipalityMapPanel = ({ onChange={(event) => setYear(Number(event.target.value) as MunicipalityMapYear)} className="min-h-11 w-full" > - {PLAZA_MAP_YEARS.map((entry) => ( + {MUNICIPALITY_MAP_YEARS.map((entry) => ( {municipalityMapYearLabels[entry]} @@ -195,7 +198,7 @@ export const MunicipalityMapPanel = ({ onChange={(event) => setScaleMode(event.target.value as MunicipalityMapScaleMode)} className="min-h-11 w-full" > - {PLAZA_MAP_SCALE_MODES.map((mode) => ( + {MUNICIPALITY_MAP_SCALE_MODES.map((mode) => ( {municipalityMapScaleModeLabels[mode]} diff --git a/src/components/campaign/MunicipalityMapPanelDynamic.tsx b/src/components/campaign/MunicipalityMapPanelDynamic.tsx index 21060046..29373254 100644 --- a/src/components/campaign/MunicipalityMapPanelDynamic.tsx +++ b/src/components/campaign/MunicipalityMapPanelDynamic.tsx @@ -3,7 +3,7 @@ import dynamic from 'next/dynamic' import type { FederalCandidateOption } from '@/utilities/electionCandidateOptions' -import type { MunicipalityMapBundle } from '@/utilities/municipalityMapData' +import type { MunicipalityMapBundle } from '@/utilities/municipalityMapContract' const MunicipalityMapPanelLazy = dynamic( () => import('@/components/campaign/MunicipalityMapPanel').then((module) => module.MunicipalityMapPanel), diff --git a/src/lib/bahiaElectionAggregates.ts b/src/lib/bahiaElectionAggregates.ts new file mode 100644 index 00000000..0c12a99c --- /dev/null +++ b/src/lib/bahiaElectionAggregates.ts @@ -0,0 +1,44 @@ +import artifactJson from '@/lib/electionAggregates/bahia-federal-baseline.json' + +/** + * Committed per-municipality election baseline (immutable TSE 2014/2018/2022): + * Solla nominal votes + federal T1 valid votes, pre-cut by the same + * `municipalityCatalog` geography the runtime uses (Salvador split by zone, + * every other municipality whole — Camaçari included). + * + * Generated by `pnpm build:election-aggregates` from the locally seeded TSE + * data — never computed at build or request time (see the script header for + * the rationale). A snapshot test pins artifact keys ↔ catalog slugs. + */ + +export type MunicipalityFederalBaseline = { + /** TSE year (as string) → Solla nominal votes inside the municipality geography. */ + votesByYear: Record + /** TSE year (as string) → federal T1 valid votes inside the municipality geography. */ + validVotesByYear: Record +} + +type FederalBaselineArtifact = { + provenance: string + candidateNumber: number + candidateName: string + years: number[] + municipalities: Record +} + +const artifact = artifactJson as FederalBaselineArtifact + +export const FEDERAL_BASELINE_CANDIDATE_NUMBER = artifact.candidateNumber + +export const federalBaselineMunicipalitySlugs = (): string[] => + Object.keys(artifact.municipalities) + +const EMPTY_BASELINE: MunicipalityFederalBaseline = { votesByYear: {}, validVotesByYear: {} } + +/** + * Baseline for one catalog municipality slug. Unknown slugs return an empty + * baseline (renders as 0 votes) — the artifact↔catalog snapshot test is the + * guard against silent drift. + */ +export const getMunicipalityFederalBaseline = (slug: string): MunicipalityFederalBaseline => + artifact.municipalities[slug] ?? EMPTY_BASELINE diff --git a/src/lib/electionAggregates/bahia-federal-baseline.json b/src/lib/electionAggregates/bahia-federal-baseline.json new file mode 100644 index 00000000..9d3ea22a --- /dev/null +++ b/src/lib/electionAggregates/bahia-federal-baseline.json @@ -0,0 +1,5232 @@ +{ + "provenance": "Derived from TSE open data seeded by pnpm db:seed:tse; regenerate with pnpm build:election-aggregates.", + "candidateNumber": 1313, + "candidateName": "Jorge Solla", + "years": [ + 2014, + 2018, + 2022 + ], + "municipalities": { + "abaira": { + "votesByYear": { + "2014": 306, + "2018": 7, + "2022": 30 + }, + "validVotesByYear": { + "2014": 4086, + "2018": 3963, + "2022": 4642 + } + }, + "abare": { + "votesByYear": { + "2014": 15, + "2018": 26, + "2022": 26 + }, + "validVotesByYear": { + "2014": 8528, + "2018": 8976, + "2022": 11213 + } + }, + "acajutiba": { + "votesByYear": { + "2014": 333, + "2018": 119, + "2022": 24 + }, + "validVotesByYear": { + "2014": 6107, + "2018": 6622, + "2022": 8138 + } + }, + "adustina": { + "votesByYear": { + "2014": 19, + "2018": 76, + "2022": 22 + }, + "validVotesByYear": { + "2014": 6769, + "2018": 7394, + "2022": 8592 + } + }, + "agua-fria": { + "votesByYear": { + "2014": 227, + "2018": 231, + "2022": 76 + }, + "validVotesByYear": { + "2014": 7233, + "2018": 6839, + "2022": 8352 + } + }, + "aiquara": { + "votesByYear": { + "2014": 20, + "2018": 23, + "2022": 12 + }, + "validVotesByYear": { + "2014": 2519, + "2018": 2447, + "2022": 2825 + } + }, + "alagoinhas": { + "votesByYear": { + "2014": 901, + "2018": 620, + "2022": 757 + }, + "validVotesByYear": { + "2014": 28443, + "2018": 63397, + "2022": 72348 + } + }, + "alcobaca": { + "votesByYear": { + "2014": 247, + "2018": 366, + "2022": 238 + }, + "validVotesByYear": { + "2014": 8535, + "2018": 9165, + "2022": 12011 + } + }, + "almadina": { + "votesByYear": { + "2014": 9, + "2018": 8, + "2022": 5 + }, + "validVotesByYear": { + "2014": 2874, + "2018": 2840, + "2022": 3343 + } + }, + "amargosa": { + "votesByYear": { + "2014": 3988, + "2018": 5727, + "2022": 7602 + }, + "validVotesByYear": { + "2014": 14554, + "2018": 15281, + "2022": 19068 + } + }, + "amelia-rodrigues": { + "votesByYear": { + "2014": 0, + "2018": 113, + "2022": 83 + }, + "validVotesByYear": { + "2014": 0, + "2018": 11120, + "2022": 13101 + } + }, + "america-dourada": { + "votesByYear": { + "2014": 23, + "2018": 16, + "2022": 11 + }, + "validVotesByYear": { + "2014": 7387, + "2018": 7770, + "2022": 9267 + } + }, + "anage": { + "votesByYear": { + "2014": 122, + "2018": 51, + "2022": 47 + }, + "validVotesByYear": { + "2014": 11072, + "2018": 10914, + "2022": 13066 + } + }, + "andarai": { + "votesByYear": { + "2014": 542, + "2018": 301, + "2022": 404 + }, + "validVotesByYear": { + "2014": 5560, + "2018": 5924, + "2022": 6696 + } + }, + "andorinha": { + "votesByYear": { + "2014": 25, + "2018": 768, + "2022": 797 + }, + "validVotesByYear": { + "2014": 7526, + "2018": 6234, + "2022": 9033 + } + }, + "angical": { + "votesByYear": { + "2014": 122, + "2018": 392, + "2022": 13 + }, + "validVotesByYear": { + "2014": 7106, + "2018": 8205, + "2022": 9286 + } + }, + "anguera": { + "votesByYear": { + "2014": 0, + "2018": 0, + "2022": 44 + }, + "validVotesByYear": { + "2014": 0, + "2018": 0, + "2022": 6422 + } + }, + "antas": { + "votesByYear": { + "2014": 19, + "2018": 760, + "2022": 46 + }, + "validVotesByYear": { + "2014": 6781, + "2018": 7223, + "2022": 7738 + } + }, + "antonio-cardoso": { + "votesByYear": { + "2014": 29, + "2018": 75, + "2022": 56 + }, + "validVotesByYear": { + "2014": 6685, + "2018": 6498, + "2022": 8279 + } + }, + "antonio-goncalves": { + "votesByYear": { + "2014": 13, + "2018": 23, + "2022": 17 + }, + "validVotesByYear": { + "2014": 4896, + "2018": 4716, + "2022": 5989 + } + }, + "apora": { + "votesByYear": { + "2014": 159, + "2018": 42, + "2022": 30 + }, + "validVotesByYear": { + "2014": 7255, + "2018": 7399, + "2022": 9233 + } + }, + "apuarema": { + "votesByYear": { + "2014": 85, + "2018": 20, + "2022": 10 + }, + "validVotesByYear": { + "2014": 3134, + "2018": 3616, + "2022": 4283 + } + }, + "aracas": { + "votesByYear": { + "2014": 0, + "2018": 38, + "2022": 35 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6101, + "2022": 6864 + } + }, + "aracatu": { + "votesByYear": { + "2014": 15, + "2018": 13, + "2022": 24 + }, + "validVotesByYear": { + "2014": 7646, + "2018": 8164, + "2022": 9101 + } + }, + "araci": { + "votesByYear": { + "2014": 86, + "2018": 94, + "2022": 43 + }, + "validVotesByYear": { + "2014": 22952, + "2018": 22638, + "2022": 26876 + } + }, + "aramari": { + "votesByYear": { + "2014": 90, + "2018": 24, + "2022": 30 + }, + "validVotesByYear": { + "2014": 5496, + "2018": 5774, + "2022": 6444 + } + }, + "arataca": { + "votesByYear": { + "2014": 10, + "2018": 19, + "2022": 8 + }, + "validVotesByYear": { + "2014": 3845, + "2018": 4014, + "2022": 4296 + } + }, + "aratuipe": { + "votesByYear": { + "2014": 96, + "2018": 54, + "2022": 51 + }, + "validVotesByYear": { + "2014": 4024, + "2018": 4676, + "2022": 5907 + } + }, + "aurelino-leal": { + "votesByYear": { + "2014": 81, + "2018": 206, + "2022": 263 + }, + "validVotesByYear": { + "2014": 5213, + "2018": 6142, + "2022": 6660 + } + }, + "baianopolis": { + "votesByYear": { + "2014": 0, + "2018": 35, + "2022": 7 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7527, + "2022": 8944 + } + }, + "baixa-grande": { + "votesByYear": { + "2014": 76, + "2018": 69, + "2022": 114 + }, + "validVotesByYear": { + "2014": 8676, + "2018": 8582, + "2022": 9630 + } + }, + "banzae": { + "votesByYear": { + "2014": 18, + "2018": 20, + "2022": 70 + }, + "validVotesByYear": { + "2014": 5940, + "2018": 6740, + "2022": 7802 + } + }, + "barra": { + "votesByYear": { + "2014": 33, + "2018": 54, + "2022": 285 + }, + "validVotesByYear": { + "2014": 21029, + "2018": 21699, + "2022": 25463 + } + }, + "barra-da-estiva": { + "votesByYear": { + "2014": 167, + "2018": 408, + "2022": 112 + }, + "validVotesByYear": { + "2014": 12127, + "2018": 11723, + "2022": 14721 + } + }, + "barra-do-choca": { + "votesByYear": { + "2014": 510, + "2018": 323, + "2022": 133 + }, + "validVotesByYear": { + "2014": 15236, + "2018": 16648, + "2022": 19584 + } + }, + "barra-do-mendes": { + "votesByYear": { + "2014": 110, + "2018": 72, + "2022": 277 + }, + "validVotesByYear": { + "2014": 5969, + "2018": 6601, + "2022": 8486 + } + }, + "barra-do-rocha": { + "votesByYear": { + "2014": 0, + "2018": 63, + "2022": 88 + }, + "validVotesByYear": { + "2014": 0, + "2018": 2426, + "2022": 2791 + } + }, + "barreiras": { + "votesByYear": { + "2014": 812, + "2018": 1088, + "2022": 288 + }, + "validVotesByYear": { + "2014": 54475, + "2018": 63210, + "2022": 75322 + } + }, + "barro-alto": { + "votesByYear": { + "2014": 47, + "2018": 27, + "2022": 183 + }, + "validVotesByYear": { + "2014": 5618, + "2018": 5768, + "2022": 7726 + } + }, + "barro-preto": { + "votesByYear": { + "2014": 17, + "2018": 13, + "2022": 8 + }, + "validVotesByYear": { + "2014": 3475, + "2018": 3430, + "2022": 4062 + } + }, + "barrocas": { + "votesByYear": { + "2014": 639, + "2018": 1090, + "2022": 656 + }, + "validVotesByYear": { + "2014": 8238, + "2018": 8796, + "2022": 9984 + } + }, + "belmonte": { + "votesByYear": { + "2014": 25, + "2018": 42, + "2022": 50 + }, + "validVotesByYear": { + "2014": 8355, + "2018": 8621, + "2022": 10119 + } + }, + "belo-campo": { + "votesByYear": { + "2014": 305, + "2018": 31, + "2022": 50 + }, + "validVotesByYear": { + "2014": 8091, + "2018": 9269, + "2022": 10868 + } + }, + "biritinga": { + "votesByYear": { + "2014": 23, + "2018": 512, + "2022": 27 + }, + "validVotesByYear": { + "2014": 6826, + "2018": 7727, + "2022": 8540 + } + }, + "boa-nova": { + "votesByYear": { + "2014": 43, + "2018": 34, + "2022": 24 + }, + "validVotesByYear": { + "2014": 6429, + "2018": 6487, + "2022": 7944 + } + }, + "boa-vista-do-tupim": { + "votesByYear": { + "2014": 43, + "2018": 25, + "2022": 44 + }, + "validVotesByYear": { + "2014": 8195, + "2018": 8232, + "2022": 9861 + } + }, + "bom-jesus-da-lapa": { + "votesByYear": { + "2014": 1458, + "2018": 1198, + "2022": 2259 + }, + "validVotesByYear": { + "2014": 24479, + "2018": 24552, + "2022": 33667 + } + }, + "bom-jesus-da-serra": { + "votesByYear": { + "2014": 28, + "2018": 116, + "2022": 94 + }, + "validVotesByYear": { + "2014": 4771, + "2018": 5134, + "2022": 6132 + } + }, + "boninal": { + "votesByYear": { + "2014": 73, + "2018": 58, + "2022": 44 + }, + "validVotesByYear": { + "2014": 5678, + "2018": 5745, + "2022": 7298 + } + }, + "bonito": { + "votesByYear": { + "2014": 17, + "2018": 34, + "2022": 51 + }, + "validVotesByYear": { + "2014": 7262, + "2018": 7744, + "2022": 8779 + } + }, + "boquira": { + "votesByYear": { + "2014": 344, + "2018": 401, + "2022": 435 + }, + "validVotesByYear": { + "2014": 9111, + "2018": 9442, + "2022": 11016 + } + }, + "botupora": { + "votesByYear": { + "2014": 0, + "2018": 32, + "2022": 247 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5632, + "2022": 6379 + } + }, + "brejoes": { + "votesByYear": { + "2014": 27, + "2018": 3160, + "2022": 2207 + }, + "validVotesByYear": { + "2014": 6222, + "2018": 5942, + "2022": 7419 + } + }, + "brejolandia": { + "votesByYear": { + "2014": 24, + "2018": 8, + "2022": 4 + }, + "validVotesByYear": { + "2014": 4470, + "2018": 4172, + "2022": 5744 + } + }, + "brotas-de-macaubas": { + "votesByYear": { + "2014": 19, + "2018": 129, + "2022": 33 + }, + "validVotesByYear": { + "2014": 5559, + "2018": 5467, + "2022": 6496 + } + }, + "brumado": { + "votesByYear": { + "2014": 429, + "2018": 204, + "2022": 167 + }, + "validVotesByYear": { + "2014": 28964, + "2018": 31899, + "2022": 35787 + } + }, + "buerarema": { + "votesByYear": { + "2014": 30, + "2018": 26, + "2022": 65 + }, + "validVotesByYear": { + "2014": 7293, + "2018": 7277, + "2022": 8574 + } + }, + "buritirama": { + "votesByYear": { + "2014": 9, + "2018": 11, + "2022": 21 + }, + "validVotesByYear": { + "2014": 8866, + "2018": 9468, + "2022": 11051 + } + }, + "caatiba": { + "votesByYear": { + "2014": 0, + "2018": 4, + "2022": 11 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4538, + "2022": 4295 + } + }, + "cabaceiras-do-paraguacu": { + "votesByYear": { + "2014": 51, + "2018": 77, + "2022": 399 + }, + "validVotesByYear": { + "2014": 6863, + "2018": 7703, + "2022": 9152 + } + }, + "cachoeira": { + "votesByYear": { + "2014": 509, + "2018": 211, + "2022": 314 + }, + "validVotesByYear": { + "2014": 14744, + "2018": 15994, + "2022": 17699 + } + }, + "cacule": { + "votesByYear": { + "2014": 70, + "2018": 27, + "2022": 17 + }, + "validVotesByYear": { + "2014": 10278, + "2018": 10538, + "2022": 12968 + } + }, + "caem": { + "votesByYear": { + "2014": 15, + "2018": 67, + "2022": 6 + }, + "validVotesByYear": { + "2014": 4847, + "2018": 4789, + "2022": 5611 + } + }, + "caetanos": { + "votesByYear": { + "2014": 0, + "2018": 13, + "2022": 7 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6089, + "2022": 7141 + } + }, + "caetite": { + "votesByYear": { + "2014": 135, + "2018": 205, + "2022": 153 + }, + "validVotesByYear": { + "2014": 23351, + "2018": 24280, + "2022": 29483 + } + }, + "cafarnaum": { + "votesByYear": { + "2014": 70, + "2018": 41, + "2022": 31 + }, + "validVotesByYear": { + "2014": 7630, + "2018": 7936, + "2022": 9694 + } + }, + "cairu": { + "votesByYear": { + "2014": 78, + "2018": 18, + "2022": 32 + }, + "validVotesByYear": { + "2014": 6288, + "2018": 8468, + "2022": 9710 + } + }, + "caldeirao-grande": { + "votesByYear": { + "2014": 47, + "2018": 22, + "2022": 8 + }, + "validVotesByYear": { + "2014": 5316, + "2018": 5992, + "2022": 7194 + } + }, + "camacan": { + "votesByYear": { + "2014": 164, + "2018": 595, + "2022": 329 + }, + "validVotesByYear": { + "2014": 10425, + "2018": 10260, + "2022": 12322 + } + }, + "camacari": { + "votesByYear": { + "2014": 708, + "2018": 524, + "2022": 715 + }, + "validVotesByYear": { + "2014": 96393, + "2018": 70536, + "2022": 132271 + } + }, + "camamu": { + "votesByYear": { + "2014": 78, + "2018": 75, + "2022": 88 + }, + "validVotesByYear": { + "2014": 12141, + "2018": 13369, + "2022": 16232 + } + }, + "campo-alegre-de-lourdes": { + "votesByYear": { + "2014": 24, + "2018": 42, + "2022": 14 + }, + "validVotesByYear": { + "2014": 12731, + "2018": 13208, + "2022": 16672 + } + }, + "campo-formoso": { + "votesByYear": { + "2014": 50, + "2018": 56, + "2022": 47 + }, + "validVotesByYear": { + "2014": 34136, + "2018": 35210, + "2022": 41597 + } + }, + "canapolis": { + "votesByYear": { + "2014": 34, + "2018": 34, + "2022": 11 + }, + "validVotesByYear": { + "2014": 4831, + "2018": 5033, + "2022": 6126 + } + }, + "canarana": { + "votesByYear": { + "2014": 108, + "2018": 53, + "2022": 29 + }, + "validVotesByYear": { + "2014": 10521, + "2018": 10967, + "2022": 14351 + } + }, + "canavieiras": { + "votesByYear": { + "2014": 523, + "2018": 62, + "2022": 47 + }, + "validVotesByYear": { + "2014": 12552, + "2018": 13398, + "2022": 15793 + } + }, + "candeal": { + "votesByYear": { + "2014": 34, + "2018": 43, + "2022": 28 + }, + "validVotesByYear": { + "2014": 4534, + "2018": 4733, + "2022": 5488 + } + }, + "candeias": { + "votesByYear": { + "2014": 299, + "2018": 276, + "2022": 374 + }, + "validVotesByYear": { + "2014": 34855, + "2018": 42684, + "2022": 43766 + } + }, + "candiba": { + "votesByYear": { + "2014": 19, + "2018": 20, + "2022": 17 + }, + "validVotesByYear": { + "2014": 6703, + "2018": 6976, + "2022": 7816 + } + }, + "candido-sales": { + "votesByYear": { + "2014": 0, + "2018": 27, + "2022": 20 + }, + "validVotesByYear": { + "2014": 0, + "2018": 12011, + "2022": 14056 + } + }, + "cansancao": { + "votesByYear": { + "2014": 34, + "2018": 24, + "2022": 34 + }, + "validVotesByYear": { + "2014": 15194, + "2018": 13352, + "2022": 20984 + } + }, + "canudos": { + "votesByYear": { + "2014": 0, + "2018": 15, + "2022": 13 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7886, + "2022": 9369 + } + }, + "capela-do-alto-alegre": { + "votesByYear": { + "2014": 0, + "2018": 29, + "2022": 11 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5394, + "2022": 6461 + } + }, + "capim-grosso": { + "votesByYear": { + "2014": 150, + "2018": 88, + "2022": 39 + }, + "validVotesByYear": { + "2014": 12495, + "2018": 12647, + "2022": 16300 + } + }, + "caraibas": { + "votesByYear": { + "2014": 41, + "2018": 35, + "2022": 25 + }, + "validVotesByYear": { + "2014": 4952, + "2018": 5205, + "2022": 6026 + } + }, + "caravelas": { + "votesByYear": { + "2014": 0, + "2018": 130, + "2022": 222 + }, + "validVotesByYear": { + "2014": 0, + "2018": 8023, + "2022": 10648 + } + }, + "cardeal-da-silva": { + "votesByYear": { + "2014": 136, + "2018": 21, + "2022": 26 + }, + "validVotesByYear": { + "2014": 3635, + "2018": 3897, + "2022": 4717 + } + }, + "carinhanha": { + "votesByYear": { + "2014": 39, + "2018": 14, + "2022": 120 + }, + "validVotesByYear": { + "2014": 11979, + "2018": 10221, + "2022": 16162 + } + }, + "casa-nova": { + "votesByYear": { + "2014": 56, + "2018": 111, + "2022": 50 + }, + "validVotesByYear": { + "2014": 31418, + "2018": 31015, + "2022": 41007 + } + }, + "castro-alves": { + "votesByYear": { + "2014": 1248, + "2018": 1247, + "2022": 1226 + }, + "validVotesByYear": { + "2014": 14110, + "2018": 13182, + "2022": 15570 + } + }, + "catolandia": { + "votesByYear": { + "2014": 15, + "2018": 9, + "2022": 0 + }, + "validVotesByYear": { + "2014": 1921, + "2018": 2460, + "2022": 2950 + } + }, + "catu": { + "votesByYear": { + "2014": 209, + "2018": 293, + "2022": 309 + }, + "validVotesByYear": { + "2014": 22257, + "2018": 22481, + "2022": 27620 + } + }, + "caturama": { + "votesByYear": { + "2014": 35, + "2018": 162, + "2022": 46 + }, + "validVotesByYear": { + "2014": 4037, + "2018": 4288, + "2022": 4831 + } + }, + "central": { + "votesByYear": { + "2014": 336, + "2018": 18, + "2022": 50 + }, + "validVotesByYear": { + "2014": 7924, + "2018": 6033, + "2022": 9630 + } + }, + "chorrocho": { + "votesByYear": { + "2014": 26, + "2018": 46, + "2022": 27 + }, + "validVotesByYear": { + "2014": 5404, + "2018": 5913, + "2022": 6563 + } + }, + "cicero-dantas": { + "votesByYear": { + "2014": 58, + "2018": 107, + "2022": 65 + }, + "validVotesByYear": { + "2014": 14552, + "2018": 15758, + "2022": 18242 + } + }, + "cipo": { + "votesByYear": { + "2014": 112, + "2018": 37, + "2022": 34 + }, + "validVotesByYear": { + "2014": 8292, + "2018": 8340, + "2022": 9761 + } + }, + "coaraci": { + "votesByYear": { + "2014": 138, + "2018": 38, + "2022": 41 + }, + "validVotesByYear": { + "2014": 9894, + "2018": 9713, + "2022": 10926 + } + }, + "cocos": { + "votesByYear": { + "2014": 4, + "2018": 4, + "2022": 16 + }, + "validVotesByYear": { + "2014": 8338, + "2018": 9285, + "2022": 11103 + } + }, + "conceicao-da-feira": { + "votesByYear": { + "2014": 245, + "2018": 66, + "2022": 89 + }, + "validVotesByYear": { + "2014": 9351, + "2018": 11023, + "2022": 12375 + } + }, + "conceicao-do-almeida": { + "votesByYear": { + "2014": 69, + "2018": 45, + "2022": 267 + }, + "validVotesByYear": { + "2014": 8994, + "2018": 8510, + "2022": 10376 + } + }, + "conceicao-do-coite": { + "votesByYear": { + "2014": 531, + "2018": 188, + "2022": 153 + }, + "validVotesByYear": { + "2014": 33567, + "2018": 32634, + "2022": 38386 + } + }, + "conceicao-do-jacuipe": { + "votesByYear": { + "2014": 96, + "2018": 118, + "2022": 113 + }, + "validVotesByYear": { + "2014": 14187, + "2018": 14148, + "2022": 17638 + } + }, + "conde": { + "votesByYear": { + "2014": 92, + "2018": 50, + "2022": 39 + }, + "validVotesByYear": { + "2014": 9137, + "2018": 10213, + "2022": 12569 + } + }, + "condeuba": { + "votesByYear": { + "2014": 878, + "2018": 288, + "2022": 472 + }, + "validVotesByYear": { + "2014": 8310, + "2018": 8086, + "2022": 9923 + } + }, + "contendas-do-sincora": { + "votesByYear": { + "2014": 0, + "2018": 11, + "2022": 8 + }, + "validVotesByYear": { + "2014": 0, + "2018": 2549, + "2022": 3097 + } + }, + "coracao-de-maria": { + "votesByYear": { + "2014": 1134, + "2018": 178, + "2022": 130 + }, + "validVotesByYear": { + "2014": 12481, + "2018": 11836, + "2022": 14422 + } + }, + "cordeiros": { + "votesByYear": { + "2014": 20, + "2018": 6, + "2022": 15 + }, + "validVotesByYear": { + "2014": 3734, + "2018": 3859, + "2022": 4707 + } + }, + "coribe": { + "votesByYear": { + "2014": 5, + "2018": 18, + "2022": 34 + }, + "validVotesByYear": { + "2014": 7798, + "2018": 7380, + "2022": 8769 + } + }, + "coronel-joao-sa": { + "votesByYear": { + "2014": 0, + "2018": 11, + "2022": 71 + }, + "validVotesByYear": { + "2014": 0, + "2018": 8705, + "2022": 10040 + } + }, + "correntina": { + "votesByYear": { + "2014": 18, + "2018": 1048, + "2022": 969 + }, + "validVotesByYear": { + "2014": 14828, + "2018": 14208, + "2022": 18223 + } + }, + "cotegipe": { + "votesByYear": { + "2014": 13, + "2018": 16, + "2022": 12 + }, + "validVotesByYear": { + "2014": 6075, + "2018": 6712, + "2022": 8406 + } + }, + "cravolandia": { + "votesByYear": { + "2014": 0, + "2018": 320, + "2022": 104 + }, + "validVotesByYear": { + "2014": 0, + "2018": 2289, + "2022": 2790 + } + }, + "crisopolis": { + "votesByYear": { + "2014": 67, + "2018": 13, + "2022": 12 + }, + "validVotesByYear": { + "2014": 9011, + "2018": 9944, + "2022": 11170 + } + }, + "cristopolis": { + "votesByYear": { + "2014": 0, + "2018": 12, + "2022": 13 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7475, + "2022": 8692 + } + }, + "cruz-das-almas": { + "votesByYear": { + "2014": 470, + "2018": 230, + "2022": 816 + }, + "validVotesByYear": { + "2014": 28187, + "2018": 30531, + "2022": 34709 + } + }, + "curaca": { + "votesByYear": { + "2014": 58, + "2018": 16, + "2022": 21 + }, + "validVotesByYear": { + "2014": 15497, + "2018": 10768, + "2022": 19210 + } + }, + "dario-meira": { + "votesByYear": { + "2014": 20, + "2018": 18, + "2022": 15 + }, + "validVotesByYear": { + "2014": 4595, + "2018": 4808, + "2022": 5432 + } + }, + "dias-d-avila": { + "votesByYear": { + "2014": 262, + "2018": 650, + "2022": 519 + }, + "validVotesByYear": { + "2014": 26751, + "2018": 28223, + "2022": 34540 + } + }, + "dom-basilio": { + "votesByYear": { + "2014": 31, + "2018": 22, + "2022": 1253 + }, + "validVotesByYear": { + "2014": 5835, + "2018": 6242, + "2022": 7743 + } + }, + "dom-macedo-costa": { + "votesByYear": { + "2014": 0, + "2018": 7, + "2022": 1430 + }, + "validVotesByYear": { + "2014": 0, + "2018": 2573, + "2022": 3296 + } + }, + "elisio-medrado": { + "votesByYear": { + "2014": 62, + "2018": 377, + "2022": 415 + }, + "validVotesByYear": { + "2014": 4460, + "2018": 4096, + "2022": 5159 + } + }, + "encruzilhada": { + "votesByYear": { + "2014": 205, + "2018": 26, + "2022": 20 + }, + "validVotesByYear": { + "2014": 8460, + "2018": 9043, + "2022": 10465 + } + }, + "entre-rios": { + "votesByYear": { + "2014": 79, + "2018": 161, + "2022": 178 + }, + "validVotesByYear": { + "2014": 16115, + "2018": 16934, + "2022": 19433 + } + }, + "erico-cardoso": { + "votesByYear": { + "2014": 32, + "2018": 95, + "2022": 70 + }, + "validVotesByYear": { + "2014": 4671, + "2018": 4241, + "2022": 5628 + } + }, + "esplanada": { + "votesByYear": { + "2014": 115, + "2018": 86, + "2022": 67 + }, + "validVotesByYear": { + "2014": 13259, + "2018": 14666, + "2022": 17308 + } + }, + "euclides-da-cunha": { + "votesByYear": { + "2014": 44, + "2018": 68, + "2022": 57 + }, + "validVotesByYear": { + "2014": 26016, + "2018": 26081, + "2022": 32174 + } + }, + "eunapolis": { + "votesByYear": { + "2014": 703, + "2018": 434, + "2022": 409 + }, + "validVotesByYear": { + "2014": 42112, + "2018": 47764, + "2022": 53978 + } + }, + "fatima": { + "votesByYear": { + "2014": 15, + "2018": 36, + "2022": 11 + }, + "validVotesByYear": { + "2014": 8215, + "2018": 6529, + "2022": 10989 + } + }, + "feira-da-mata": { + "votesByYear": { + "2014": 10, + "2018": 10, + "2022": 3 + }, + "validVotesByYear": { + "2014": 3026, + "2018": 2992, + "2022": 3869 + } + }, + "feira-de-santana": { + "votesByYear": { + "2014": 3995, + "2018": 5081, + "2022": 3074 + }, + "validVotesByYear": { + "2014": 244006, + "2018": 258909, + "2022": 301092 + } + }, + "filadelfia": { + "votesByYear": { + "2014": 32, + "2018": 1145, + "2022": 38 + }, + "validVotesByYear": { + "2014": 8152, + "2018": 6037, + "2022": 10352 + } + }, + "firmino-alves": { + "votesByYear": { + "2014": 6, + "2018": 2, + "2022": 3 + }, + "validVotesByYear": { + "2014": 2594, + "2018": 2877, + "2022": 3438 + } + }, + "floresta-azul": { + "votesByYear": { + "2014": 58, + "2018": 8, + "2022": 19 + }, + "validVotesByYear": { + "2014": 4193, + "2018": 4564, + "2022": 5505 + } + }, + "formosa-do-rio-preto": { + "votesByYear": { + "2014": 50, + "2018": 16, + "2022": 16 + }, + "validVotesByYear": { + "2014": 10715, + "2018": 11183, + "2022": 13790 + } + }, + "gandu": { + "votesByYear": { + "2014": 201, + "2018": 59, + "2022": 193 + }, + "validVotesByYear": { + "2014": 12412, + "2018": 12532, + "2022": 16429 + } + }, + "gaviao": { + "votesByYear": { + "2014": 7, + "2018": 16, + "2022": 9 + }, + "validVotesByYear": { + "2014": 2467, + "2018": 2591, + "2022": 2975 + } + }, + "gentio-do-ouro": { + "votesByYear": { + "2014": 0, + "2018": 13, + "2022": 14 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5868, + "2022": 7041 + } + }, + "gloria": { + "votesByYear": { + "2014": 23, + "2018": 56, + "2022": 47 + }, + "validVotesByYear": { + "2014": 7887, + "2018": 8010, + "2022": 9310 + } + }, + "gongogi": { + "votesByYear": { + "2014": 21, + "2018": 16, + "2022": 20 + }, + "validVotesByYear": { + "2014": 2835, + "2018": 2874, + "2022": 3485 + } + }, + "governador-mangabeira": { + "votesByYear": { + "2014": 2938, + "2018": 3419, + "2022": 2336 + }, + "validVotesByYear": { + "2014": 8969, + "2018": 9959, + "2022": 12300 + } + }, + "guajeru": { + "votesByYear": { + "2014": 6, + "2018": 9, + "2022": 9 + }, + "validVotesByYear": { + "2014": 4060, + "2018": 3864, + "2022": 4518 + } + }, + "guanambi": { + "votesByYear": { + "2014": 1437, + "2018": 280, + "2022": 327 + }, + "validVotesByYear": { + "2014": 37372, + "2018": 42094, + "2022": 48285 + } + }, + "guaratinga": { + "votesByYear": { + "2014": 37, + "2018": 50, + "2022": 66 + }, + "validVotesByYear": { + "2014": 8928, + "2018": 8383, + "2022": 10093 + } + }, + "heliopolis": { + "votesByYear": { + "2014": 0, + "2018": 20, + "2022": 26 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7275, + "2022": 8690 + } + }, + "iacu": { + "votesByYear": { + "2014": 53, + "2018": 139, + "2022": 1557 + }, + "validVotesByYear": { + "2014": 10749, + "2018": 11381, + "2022": 13116 + } + }, + "ibiassuce": { + "votesByYear": { + "2014": 21, + "2018": 10, + "2022": 5 + }, + "validVotesByYear": { + "2014": 5630, + "2018": 5521, + "2022": 6320 + } + }, + "ibicarai": { + "votesByYear": { + "2014": 63, + "2018": 43, + "2022": 49 + }, + "validVotesByYear": { + "2014": 10679, + "2018": 10580, + "2022": 12916 + } + }, + "ibicoara": { + "votesByYear": { + "2014": 26, + "2018": 67, + "2022": 33 + }, + "validVotesByYear": { + "2014": 7798, + "2018": 7558, + "2022": 10665 + } + }, + "ibicui": { + "votesByYear": { + "2014": 41, + "2018": 9, + "2022": 25 + }, + "validVotesByYear": { + "2014": 6453, + "2018": 6571, + "2022": 8105 + } + }, + "ibipeba": { + "votesByYear": { + "2014": 40, + "2018": 15, + "2022": 45 + }, + "validVotesByYear": { + "2014": 7259, + "2018": 8465, + "2022": 9917 + } + }, + "ibipitanga": { + "votesByYear": { + "2014": 58, + "2018": 8, + "2022": 19 + }, + "validVotesByYear": { + "2014": 6986, + "2018": 6856, + "2022": 8694 + } + }, + "ibiquera": { + "votesByYear": { + "2014": 6, + "2018": 1, + "2022": 12 + }, + "validVotesByYear": { + "2014": 2010, + "2018": 2079, + "2022": 2806 + } + }, + "ibirapitanga": { + "votesByYear": { + "2014": 20, + "2018": 72, + "2022": 58 + }, + "validVotesByYear": { + "2014": 8415, + "2018": 7427, + "2022": 9290 + } + }, + "ibirapua": { + "votesByYear": { + "2014": 129, + "2018": 14, + "2022": 83 + }, + "validVotesByYear": { + "2014": 3868, + "2018": 4071, + "2022": 5280 + } + }, + "ibirataia": { + "votesByYear": { + "2014": 66, + "2018": 334, + "2022": 64 + }, + "validVotesByYear": { + "2014": 9098, + "2018": 9002, + "2022": 10143 + } + }, + "ibitiara": { + "votesByYear": { + "2014": 0, + "2018": 17, + "2022": 59 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7130, + "2022": 8239 + } + }, + "ibitita": { + "votesByYear": { + "2014": 0, + "2018": 34, + "2022": 51 + }, + "validVotesByYear": { + "2014": 0, + "2018": 9481, + "2022": 11478 + } + }, + "ibotirama": { + "votesByYear": { + "2014": 245, + "2018": 89, + "2022": 62 + }, + "validVotesByYear": { + "2014": 10018, + "2018": 12362, + "2022": 15084 + } + }, + "ichu": { + "votesByYear": { + "2014": 0, + "2018": 19, + "2022": 11 + }, + "validVotesByYear": { + "2014": 0, + "2018": 3142, + "2022": 4374 + } + }, + "igapora": { + "votesByYear": { + "2014": 0, + "2018": 52, + "2022": 17 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7302, + "2022": 8853 + } + }, + "igrapiuna": { + "votesByYear": { + "2014": 0, + "2018": 51, + "2022": 100 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5365, + "2022": 6006 + } + }, + "iguai": { + "votesByYear": { + "2014": 1415, + "2018": 33, + "2022": 21 + }, + "validVotesByYear": { + "2014": 10250, + "2018": 10598, + "2022": 12206 + } + }, + "ilheus": { + "votesByYear": { + "2014": 1298, + "2018": 1588, + "2022": 1883 + }, + "validVotesByYear": { + "2014": 73918, + "2018": 72337, + "2022": 90290 + } + }, + "inhambupe": { + "votesByYear": { + "2014": 369, + "2018": 628, + "2022": 621 + }, + "validVotesByYear": { + "2014": 14469, + "2018": 14926, + "2022": 17808 + } + }, + "ipecaeta": { + "votesByYear": { + "2014": 13, + "2018": 109, + "2022": 43 + }, + "validVotesByYear": { + "2014": 8014, + "2018": 6040, + "2022": 10127 + } + }, + "ipiau": { + "votesByYear": { + "2014": 672, + "2018": 876, + "2022": 643 + }, + "validVotesByYear": { + "2014": 17781, + "2018": 16849, + "2022": 22680 + } + }, + "ipira": { + "votesByYear": { + "2014": 213, + "2018": 357, + "2022": 581 + }, + "validVotesByYear": { + "2014": 25033, + "2018": 26728, + "2022": 31420 + } + }, + "ipupiara": { + "votesByYear": { + "2014": 11, + "2018": 14, + "2022": 10 + }, + "validVotesByYear": { + "2014": 4808, + "2018": 5100, + "2022": 6198 + } + }, + "irajuba": { + "votesByYear": { + "2014": 19, + "2018": 26, + "2022": 29 + }, + "validVotesByYear": { + "2014": 3264, + "2018": 3288, + "2022": 3671 + } + }, + "iramaia": { + "votesByYear": { + "2014": 0, + "2018": 9, + "2022": 64 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4972, + "2022": 5956 + } + }, + "iraquara": { + "votesByYear": { + "2014": 0, + "2018": 664, + "2022": 399 + }, + "validVotesByYear": { + "2014": 0, + "2018": 10512, + "2022": 13358 + } + }, + "irara": { + "votesByYear": { + "2014": 4113, + "2018": 3187, + "2022": 284 + }, + "validVotesByYear": { + "2014": 13627, + "2018": 13363, + "2022": 16310 + } + }, + "irece": { + "votesByYear": { + "2014": 702, + "2018": 606, + "2022": 780 + }, + "validVotesByYear": { + "2014": 23065, + "2018": 28646, + "2022": 36432 + } + }, + "itabela": { + "votesByYear": { + "2014": 67, + "2018": 74, + "2022": 73 + }, + "validVotesByYear": { + "2014": 11535, + "2018": 12056, + "2022": 14580 + } + }, + "itaberaba": { + "votesByYear": { + "2014": 1008, + "2018": 902, + "2022": 2558 + }, + "validVotesByYear": { + "2014": 25094, + "2018": 25151, + "2022": 33592 + } + }, + "itabuna": { + "votesByYear": { + "2014": 892, + "2018": 1459, + "2022": 1470 + }, + "validVotesByYear": { + "2014": 89048, + "2018": 89217, + "2022": 103067 + } + }, + "itacare": { + "votesByYear": { + "2014": 0, + "2018": 91, + "2022": 1311 + }, + "validVotesByYear": { + "2014": 0, + "2018": 8725, + "2022": 12510 + } + }, + "itaete": { + "votesByYear": { + "2014": 0, + "2018": 38, + "2022": 21 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6295, + "2022": 7251 + } + }, + "itagi": { + "votesByYear": { + "2014": 66, + "2018": 49, + "2022": 18 + }, + "validVotesByYear": { + "2014": 6353, + "2018": 6078, + "2022": 6868 + } + }, + "itagiba": { + "votesByYear": { + "2014": 36, + "2018": 105, + "2022": 38 + }, + "validVotesByYear": { + "2014": 7010, + "2018": 6654, + "2022": 8313 + } + }, + "itagimirim": { + "votesByYear": { + "2014": 13, + "2018": 20, + "2022": 11 + }, + "validVotesByYear": { + "2014": 3289, + "2018": 3704, + "2022": 4023 + } + }, + "itaguacu-da-bahia": { + "votesByYear": { + "2014": 15, + "2018": 19, + "2022": 29 + }, + "validVotesByYear": { + "2014": 6119, + "2018": 6010, + "2022": 7638 + } + }, + "itaju-do-colonia": { + "votesByYear": { + "2014": 5, + "2018": 6, + "2022": 7 + }, + "validVotesByYear": { + "2014": 2801, + "2018": 3165, + "2022": 3779 + } + }, + "itajuipe": { + "votesByYear": { + "2014": 328, + "2018": 105, + "2022": 59 + }, + "validVotesByYear": { + "2014": 9867, + "2018": 9685, + "2022": 11500 + } + }, + "itamaraju": { + "votesByYear": { + "2014": 267, + "2018": 205, + "2022": 656 + }, + "validVotesByYear": { + "2014": 24529, + "2018": 26466, + "2022": 31944 + } + }, + "itamari": { + "votesByYear": { + "2014": 7, + "2018": 23, + "2022": 10 + }, + "validVotesByYear": { + "2014": 4004, + "2018": 2318, + "2022": 4339 + } + }, + "itambe": { + "votesByYear": { + "2014": 92, + "2018": 153, + "2022": 25 + }, + "validVotesByYear": { + "2014": 10948, + "2018": 10501, + "2022": 13402 + } + }, + "itanagra": { + "votesByYear": { + "2014": 10, + "2018": 14, + "2022": 122 + }, + "validVotesByYear": { + "2014": 2847, + "2018": 3483, + "2022": 4129 + } + }, + "itanhem": { + "votesByYear": { + "2014": 252, + "2018": 65, + "2022": 56 + }, + "validVotesByYear": { + "2014": 9123, + "2018": 8824, + "2022": 10123 + } + }, + "itaparica": { + "votesByYear": { + "2014": 123, + "2018": 86, + "2022": 168 + }, + "validVotesByYear": { + "2014": 8962, + "2018": 10982, + "2022": 12730 + } + }, + "itape": { + "votesByYear": { + "2014": 35, + "2018": 47, + "2022": 17 + }, + "validVotesByYear": { + "2014": 4944, + "2018": 5251, + "2022": 5826 + } + }, + "itapebi": { + "votesByYear": { + "2014": 11, + "2018": 17, + "2022": 42 + }, + "validVotesByYear": { + "2014": 4262, + "2018": 4771, + "2022": 5208 + } + }, + "itapetinga": { + "votesByYear": { + "2014": 121, + "2018": 1761, + "2022": 133 + }, + "validVotesByYear": { + "2014": 29262, + "2018": 27666, + "2022": 33415 + } + }, + "itapicuru": { + "votesByYear": { + "2014": 19, + "2018": 35, + "2022": 85 + }, + "validVotesByYear": { + "2014": 12559, + "2018": 12856, + "2022": 15546 + } + }, + "itapitanga": { + "votesByYear": { + "2014": 3, + "2018": 9, + "2022": 10 + }, + "validVotesByYear": { + "2014": 3677, + "2018": 3612, + "2022": 4279 + } + }, + "itaquara": { + "votesByYear": { + "2014": 135, + "2018": 81, + "2022": 50 + }, + "validVotesByYear": { + "2014": 3766, + "2018": 3809, + "2022": 4407 + } + }, + "itarantim": { + "votesByYear": { + "2014": 0, + "2018": 420, + "2022": 20 + }, + "validVotesByYear": { + "2014": 0, + "2018": 8314, + "2022": 9672 + } + }, + "itatim": { + "votesByYear": { + "2014": 56, + "2018": 46, + "2022": 53 + }, + "validVotesByYear": { + "2014": 7744, + "2018": 7511, + "2022": 9080 + } + }, + "itirucu": { + "votesByYear": { + "2014": 1165, + "2018": 1238, + "2022": 1993 + }, + "validVotesByYear": { + "2014": 5234, + "2018": 4893, + "2022": 6105 + } + }, + "itiuba": { + "votesByYear": { + "2014": 146, + "2018": 112, + "2022": 41 + }, + "validVotesByYear": { + "2014": 15923, + "2018": 12883, + "2022": 19497 + } + }, + "itororo": { + "votesByYear": { + "2014": 22, + "2018": 90, + "2022": 31 + }, + "validVotesByYear": { + "2014": 8688, + "2018": 9376, + "2022": 11167 + } + }, + "ituacu": { + "votesByYear": { + "2014": 51, + "2018": 26, + "2022": 18 + }, + "validVotesByYear": { + "2014": 9396, + "2018": 8728, + "2022": 10703 + } + }, + "itubera": { + "votesByYear": { + "2014": 40, + "2018": 70, + "2022": 52 + }, + "validVotesByYear": { + "2014": 11010, + "2018": 10469, + "2022": 11847 + } + }, + "iuiu": { + "votesByYear": { + "2014": 0, + "2018": 7, + "2022": 13 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5560, + "2022": 6277 + } + }, + "jaborandi": { + "votesByYear": { + "2014": 6, + "2018": 48, + "2022": 25 + }, + "validVotesByYear": { + "2014": 4623, + "2018": 5076, + "2022": 6574 + } + }, + "jacaraci": { + "votesByYear": { + "2014": 28, + "2018": 12, + "2022": 23 + }, + "validVotesByYear": { + "2014": 6909, + "2018": 6957, + "2022": 7927 + } + }, + "jacobina": { + "votesByYear": { + "2014": 162, + "2018": 3836, + "2022": 114 + }, + "validVotesByYear": { + "2014": 32396, + "2018": 33576, + "2022": 41199 + } + }, + "jaguaquara": { + "votesByYear": { + "2014": 669, + "2018": 654, + "2022": 627 + }, + "validVotesByYear": { + "2014": 19392, + "2018": 17660, + "2022": 22581 + } + }, + "jaguarari": { + "votesByYear": { + "2014": 38, + "2018": 197, + "2022": 67 + }, + "validVotesByYear": { + "2014": 14023, + "2018": 11895, + "2022": 15154 + } + }, + "jaguaripe": { + "votesByYear": { + "2014": 210, + "2018": 60, + "2022": 192 + }, + "validVotesByYear": { + "2014": 7001, + "2018": 8506, + "2022": 10524 + } + }, + "jandaira": { + "votesByYear": { + "2014": 25, + "2018": 16, + "2022": 7 + }, + "validVotesByYear": { + "2014": 4363, + "2018": 4753, + "2022": 5751 + } + }, + "jequie": { + "votesByYear": { + "2014": 1915, + "2018": 4975, + "2022": 1181 + }, + "validVotesByYear": { + "2014": 68276, + "2018": 70811, + "2022": 82501 + } + }, + "jeremoabo": { + "votesByYear": { + "2014": 65, + "2018": 56, + "2022": 68 + }, + "validVotesByYear": { + "2014": 16407, + "2018": 17206, + "2022": 20692 + } + }, + "jiquirica": { + "votesByYear": { + "2014": 0, + "2018": 117, + "2022": 208 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6396, + "2022": 7771 + } + }, + "jitauna": { + "votesByYear": { + "2014": 0, + "2018": 893, + "2022": 164 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7158, + "2022": 8611 + } + }, + "joao-dourado": { + "votesByYear": { + "2014": 77, + "2018": 41, + "2022": 27 + }, + "validVotesByYear": { + "2014": 10425, + "2018": 11239, + "2022": 13515 + } + }, + "juazeiro": { + "votesByYear": { + "2014": 314, + "2018": 438, + "2022": 742 + }, + "validVotesByYear": { + "2014": 65174, + "2018": 58471, + "2022": 114149 + } + }, + "jucurucu": { + "votesByYear": { + "2014": 27, + "2018": 26, + "2022": 27 + }, + "validVotesByYear": { + "2014": 4853, + "2018": 5004, + "2022": 6011 + } + }, + "jussara": { + "votesByYear": { + "2014": 22, + "2018": 81, + "2022": 40 + }, + "validVotesByYear": { + "2014": 7381, + "2018": 5921, + "2022": 9614 + } + }, + "jussari": { + "votesByYear": { + "2014": 0, + "2018": 23, + "2022": 18 + }, + "validVotesByYear": { + "2014": 0, + "2018": 2734, + "2022": 3200 + } + }, + "jussiape": { + "votesByYear": { + "2014": 12, + "2018": 23, + "2022": 8 + }, + "validVotesByYear": { + "2014": 4136, + "2018": 3868, + "2022": 4731 + } + }, + "lafaiete-coutinho": { + "votesByYear": { + "2014": 0, + "2018": 48, + "2022": 8 + }, + "validVotesByYear": { + "2014": 0, + "2018": 2388, + "2022": 3042 + } + }, + "lagoa-real": { + "votesByYear": { + "2014": 12, + "2018": 16, + "2022": 16 + }, + "validVotesByYear": { + "2014": 6129, + "2018": 6432, + "2022": 7544 + } + }, + "laje": { + "votesByYear": { + "2014": 148, + "2018": 87, + "2022": 1661 + }, + "validVotesByYear": { + "2014": 10934, + "2018": 11667, + "2022": 13935 + } + }, + "lajedao": { + "votesByYear": { + "2014": 7, + "2018": 14, + "2022": 87 + }, + "validVotesByYear": { + "2014": 2798, + "2018": 2933, + "2022": 3066 + } + }, + "lajedinho": { + "votesByYear": { + "2014": 4, + "2018": 3, + "2022": 14 + }, + "validVotesByYear": { + "2014": 1908, + "2018": 2137, + "2022": 2513 + } + }, + "lajedo-do-tabocal": { + "votesByYear": { + "2014": 34, + "2018": 34, + "2022": 69 + }, + "validVotesByYear": { + "2014": 3336, + "2018": 3448, + "2022": 4152 + } + }, + "lamarao": { + "votesByYear": { + "2014": 47, + "2018": 22, + "2022": 47 + }, + "validVotesByYear": { + "2014": 3744, + "2018": 4232, + "2022": 5002 + } + }, + "lapao": { + "votesByYear": { + "2014": 34, + "2018": 24, + "2022": 38 + }, + "validVotesByYear": { + "2014": 13026, + "2018": 14551, + "2022": 16952 + } + }, + "lauro-de-freitas": { + "votesByYear": { + "2014": 563, + "2018": 1478, + "2022": 1929 + }, + "validVotesByYear": { + "2014": 68154, + "2018": 80031, + "2022": 103855 + } + }, + "lencois": { + "votesByYear": { + "2014": 0, + "2018": 31, + "2022": 50 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4318, + "2022": 5527 + } + }, + "licinio-de-almeida": { + "votesByYear": { + "2014": 0, + "2018": 25, + "2022": 19 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5984, + "2022": 6891 + } + }, + "livramento-de-nossa-senhora": { + "votesByYear": { + "2014": 135, + "2018": 940, + "2022": 301 + }, + "validVotesByYear": { + "2014": 19116, + "2018": 19903, + "2022": 24702 + } + }, + "luis-eduardo-magalhaes": { + "votesByYear": { + "2014": 44, + "2018": 99, + "2022": 105 + }, + "validVotesByYear": { + "2014": 23622, + "2018": 32078, + "2022": 48216 + } + }, + "macajuba": { + "votesByYear": { + "2014": 34, + "2018": 15, + "2022": 20 + }, + "validVotesByYear": { + "2014": 4941, + "2018": 5287, + "2022": 6573 + } + }, + "macarani": { + "votesByYear": { + "2014": 30, + "2018": 21, + "2022": 24 + }, + "validVotesByYear": { + "2014": 7504, + "2018": 7092, + "2022": 8386 + } + }, + "macaubas": { + "votesByYear": { + "2014": 355, + "2018": 128, + "2022": 230 + }, + "validVotesByYear": { + "2014": 19299, + "2018": 18416, + "2022": 22190 + } + }, + "macurure": { + "votesByYear": { + "2014": 11, + "2018": 10, + "2022": 9 + }, + "validVotesByYear": { + "2014": 4217, + "2018": 3517, + "2022": 4929 + } + }, + "madre-de-deus": { + "votesByYear": { + "2014": 142, + "2018": 67, + "2022": 339 + }, + "validVotesByYear": { + "2014": 8832, + "2018": 10936, + "2022": 11693 + } + }, + "maetinga": { + "votesByYear": { + "2014": 0, + "2018": 7, + "2022": 57 + }, + "validVotesByYear": { + "2014": 0, + "2018": 3616, + "2022": 4300 + } + }, + "maiquinique": { + "votesByYear": { + "2014": 26, + "2018": 24, + "2022": 11 + }, + "validVotesByYear": { + "2014": 4417, + "2018": 4167, + "2022": 5193 + } + }, + "mairi": { + "votesByYear": { + "2014": 20, + "2018": 42, + "2022": 38 + }, + "validVotesByYear": { + "2014": 7634, + "2018": 7784, + "2022": 9204 + } + }, + "malhada": { + "votesByYear": { + "2014": 16, + "2018": 57, + "2022": 102 + }, + "validVotesByYear": { + "2014": 7415, + "2018": 8018, + "2022": 9722 + } + }, + "malhada-de-pedras": { + "votesByYear": { + "2014": 8, + "2018": 8, + "2022": 1 + }, + "validVotesByYear": { + "2014": 4771, + "2018": 4929, + "2022": 5779 + } + }, + "manoel-vitorino": { + "votesByYear": { + "2014": 120, + "2018": 173, + "2022": 29 + }, + "validVotesByYear": { + "2014": 7227, + "2018": 7876, + "2022": 9004 + } + }, + "mansidao": { + "votesByYear": { + "2014": 37, + "2018": 42, + "2022": 565 + }, + "validVotesByYear": { + "2014": 4874, + "2018": 5595, + "2022": 6709 + } + }, + "maracas": { + "votesByYear": { + "2014": 152, + "2018": 75, + "2022": 123 + }, + "validVotesByYear": { + "2014": 10826, + "2018": 11372, + "2022": 13602 + } + }, + "maragogipe": { + "votesByYear": { + "2014": 0, + "2018": 80, + "2022": 275 + }, + "validVotesByYear": { + "2014": 0, + "2018": 19400, + "2022": 24471 + } + }, + "marau": { + "votesByYear": { + "2014": 0, + "2018": 246, + "2022": 503 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7520, + "2022": 9907 + } + }, + "marcionilio-souza": { + "votesByYear": { + "2014": 75, + "2018": 54, + "2022": 127 + }, + "validVotesByYear": { + "2014": 4291, + "2018": 4384, + "2022": 5227 + } + }, + "mascote": { + "votesByYear": { + "2014": 12, + "2018": 228, + "2022": 199 + }, + "validVotesByYear": { + "2014": 5196, + "2018": 5081, + "2022": 6189 + } + }, + "mata-de-sao-joao": { + "votesByYear": { + "2014": 82, + "2018": 326, + "2022": 177 + }, + "validVotesByYear": { + "2014": 22026, + "2018": 22752, + "2022": 27562 + } + }, + "matina": { + "votesByYear": { + "2014": 181, + "2018": 253, + "2022": 17 + }, + "validVotesByYear": { + "2014": 5099, + "2018": 5578, + "2022": 6836 + } + }, + "medeiros-neto": { + "votesByYear": { + "2014": 28, + "2018": 86, + "2022": 58 + }, + "validVotesByYear": { + "2014": 10743, + "2018": 10669, + "2022": 12912 + } + }, + "miguel-calmon": { + "votesByYear": { + "2014": 141, + "2018": 158, + "2022": 78 + }, + "validVotesByYear": { + "2014": 11693, + "2018": 12251, + "2022": 14018 + } + }, + "milagres": { + "votesByYear": { + "2014": 87, + "2018": 50, + "2022": 198 + }, + "validVotesByYear": { + "2014": 5853, + "2018": 5543, + "2022": 6456 + } + }, + "mirangaba": { + "votesByYear": { + "2014": 35, + "2018": 53, + "2022": 8 + }, + "validVotesByYear": { + "2014": 7102, + "2018": 7426, + "2022": 8874 + } + }, + "mirante": { + "votesByYear": { + "2014": 36, + "2018": 28, + "2022": 8 + }, + "validVotesByYear": { + "2014": 4194, + "2018": 4695, + "2022": 5561 + } + }, + "monte-santo": { + "votesByYear": { + "2014": 85, + "2018": 174, + "2022": 189 + }, + "validVotesByYear": { + "2014": 23608, + "2018": 24103, + "2022": 29585 + } + }, + "morpara": { + "votesByYear": { + "2014": 13, + "2018": 8, + "2022": 10 + }, + "validVotesByYear": { + "2014": 3909, + "2018": 4495, + "2022": 5369 + } + }, + "morro-do-chapeu": { + "votesByYear": { + "2014": 41, + "2018": 37, + "2022": 50 + }, + "validVotesByYear": { + "2014": 14850, + "2018": 13945, + "2022": 18185 + } + }, + "mortugaba": { + "votesByYear": { + "2014": 35, + "2018": 10, + "2022": 46 + }, + "validVotesByYear": { + "2014": 5719, + "2018": 5627, + "2022": 6415 + } + }, + "mucuge": { + "votesByYear": { + "2014": 43, + "2018": 261, + "2022": 35 + }, + "validVotesByYear": { + "2014": 5145, + "2018": 5236, + "2022": 6852 + } + }, + "mucuri": { + "votesByYear": { + "2014": 243, + "2018": 375, + "2022": 83 + }, + "validVotesByYear": { + "2014": 14555, + "2018": 14324, + "2022": 18390 + } + }, + "mulungu-do-morro": { + "votesByYear": { + "2014": 1779, + "2018": 1406, + "2022": 145 + }, + "validVotesByYear": { + "2014": 5807, + "2018": 6542, + "2022": 7834 + } + }, + "mundo-novo": { + "votesByYear": { + "2014": 32, + "2018": 16, + "2022": 31 + }, + "validVotesByYear": { + "2014": 8495, + "2018": 8737, + "2022": 10137 + } + }, + "muniz-ferreira": { + "votesByYear": { + "2014": 0, + "2018": 37, + "2022": 32 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4744, + "2022": 5749 + } + }, + "muquem-de-sao-francisco": { + "votesByYear": { + "2014": 0, + "2018": 9, + "2022": 7 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5444, + "2022": 6978 + } + }, + "muritiba": { + "votesByYear": { + "2014": 114, + "2018": 243, + "2022": 375 + }, + "validVotesByYear": { + "2014": 11889, + "2018": 13273, + "2022": 14728 + } + }, + "mutuipe": { + "votesByYear": { + "2014": 141, + "2018": 775, + "2022": 2816 + }, + "validVotesByYear": { + "2014": 9813, + "2018": 9968, + "2022": 12167 + } + }, + "nazare": { + "votesByYear": { + "2014": 647, + "2018": 569, + "2022": 408 + }, + "validVotesByYear": { + "2014": 11477, + "2018": 13308, + "2022": 15603 + } + }, + "nilo-pecanha": { + "votesByYear": { + "2014": 17, + "2018": 42, + "2022": 58 + }, + "validVotesByYear": { + "2014": 4976, + "2018": 5395, + "2022": 6546 + } + }, + "nordestina": { + "votesByYear": { + "2014": 16, + "2018": 11, + "2022": 18 + }, + "validVotesByYear": { + "2014": 5441, + "2018": 5734, + "2022": 6926 + } + }, + "nova-canaa": { + "votesByYear": { + "2014": 373, + "2018": 37, + "2022": 42 + }, + "validVotesByYear": { + "2014": 7051, + "2018": 6630, + "2022": 8346 + } + }, + "nova-fatima": { + "votesByYear": { + "2014": 0, + "2018": 542, + "2022": 136 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4789, + "2022": 5397 + } + }, + "nova-ibia": { + "votesByYear": { + "2014": 10, + "2018": 8, + "2022": 11 + }, + "validVotesByYear": { + "2014": 2984, + "2018": 3157, + "2022": 3678 + } + }, + "nova-itarana": { + "votesByYear": { + "2014": 7, + "2018": 60, + "2022": 189 + }, + "validVotesByYear": { + "2014": 3188, + "2018": 3602, + "2022": 4372 + } + }, + "nova-redencao": { + "votesByYear": { + "2014": 16, + "2018": 13, + "2022": 15 + }, + "validVotesByYear": { + "2014": 3502, + "2018": 3746, + "2022": 4366 + } + }, + "nova-soure": { + "votesByYear": { + "2014": 23, + "2018": 37, + "2022": 26 + }, + "validVotesByYear": { + "2014": 11290, + "2018": 9583, + "2022": 14310 + } + }, + "nova-vicosa": { + "votesByYear": { + "2014": 405, + "2018": 345, + "2022": 105 + }, + "validVotesByYear": { + "2014": 16003, + "2018": 16052, + "2022": 20667 + } + }, + "novo-horizonte": { + "votesByYear": { + "2014": 0, + "2018": 10, + "2022": 10 + }, + "validVotesByYear": { + "2014": 0, + "2018": 5861, + "2022": 6814 + } + }, + "novo-triunfo": { + "votesByYear": { + "2014": 13, + "2018": 15, + "2022": 7 + }, + "validVotesByYear": { + "2014": 4507, + "2018": 4750, + "2022": 5840 + } + }, + "olindina": { + "votesByYear": { + "2014": 40, + "2018": 32, + "2022": 36 + }, + "validVotesByYear": { + "2014": 10715, + "2018": 11248, + "2022": 13547 + } + }, + "oliveira-dos-brejinhos": { + "votesByYear": { + "2014": 45, + "2018": 43, + "2022": 75 + }, + "validVotesByYear": { + "2014": 9847, + "2018": 8672, + "2022": 11578 + } + }, + "ouricangas": { + "votesByYear": { + "2014": 39, + "2018": 24, + "2022": 25 + }, + "validVotesByYear": { + "2014": 4142, + "2018": 4011, + "2022": 4691 + } + }, + "ourolandia": { + "votesByYear": { + "2014": 69, + "2018": 213, + "2022": 432 + }, + "validVotesByYear": { + "2014": 7471, + "2018": 8156, + "2022": 9955 + } + }, + "palmas-de-monte-alto": { + "votesByYear": { + "2014": 55, + "2018": 44, + "2022": 64 + }, + "validVotesByYear": { + "2014": 10601, + "2018": 11018, + "2022": 12665 + } + }, + "palmeiras": { + "votesByYear": { + "2014": 32, + "2018": 53, + "2022": 36 + }, + "validVotesByYear": { + "2014": 3813, + "2018": 3350, + "2022": 5603 + } + }, + "paramirim": { + "votesByYear": { + "2014": 674, + "2018": 37, + "2022": 48 + }, + "validVotesByYear": { + "2014": 9226, + "2018": 9621, + "2022": 11597 + } + }, + "paratinga": { + "votesByYear": { + "2014": 76, + "2018": 62, + "2022": 27 + }, + "validVotesByYear": { + "2014": 12459, + "2018": 13149, + "2022": 16923 + } + }, + "paripiranga": { + "votesByYear": { + "2014": 83, + "2018": 68, + "2022": 40 + }, + "validVotesByYear": { + "2014": 12838, + "2018": 13200, + "2022": 15702 + } + }, + "pau-brasil": { + "votesByYear": { + "2014": 30, + "2018": 32, + "2022": 23 + }, + "validVotesByYear": { + "2014": 4407, + "2018": 4279, + "2022": 5346 + } + }, + "paulo-afonso": { + "votesByYear": { + "2014": 275, + "2018": 380, + "2022": 435 + }, + "validVotesByYear": { + "2014": 45403, + "2018": 42721, + "2022": 55759 + } + }, + "pe-de-serra": { + "votesByYear": { + "2014": 11, + "2018": 30, + "2022": 14 + }, + "validVotesByYear": { + "2014": 6847, + "2018": 7018, + "2022": 8405 + } + }, + "pedrao": { + "votesByYear": { + "2014": 0, + "2018": 28, + "2022": 9 + }, + "validVotesByYear": { + "2014": 0, + "2018": 3646, + "2022": 4437 + } + }, + "pedro-alexandre": { + "votesByYear": { + "2014": 10, + "2018": 102, + "2022": 7 + }, + "validVotesByYear": { + "2014": 5377, + "2018": 5853, + "2022": 6988 + } + }, + "piata": { + "votesByYear": { + "2014": 27, + "2018": 30, + "2022": 238 + }, + "validVotesByYear": { + "2014": 8928, + "2018": 9111, + "2022": 11201 + } + }, + "pilao-arcado": { + "votesByYear": { + "2014": 45, + "2018": 84, + "2022": 71 + }, + "validVotesByYear": { + "2014": 15045, + "2018": 14639, + "2022": 19947 + } + }, + "pindai": { + "votesByYear": { + "2014": 43, + "2018": 31, + "2022": 19 + }, + "validVotesByYear": { + "2014": 7161, + "2018": 7863, + "2022": 9451 + } + }, + "pindobacu": { + "votesByYear": { + "2014": 0, + "2018": 61, + "2022": 13 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7081, + "2022": 11491 + } + }, + "pintadas": { + "votesByYear": { + "2014": 9, + "2018": 25, + "2022": 21 + }, + "validVotesByYear": { + "2014": 4913, + "2018": 5357, + "2022": 5784 + } + }, + "pirai-do-norte": { + "votesByYear": { + "2014": 0, + "2018": 8, + "2022": 12 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4128, + "2022": 4897 + } + }, + "piripa": { + "votesByYear": { + "2014": 0, + "2018": 5, + "2022": 7 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4884, + "2022": 5903 + } + }, + "piritiba": { + "votesByYear": { + "2014": 31, + "2018": 56, + "2022": 29 + }, + "validVotesByYear": { + "2014": 7946, + "2018": 8189, + "2022": 9970 + } + }, + "planaltino": { + "votesByYear": { + "2014": 39, + "2018": 19, + "2022": 14 + }, + "validVotesByYear": { + "2014": 3897, + "2018": 4076, + "2022": 4818 + } + }, + "planalto": { + "votesByYear": { + "2014": 524, + "2018": 23, + "2022": 36 + }, + "validVotesByYear": { + "2014": 11296, + "2018": 12026, + "2022": 13837 + } + }, + "pocoes": { + "votesByYear": { + "2014": 556, + "2018": 137, + "2022": 135 + }, + "validVotesByYear": { + "2014": 18507, + "2018": 19762, + "2022": 24539 + } + }, + "pojuca": { + "votesByYear": { + "2014": 696, + "2018": 177, + "2022": 207 + }, + "validVotesByYear": { + "2014": 14424, + "2018": 14474, + "2022": 17251 + } + }, + "ponto-novo": { + "votesByYear": { + "2014": 0, + "2018": 80, + "2022": 18 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6994, + "2022": 9496 + } + }, + "porto-seguro": { + "votesByYear": { + "2014": 280, + "2018": 633, + "2022": 639 + }, + "validVotesByYear": { + "2014": 43476, + "2018": 50531, + "2022": 66164 + } + }, + "potiragua": { + "votesByYear": { + "2014": 0, + "2018": 10, + "2022": 11 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4841, + "2022": 5525 + } + }, + "prado": { + "votesByYear": { + "2014": 58, + "2018": 85, + "2022": 62 + }, + "validVotesByYear": { + "2014": 10996, + "2018": 12080, + "2022": 15847 + } + }, + "presidente-dutra": { + "votesByYear": { + "2014": 0, + "2018": 46, + "2022": 28 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7142, + "2022": 8941 + } + }, + "presidente-janio-quadros": { + "votesByYear": { + "2014": 14, + "2018": 11, + "2022": 27 + }, + "validVotesByYear": { + "2014": 5854, + "2018": 6045, + "2022": 7019 + } + }, + "presidente-tancredo-neves": { + "votesByYear": { + "2014": 0, + "2018": 64, + "2022": 76 + }, + "validVotesByYear": { + "2014": 0, + "2018": 11500, + "2022": 14779 + } + }, + "queimadas": { + "votesByYear": { + "2014": 53, + "2018": 62, + "2022": 86 + }, + "validVotesByYear": { + "2014": 10933, + "2018": 11240, + "2022": 13815 + } + }, + "quijingue": { + "votesByYear": { + "2014": 28, + "2018": 37, + "2022": 31 + }, + "validVotesByYear": { + "2014": 13186, + "2018": 12585, + "2022": 15140 + } + }, + "quixabeira": { + "votesByYear": { + "2014": 26, + "2018": 21, + "2022": 15 + }, + "validVotesByYear": { + "2014": 4534, + "2018": 4914, + "2022": 5965 + } + }, + "rafael-jambeiro": { + "votesByYear": { + "2014": 365, + "2018": 77, + "2022": 124 + }, + "validVotesByYear": { + "2014": 9985, + "2018": 9794, + "2022": 11937 + } + }, + "remanso": { + "votesByYear": { + "2014": 111, + "2018": 489, + "2022": 152 + }, + "validVotesByYear": { + "2014": 17973, + "2018": 12431, + "2022": 22376 + } + }, + "retirolandia": { + "votesByYear": { + "2014": 0, + "2018": 346, + "2022": 206 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7935, + "2022": 9080 + } + }, + "riachao-das-neves": { + "votesByYear": { + "2014": 91, + "2018": 102, + "2022": 24 + }, + "validVotesByYear": { + "2014": 10296, + "2018": 11452, + "2022": 13363 + } + }, + "riachao-do-jacuipe": { + "votesByYear": { + "2014": 113, + "2018": 108, + "2022": 97 + }, + "validVotesByYear": { + "2014": 15779, + "2018": 16741, + "2022": 19750 + } + }, + "riacho-de-santana": { + "votesByYear": { + "2014": 82, + "2018": 86, + "2022": 432 + }, + "validVotesByYear": { + "2014": 13715, + "2018": 13133, + "2022": 15876 + } + }, + "ribeira-do-amparo": { + "votesByYear": { + "2014": 28, + "2018": 24, + "2022": 7 + }, + "validVotesByYear": { + "2014": 6690, + "2018": 4632, + "2022": 8468 + } + }, + "ribeira-do-pombal": { + "votesByYear": { + "2014": 561, + "2018": 875, + "2022": 149 + }, + "validVotesByYear": { + "2014": 23624, + "2018": 25918, + "2022": 31603 + } + }, + "ribeirao-do-largo": { + "votesByYear": { + "2014": 31, + "2018": 18, + "2022": 16 + }, + "validVotesByYear": { + "2014": 4703, + "2018": 4946, + "2022": 5649 + } + }, + "rio-de-contas": { + "votesByYear": { + "2014": 35, + "2018": 122, + "2022": 102 + }, + "validVotesByYear": { + "2014": 7127, + "2018": 7091, + "2022": 8579 + } + }, + "rio-do-antonio": { + "votesByYear": { + "2014": 240, + "2018": 56, + "2022": 51 + }, + "validVotesByYear": { + "2014": 6213, + "2018": 6268, + "2022": 7591 + } + }, + "rio-do-pires": { + "votesByYear": { + "2014": 113, + "2018": 140, + "2022": 241 + }, + "validVotesByYear": { + "2014": 5573, + "2018": 5471, + "2022": 6093 + } + }, + "rio-real": { + "votesByYear": { + "2014": 32, + "2018": 56, + "2022": 48 + }, + "validVotesByYear": { + "2014": 16772, + "2018": 16763, + "2022": 19304 + } + }, + "rodelas": { + "votesByYear": { + "2014": 55, + "2018": 61, + "2022": 2 + }, + "validVotesByYear": { + "2014": 4211, + "2018": 4399, + "2022": 5759 + } + }, + "ruy-barbosa": { + "votesByYear": { + "2014": 64, + "2018": 50, + "2022": 55 + }, + "validVotesByYear": { + "2014": 13046, + "2018": 12897, + "2022": 15476 + } + }, + "salinas-da-margarida": { + "votesByYear": { + "2014": 48, + "2018": 66, + "2022": 280 + }, + "validVotesByYear": { + "2014": 6578, + "2018": 7665, + "2022": 9650 + } + }, + "salvador-ze-1": { + "votesByYear": { + "2014": 1127, + "2018": 1130, + "2022": 1298 + }, + "validVotesByYear": { + "2014": 40230, + "2018": 46998, + "2022": 53575 + } + }, + "salvador-ze-2": { + "votesByYear": { + "2014": 903, + "2018": 1069, + "2022": 1199 + }, + "validVotesByYear": { + "2014": 45097, + "2018": 52198, + "2022": 61247 + } + }, + "salvador-ze-3": { + "votesByYear": { + "2014": 1329, + "2018": 1320, + "2022": 1257 + }, + "validVotesByYear": { + "2014": 36509, + "2018": 59100, + "2022": 69090 + } + }, + "salvador-ze-4": { + "votesByYear": { + "2014": 1243, + "2018": 1129, + "2022": 1531 + }, + "validVotesByYear": { + "2014": 58777, + "2018": 66335, + "2022": 76325 + } + }, + "salvador-ze-5": { + "votesByYear": { + "2014": 1209, + "2018": 981, + "2022": 893 + }, + "validVotesByYear": { + "2014": 47504, + "2018": 47818, + "2022": 57013 + } + }, + "salvador-ze-6": { + "votesByYear": { + "2014": 1137, + "2018": 1299, + "2022": 1475 + }, + "validVotesByYear": { + "2014": 44546, + "2018": 56268, + "2022": 64496 + } + }, + "salvador-ze-7": { + "votesByYear": { + "2014": 964, + "2018": 1418, + "2022": 1774 + }, + "validVotesByYear": { + "2014": 36232, + "2018": 53374, + "2022": 63799 + } + }, + "salvador-ze-8": { + "votesByYear": { + "2014": 1805, + "2018": 1441, + "2022": 1455 + }, + "validVotesByYear": { + "2014": 76786, + "2018": 57230, + "2022": 67371 + } + }, + "salvador-ze-9": { + "votesByYear": { + "2014": 824, + "2018": 1083, + "2022": 1265 + }, + "validVotesByYear": { + "2014": 42287, + "2018": 51621, + "2022": 60344 + } + }, + "salvador-ze-10": { + "votesByYear": { + "2014": 2049, + "2018": 1857, + "2022": 2260 + }, + "validVotesByYear": { + "2014": 75970, + "2018": 82372, + "2022": 95719 + } + }, + "salvador-ze-11": { + "votesByYear": { + "2014": 1198, + "2018": 1904, + "2022": 1460 + }, + "validVotesByYear": { + "2014": 39988, + "2018": 63609, + "2022": 75491 + } + }, + "salvador-ze-12": { + "votesByYear": { + "2014": 1100, + "2018": 1283, + "2022": 1436 + }, + "validVotesByYear": { + "2014": 42903, + "2018": 65371, + "2022": 78414 + } + }, + "salvador-ze-13": { + "votesByYear": { + "2014": 1447, + "2018": 1408, + "2022": 1721 + }, + "validVotesByYear": { + "2014": 55891, + "2018": 67841, + "2022": 76839 + } + }, + "salvador-ze-14": { + "votesByYear": { + "2014": 1801, + "2018": 2260, + "2022": 1682 + }, + "validVotesByYear": { + "2014": 52287, + "2018": 60893, + "2022": 72988 + } + }, + "salvador-ze-15": { + "votesByYear": { + "2014": 1312, + "2018": 1050, + "2022": 1289 + }, + "validVotesByYear": { + "2014": 49356, + "2018": 47216, + "2022": 57956 + } + }, + "salvador-ze-16": { + "votesByYear": { + "2014": 1603, + "2018": 1665, + "2022": 1643 + }, + "validVotesByYear": { + "2014": 65915, + "2018": 56490, + "2022": 67117 + } + }, + "salvador-ze-17": { + "votesByYear": { + "2014": 1992, + "2018": 1055, + "2022": 990 + }, + "validVotesByYear": { + "2014": 58268, + "2018": 54490, + "2022": 62509 + } + }, + "salvador-ze-18": { + "votesByYear": { + "2014": 999, + "2018": 910, + "2022": 860 + }, + "validVotesByYear": { + "2014": 44345, + "2018": 40880, + "2022": 47708 + } + }, + "salvador-ze-19": { + "votesByYear": { + "2014": 1709, + "2018": 1483, + "2022": 1776 + }, + "validVotesByYear": { + "2014": 73723, + "2018": 66051, + "2022": 82349 + } + }, + "santa-barbara": { + "votesByYear": { + "2014": 82, + "2018": 2873, + "2022": 1342 + }, + "validVotesByYear": { + "2014": 10892, + "2018": 11059, + "2022": 14059 + } + }, + "santa-brigida": { + "votesByYear": { + "2014": 0, + "2018": 59, + "2022": 33 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7071, + "2022": 8129 + } + }, + "santa-cruz-cabralia": { + "votesByYear": { + "2014": 0, + "2018": 56, + "2022": 68 + }, + "validVotesByYear": { + "2014": 0, + "2018": 10593, + "2022": 14012 + } + }, + "santa-cruz-da-vitoria": { + "votesByYear": { + "2014": 2, + "2018": 2, + "2022": 7 + }, + "validVotesByYear": { + "2014": 2327, + "2018": 2321, + "2022": 3090 + } + }, + "santa-ines": { + "votesByYear": { + "2014": 0, + "2018": 3071, + "2022": 2925 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4936, + "2022": 5333 + } + }, + "santa-luzia": { + "votesByYear": { + "2014": 23, + "2018": 20, + "2022": 16 + }, + "validVotesByYear": { + "2014": 4409, + "2018": 4711, + "2022": 5811 + } + }, + "santa-maria-da-vitoria": { + "votesByYear": { + "2014": 79, + "2018": 114, + "2022": 310 + }, + "validVotesByYear": { + "2014": 17606, + "2018": 16197, + "2022": 21423 + } + }, + "santa-rita-de-cassia": { + "votesByYear": { + "2014": 178, + "2018": 109, + "2022": 70 + }, + "validVotesByYear": { + "2014": 12405, + "2018": 12547, + "2022": 15313 + } + }, + "santa-teresinha": { + "votesByYear": { + "2014": 55, + "2018": 44, + "2022": 91 + }, + "validVotesByYear": { + "2014": 4718, + "2018": 4782, + "2022": 6076 + } + }, + "santaluz": { + "votesByYear": { + "2014": 26, + "2018": 63, + "2022": 90 + }, + "validVotesByYear": { + "2014": 18028, + "2018": 18912, + "2022": 21843 + } + }, + "santana": { + "votesByYear": { + "2014": 27, + "2018": 26, + "2022": 31 + }, + "validVotesByYear": { + "2014": 11828, + "2018": 12110, + "2022": 14635 + } + }, + "santanopolis": { + "votesByYear": { + "2014": 42, + "2018": 46, + "2022": 29 + }, + "validVotesByYear": { + "2014": 5196, + "2018": 5113, + "2022": 6242 + } + }, + "santo-amaro": { + "votesByYear": { + "2014": 562, + "2018": 539, + "2022": 675 + }, + "validVotesByYear": { + "2014": 26524, + "2018": 28074, + "2022": 32345 + } + }, + "santo-antonio-de-jesus": { + "votesByYear": { + "2014": 1655, + "2018": 441, + "2022": 569 + }, + "validVotesByYear": { + "2014": 37702, + "2018": 36438, + "2022": 51564 + } + }, + "santo-estevao": { + "votesByYear": { + "2014": 67, + "2018": 107, + "2022": 638 + }, + "validVotesByYear": { + "2014": 20501, + "2018": 13734, + "2022": 25940 + } + }, + "sao-desiderio": { + "votesByYear": { + "2014": 31, + "2018": 53, + "2022": 70 + }, + "validVotesByYear": { + "2014": 12304, + "2018": 13485, + "2022": 18527 + } + }, + "sao-domingos": { + "votesByYear": { + "2014": 46, + "2018": 14, + "2022": 9 + }, + "validVotesByYear": { + "2014": 4621, + "2018": 5169, + "2022": 5993 + } + }, + "sao-felipe": { + "votesByYear": { + "2014": 119, + "2018": 126, + "2022": 830 + }, + "validVotesByYear": { + "2014": 10146, + "2018": 10404, + "2022": 12149 + } + }, + "sao-felix": { + "votesByYear": { + "2014": 53, + "2018": 82, + "2022": 67 + }, + "validVotesByYear": { + "2014": 6547, + "2018": 6542, + "2022": 7121 + } + }, + "sao-felix-do-coribe": { + "votesByYear": { + "2014": 15, + "2018": 225, + "2022": 518 + }, + "validVotesByYear": { + "2014": 6785, + "2018": 6361, + "2022": 8960 + } + }, + "sao-francisco-do-conde": { + "votesByYear": { + "2014": 3146, + "2018": 2673, + "2022": 1162 + }, + "validVotesByYear": { + "2014": 18381, + "2018": 22136, + "2022": 22584 + } + }, + "sao-gabriel": { + "votesByYear": { + "2014": 59, + "2018": 78, + "2022": 43 + }, + "validVotesByYear": { + "2014": 9704, + "2018": 9811, + "2022": 11980 + } + }, + "sao-goncalo-dos-campos": { + "votesByYear": { + "2014": 1068, + "2018": 721, + "2022": 616 + }, + "validVotesByYear": { + "2014": 15059, + "2018": 18007, + "2022": 19561 + } + }, + "sao-jose-da-vitoria": { + "votesByYear": { + "2014": 10, + "2018": 23, + "2022": 20 + }, + "validVotesByYear": { + "2014": 3014, + "2018": 2719, + "2022": 3439 + } + }, + "sao-jose-do-jacuipe": { + "votesByYear": { + "2014": 162, + "2018": 74, + "2022": 79 + }, + "validVotesByYear": { + "2014": 4337, + "2018": 4553, + "2022": 5711 + } + }, + "sao-miguel-das-matas": { + "votesByYear": { + "2014": 21, + "2018": 74, + "2022": 232 + }, + "validVotesByYear": { + "2014": 5655, + "2018": 5750, + "2022": 6996 + } + }, + "sao-sebastiao-do-passe": { + "votesByYear": { + "2014": 1251, + "2018": 909, + "2022": 226 + }, + "validVotesByYear": { + "2014": 18748, + "2018": 21593, + "2022": 23546 + } + }, + "sapeacu": { + "votesByYear": { + "2014": 0, + "2018": 67, + "2022": 383 + }, + "validVotesByYear": { + "2014": 0, + "2018": 8787, + "2022": 10797 + } + }, + "satiro-dias": { + "votesByYear": { + "2014": 9, + "2018": 46, + "2022": 22 + }, + "validVotesByYear": { + "2014": 7748, + "2018": 7819, + "2022": 9025 + } + }, + "saubara": { + "votesByYear": { + "2014": 38, + "2018": 46, + "2022": 45 + }, + "validVotesByYear": { + "2014": 5260, + "2018": 7550, + "2022": 8833 + } + }, + "saude": { + "votesByYear": { + "2014": 36, + "2018": 42, + "2022": 11 + }, + "validVotesByYear": { + "2014": 4543, + "2018": 4617, + "2022": 6057 + } + }, + "seabra": { + "votesByYear": { + "2014": 2349, + "2018": 1635, + "2022": 2271 + }, + "validVotesByYear": { + "2014": 17448, + "2018": 18738, + "2022": 24145 + } + }, + "sebastiao-laranjeiras": { + "votesByYear": { + "2014": 33, + "2018": 4, + "2022": 11 + }, + "validVotesByYear": { + "2014": 4247, + "2018": 4347, + "2022": 5153 + } + }, + "senhor-do-bonfim": { + "votesByYear": { + "2014": 616, + "2018": 329, + "2022": 347 + }, + "validVotesByYear": { + "2014": 29927, + "2018": 26268, + "2022": 35482 + } + }, + "sento-se": { + "votesByYear": { + "2014": 73, + "2018": 54, + "2022": 80 + }, + "validVotesByYear": { + "2014": 16005, + "2018": 11627, + "2022": 21088 + } + }, + "serra-do-ramalho": { + "votesByYear": { + "2014": 784, + "2018": 231, + "2022": 79 + }, + "validVotesByYear": { + "2014": 12583, + "2018": 13669, + "2022": 17959 + } + }, + "serra-dourada": { + "votesByYear": { + "2014": 31, + "2018": 13, + "2022": 19 + }, + "validVotesByYear": { + "2014": 9143, + "2018": 8626, + "2022": 10280 + } + }, + "serra-preta": { + "votesByYear": { + "2014": 0, + "2018": 0, + "2022": 122 + }, + "validVotesByYear": { + "2014": 0, + "2018": 0, + "2022": 11989 + } + }, + "serrinha": { + "votesByYear": { + "2014": 386, + "2018": 443, + "2022": 360 + }, + "validVotesByYear": { + "2014": 33178, + "2018": 35509, + "2022": 42844 + } + }, + "serrolandia": { + "votesByYear": { + "2014": 0, + "2018": 36, + "2022": 15 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6378, + "2022": 7490 + } + }, + "simoes-filho": { + "votesByYear": { + "2014": 299, + "2018": 325, + "2022": 771 + }, + "validVotesByYear": { + "2014": 45685, + "2018": 45160, + "2022": 58245 + } + }, + "sitio-do-mato": { + "votesByYear": { + "2014": 31, + "2018": 10, + "2022": 71 + }, + "validVotesByYear": { + "2014": 4481, + "2018": 5269, + "2022": 7753 + } + }, + "sitio-do-quinto": { + "votesByYear": { + "2014": 5, + "2018": 37, + "2022": 16 + }, + "validVotesByYear": { + "2014": 5605, + "2018": 5890, + "2022": 7005 + } + }, + "sobradinho": { + "votesByYear": { + "2014": 37, + "2018": 75, + "2022": 73 + }, + "validVotesByYear": { + "2014": 10462, + "2018": 10571, + "2022": 13973 + } + }, + "souto-soares": { + "votesByYear": { + "2014": 53, + "2018": 93, + "2022": 96 + }, + "validVotesByYear": { + "2014": 7619, + "2018": 8857, + "2022": 10263 + } + }, + "tabocas-do-brejo-velho": { + "votesByYear": { + "2014": 27, + "2018": 45, + "2022": 8 + }, + "validVotesByYear": { + "2014": 5526, + "2018": 4468, + "2022": 7305 + } + }, + "tanhacu": { + "votesByYear": { + "2014": 0, + "2018": 17, + "2022": 17 + }, + "validVotesByYear": { + "2014": 0, + "2018": 10990, + "2022": 12645 + } + }, + "tanque-novo": { + "votesByYear": { + "2014": 0, + "2018": 11, + "2022": 13 + }, + "validVotesByYear": { + "2014": 0, + "2018": 7879, + "2022": 10444 + } + }, + "tanquinho": { + "votesByYear": { + "2014": 0, + "2018": 151, + "2022": 22 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4900, + "2022": 5556 + } + }, + "taperoa": { + "votesByYear": { + "2014": 27, + "2018": 32, + "2022": 28 + }, + "validVotesByYear": { + "2014": 6924, + "2018": 7955, + "2022": 9763 + } + }, + "tapiramuta": { + "votesByYear": { + "2014": 9, + "2018": 9, + "2022": 9 + }, + "validVotesByYear": { + "2014": 6011, + "2018": 6194, + "2022": 7444 + } + }, + "teixeira-de-freitas": { + "votesByYear": { + "2014": 1696, + "2018": 2319, + "2022": 767 + }, + "validVotesByYear": { + "2014": 51870, + "2018": 58189, + "2022": 70270 + } + }, + "teodoro-sampaio": { + "votesByYear": { + "2014": 0, + "2018": 75, + "2022": 87 + }, + "validVotesByYear": { + "2014": 0, + "2018": 4087, + "2022": 4694 + } + }, + "teofilandia": { + "votesByYear": { + "2014": 153, + "2018": 133, + "2022": 102 + }, + "validVotesByYear": { + "2014": 9315, + "2018": 9882, + "2022": 11964 + } + }, + "teolandia": { + "votesByYear": { + "2014": 0, + "2018": 20, + "2022": 26 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6424, + "2022": 7875 + } + }, + "terra-nova": { + "votesByYear": { + "2014": 0, + "2018": 31, + "2022": 227 + }, + "validVotesByYear": { + "2014": 0, + "2018": 6117, + "2022": 6743 + } + }, + "tremedal": { + "votesByYear": { + "2014": 313, + "2018": 47, + "2022": 26 + }, + "validVotesByYear": { + "2014": 8413, + "2018": 8492, + "2022": 10157 + } + }, + "tucano": { + "votesByYear": { + "2014": 1105, + "2018": 232, + "2022": 34 + }, + "validVotesByYear": { + "2014": 20854, + "2018": 21734, + "2022": 28950 + } + }, + "uaua": { + "votesByYear": { + "2014": 53, + "2018": 25, + "2022": 48 + }, + "validVotesByYear": { + "2014": 12067, + "2018": 9929, + "2022": 14910 + } + }, + "ubaira": { + "votesByYear": { + "2014": 109, + "2018": 212, + "2022": 640 + }, + "validVotesByYear": { + "2014": 9217, + "2018": 9414, + "2022": 10877 + } + }, + "ubaitaba": { + "votesByYear": { + "2014": 479, + "2018": 648, + "2022": 940 + }, + "validVotesByYear": { + "2014": 8064, + "2018": 8437, + "2022": 9542 + } + }, + "ubata": { + "votesByYear": { + "2014": 99, + "2018": 72, + "2022": 61 + }, + "validVotesByYear": { + "2014": 7231, + "2018": 7503, + "2022": 8235 + } + }, + "uibai": { + "votesByYear": { + "2014": 69, + "2018": 25, + "2022": 27 + }, + "validVotesByYear": { + "2014": 6833, + "2018": 7558, + "2022": 8714 + } + }, + "umburanas": { + "votesByYear": { + "2014": 7, + "2018": 41, + "2022": 26 + }, + "validVotesByYear": { + "2014": 6569, + "2018": 6318, + "2022": 8093 + } + }, + "una": { + "votesByYear": { + "2014": 352, + "2018": 51, + "2022": 46 + }, + "validVotesByYear": { + "2014": 8061, + "2018": 8877, + "2022": 10688 + } + }, + "urandi": { + "votesByYear": { + "2014": 35, + "2018": 14, + "2022": 14 + }, + "validVotesByYear": { + "2014": 7057, + "2018": 7623, + "2022": 9357 + } + }, + "urucuca": { + "votesByYear": { + "2014": 46, + "2018": 71, + "2022": 96 + }, + "validVotesByYear": { + "2014": 10455, + "2018": 10005, + "2022": 12307 + } + }, + "utinga": { + "votesByYear": { + "2014": 24, + "2018": 142, + "2022": 64 + }, + "validVotesByYear": { + "2014": 7553, + "2018": 7570, + "2022": 8685 + } + }, + "valenca": { + "votesByYear": { + "2014": 853, + "2018": 362, + "2022": 430 + }, + "validVotesByYear": { + "2014": 31793, + "2018": 38277, + "2022": 41788 + } + }, + "valente": { + "votesByYear": { + "2014": 111, + "2018": 274, + "2022": 51 + }, + "validVotesByYear": { + "2014": 12220, + "2018": 13055, + "2022": 14249 + } + }, + "varzea-da-roca": { + "votesByYear": { + "2014": 13, + "2018": 13, + "2022": 47 + }, + "validVotesByYear": { + "2014": 5978, + "2018": 6210, + "2022": 7188 + } + }, + "varzea-do-poco": { + "votesByYear": { + "2014": 0, + "2018": 22, + "2022": 11 + }, + "validVotesByYear": { + "2014": 0, + "2018": 3828, + "2022": 4763 + } + }, + "varzea-nova": { + "votesByYear": { + "2014": 19, + "2018": 79, + "2022": 22 + }, + "validVotesByYear": { + "2014": 4959, + "2018": 5206, + "2022": 7019 + } + }, + "varzedo": { + "votesByYear": { + "2014": 40, + "2018": 15, + "2022": 32 + }, + "validVotesByYear": { + "2014": 5028, + "2018": 5398, + "2022": 6635 + } + }, + "vera-cruz": { + "votesByYear": { + "2014": 265, + "2018": 221, + "2022": 1106 + }, + "validVotesByYear": { + "2014": 16295, + "2018": 20100, + "2022": 22951 + } + }, + "vereda": { + "votesByYear": { + "2014": 53, + "2018": 286, + "2022": 39 + }, + "validVotesByYear": { + "2014": 3698, + "2018": 3852, + "2022": 4585 + } + }, + "vitoria-da-conquista": { + "votesByYear": { + "2014": 7596, + "2018": 5021, + "2022": 5005 + }, + "validVotesByYear": { + "2014": 138841, + "2018": 146430, + "2022": 178574 + } + }, + "wagner": { + "votesByYear": { + "2014": 4, + "2018": 15, + "2022": 36 + }, + "validVotesByYear": { + "2014": 4036, + "2018": 3943, + "2022": 4718 + } + }, + "wanderley": { + "votesByYear": { + "2014": 24, + "2018": 72, + "2022": 86 + }, + "validVotesByYear": { + "2014": 5733, + "2018": 5858, + "2022": 7922 + } + }, + "wenceslau-guimaraes": { + "votesByYear": { + "2014": 0, + "2018": 46, + "2022": 131 + }, + "validVotesByYear": { + "2014": 0, + "2018": 9848, + "2022": 11628 + } + }, + "xique-xique": { + "votesByYear": { + "2014": 31, + "2018": 60, + "2022": 170 + }, + "validVotesByYear": { + "2014": 18120, + "2018": 20629, + "2022": 23857 + } + } + } +} diff --git a/src/utilities/campaignDashboardData.ts b/src/utilities/campaignDashboardData.ts index 80a788e7..58937e13 100644 --- a/src/utilities/campaignDashboardData.ts +++ b/src/utilities/campaignDashboardData.ts @@ -1,12 +1,10 @@ import type { Payload } from 'payload' import type { CampaignUser, VotePledge } from '@/payload-types' +import { loadMunicipalityScope } from '@/utilities/campaignMunicipalityScope' import { relationshipId, requireRelationshipId } from '@/utilities/relationship' import type { VoteEstimateScenario } from '@/utilities/voteEstimate' -import { - aggregatePledgesByMunicipality, - rollupMunicipalityStaffVotes, -} from '@/utilities/votePledgeData' +import { rollupMunicipalityStaffVotes } from '@/utilities/votePledgeData' export type StaffDashboardView = { kind: 'staff' @@ -62,24 +60,17 @@ export const getCampaignDashboardData = async ( payload: Payload, user: CampaignUser, ): Promise => { - const municipalities = await payload.find({ - collection: 'municipality', - depth: 0, - limit: 0, - pagination: false, - select: { name: true, slug: true, advisors: true, priority: true, expectedVotes: true }, - where: {}, - user, - overrideAccess: false, - }) + // Request-scoped shared load — the map bundle on the same page reuses it. + const scope = await loadMunicipalityScope(payload, user, {}) + const municipalities = { docs: scope.municipalities } + const pledgeAggregates = scope.pledgeAggregates const municipalityIDs = municipalities.docs.map((municipality) => municipality.id) const municipalityById = new Map( municipalities.docs.map((municipality) => [municipality.id, municipality]), ) // Independent of each other — only depend on municipalityIDs. - const [pledgeAggregates, missingEstimatePledges, recentUpdatesResult] = await Promise.all([ - aggregatePledgesByMunicipality(payload, municipalityIDs), + const [missingEstimatePledges, recentUpdatesResult] = await Promise.all([ municipalityIDs.length ? payload.find({ collection: 'votePledge', diff --git a/src/utilities/campaignMunicipalityScope.ts b/src/utilities/campaignMunicipalityScope.ts new file mode 100644 index 00000000..0c23dd0e --- /dev/null +++ b/src/utilities/campaignMunicipalityScope.ts @@ -0,0 +1,74 @@ +import 'server-only' + +import { cache } from 'react' + +import type { Payload, Where } from 'payload' + +import type { CampaignUser, Municipality } from '@/payload-types' +import { + aggregatePledgesByMunicipality, + type MunicipalityPledgeAggregate, +} from '@/utilities/votePledgeData' + +/** Superset of the fields the dashboard, map, and list overview consume. */ +export type ScopedMunicipalityDoc = Pick< + Municipality, + 'id' | 'name' | 'slug' | 'kind' | 'ibgeCode' | 'advisors' | 'priority' | 'expectedVotes' +> + +export type MunicipalityScope = { + municipalities: ScopedMunicipalityDoc[] + pledgeAggregates: Map +} + +const loadScope = async ( + payload: Payload, + user: CampaignUser, + where: Where, +): Promise => { + const result = await payload.find({ + collection: 'municipality', + depth: 0, + limit: 0, + pagination: false, + select: { + name: true, + slug: true, + kind: true, + ibgeCode: true, + advisors: true, + priority: true, + expectedVotes: true, + }, + where, + user, + overrideAccess: false, + }) + + const municipalities = result.docs as ScopedMunicipalityDoc[] + const pledgeAggregates = await aggregatePledgesByMunicipality( + payload, + municipalities.map((municipality) => municipality.id), + ) + + return { municipalities, pledgeAggregates } +} + +/** + * Request-scoped municipality scope (accessible docs + pledge aggregates), + * deduplicated with React `cache()`: the staff dashboard and the map bundle + * both need the same scope on one request and used to load it twice. + * + * `payload` (module singleton) and `user` (from the request-cached + * `getCampaignUser`) are reference-stable within a request; the `where` clause + * participates via its canonical JSON string. + */ +const loadScopeCached = cache((payload: Payload, user: CampaignUser, whereKey: string) => + loadScope(payload, user, JSON.parse(whereKey) as Where), +) + +export const loadMunicipalityScope = ( + payload: Payload, + user: CampaignUser, + where: Where, +): Promise => loadScopeCached(payload, user, JSON.stringify(where)) diff --git a/src/utilities/documents.ts b/src/utilities/documents.ts index 3ffeb201..8f07e9fb 100644 --- a/src/utilities/documents.ts +++ b/src/utilities/documents.ts @@ -9,6 +9,19 @@ type Collection = keyof Config['collections'] export const getDocuments = (collection: Slug, depth?: number) => getPayload({ config: configPromise }).then((payload) => payload.find({ collection, depth })) +/** Bare petition ids (no relationship population) — for `generateStaticParams`. */ +export const getPetitionIds = async (): Promise => { + const payload = await getPayload({ config: configPromise }) + const result = await payload.find({ + collection: 'petition', + depth: 0, + limit: 0, + pagination: false, + select: {}, + }) + return result.docs.map((doc) => String(doc.id)) +} + export const getDocumentById = ( collection: Slug, id: string | number, diff --git a/src/utilities/electionCache.ts b/src/utilities/electionCache.ts new file mode 100644 index 00000000..3897c198 --- /dev/null +++ b/src/utilities/electionCache.ts @@ -0,0 +1,7 @@ +/** + * Cache tag for every `unstable_cache` entry derived from the immutable TSE + * election collections (2014/2018/2022). The data only changes when the seed + * is re-run (`pnpm db:seed:tse`), so the runbook is: re-seed → `POST + * /api/revalidate?tag=election-tse` (see revalidateRequest.ts allowlist). + */ +export const ELECTION_TSE_CACHE_TAG = 'election-tse' diff --git a/src/utilities/electionCandidateOptions.ts b/src/utilities/electionCandidateOptions.ts index 0228f176..32feb13c 100644 --- a/src/utilities/electionCandidateOptions.ts +++ b/src/utilities/electionCandidateOptions.ts @@ -1,4 +1,6 @@ -import type { Payload } from 'payload' +import configPromise from '@payload-config' +import { unstable_cache } from 'next/cache' +import { getPayload } from 'payload' import { BASELINE_TICKET_2022, @@ -7,6 +9,7 @@ import { } from '@/lib/electionResults' import type { CampaignUser, User } from '@/payload-types' import { assertCanReadElectionData } from '@/utilities/campaignAccess' +import { ELECTION_TSE_CACHE_TAG } from '@/utilities/electionCache' export type FederalCandidateOption = { candidateNumber: number @@ -15,17 +18,32 @@ export type FederalCandidateOption = { totalVotesState: number } +/** Cross-request cached core — immutable 2022 registry, busted with `election-tse`. */ +const loadFederalCandidateOptionsCached = unstable_cache( + async (limit: number): Promise => { + const payload = await getPayload({ config: configPromise }) + return queryFederalCandidateOptions(limit, payload) + }, + ['federal-candidate-options'], + { tags: [ELECTION_TSE_CACHE_TAG] }, +) + /** * Federal-deputy candidates available for comparison (2022 registry ordered by * statewide votes). Solla himself is excluded — he is always the reference. */ -export const loadFederalCandidateOptions = async ( - payload: Payload, +export const loadFederalCandidateOptions = ( user: CampaignUser | User, limit = 60, ): Promise => { assertCanReadElectionData(user) + return loadFederalCandidateOptionsCached(limit) +} +const queryFederalCandidateOptions = async ( + limit: number, + payload: Awaited>, +): Promise => { const result = await payload.find({ collection: 'electionCandidate', where: { diff --git a/src/utilities/municipalityCandidateComparison.ts b/src/utilities/municipalityCandidateComparison.ts index 13836fcc..afd7fdcb 100644 --- a/src/utilities/municipalityCandidateComparison.ts +++ b/src/utilities/municipalityCandidateComparison.ts @@ -1,4 +1,6 @@ -import type { Payload } from 'payload' +import configPromise from '@payload-config' +import { unstable_cache } from 'next/cache' +import { getPayload, type Payload } from 'payload' import { BASELINE_TICKET_2022, @@ -8,6 +10,7 @@ import { } from '@/lib/electionResults' import type { CampaignUser, User } from '@/payload-types' import { assertCanReadElectionData } from '@/utilities/campaignAccess' +import { ELECTION_TSE_CACHE_TAG } from '@/utilities/electionCache' import { municipalityGeographyWhere, type MunicipalityElectionGeography, @@ -28,8 +31,20 @@ export type CandidateComparisonRow = { * inside one municipality geography, across the TSE series years. This is the * coordination's core analysis tool ("com quem comparar nesta Praça?"). */ -export const loadMunicipalityCandidateComparison = async ( - payload: Payload, +/** Cross-request cached core — immutable TSE series, busted with `election-tse`. */ +const loadMunicipalityCandidateComparisonCached = unstable_cache( + async ( + geography: MunicipalityElectionGeography, + candidateNumbers: number[], + ): Promise => { + const payload = await getPayload({ config: configPromise }) + return queryMunicipalityCandidateComparison(payload, geography, candidateNumbers) + }, + ['municipality-candidate-comparison'], + { tags: [ELECTION_TSE_CACHE_TAG] }, +) + +export const loadMunicipalityCandidateComparison = ( user: CampaignUser | User, geography: MunicipalityElectionGeography, compareCandidateNumbers: number[], @@ -43,6 +58,14 @@ export const loadMunicipalityCandidateComparison = async ( .slice(0, MAX_COMPARISON_CANDIDATES), ] + return loadMunicipalityCandidateComparisonCached(geography, candidateNumbers) +} + +const queryMunicipalityCandidateComparison = async ( + payload: Payload, + geography: MunicipalityElectionGeography, + candidateNumbers: number[], +): Promise => { const [votes, registry] = await Promise.all([ payload.find({ collection: 'electionCandidateVote', diff --git a/src/utilities/municipalityElectoralBaseline.ts b/src/utilities/municipalityElectoralBaseline.ts index 5812ef3e..bc0b2491 100644 --- a/src/utilities/municipalityElectoralBaseline.ts +++ b/src/utilities/municipalityElectoralBaseline.ts @@ -1,4 +1,6 @@ -import type { Payload } from 'payload' +import configPromise from '@payload-config' +import { unstable_cache } from 'next/cache' +import { getPayload, type Payload } from 'payload' import { BASELINE_TICKET_2022, @@ -8,6 +10,7 @@ import { } from '@/lib/electionResults' import type { CampaignUser, User } from '@/payload-types' import { assertCanReadElectionData } from '@/utilities/campaignAccess' +import { ELECTION_TSE_CACHE_TAG } from '@/utilities/electionCache' import { municipalityGeographyWhere, type MunicipalityElectionGeography, @@ -35,17 +38,38 @@ export type MunicipalityElectoralBaseline = { } } +/** + * Cross-request cached core — the TSE collections are immutable, so entries + * live until the `election-tse` tag is busted after a re-seed. The geography + * argument is part of the cache key (JSON-serialized by `unstable_cache`). + */ +const loadMunicipalityElectoralBaselineCached = unstable_cache( + async ( + geography: MunicipalityElectionGeography, + ): Promise => { + const payload = await getPayload({ config: configPromise }) + return queryMunicipalityElectoralBaseline(payload, geography) + }, + ['municipality-electoral-baseline'], + { tags: [ELECTION_TSE_CACHE_TAG] }, +) + /** * Candidate (Solla) vote series 2014/2018/2022 + 2022 turnout inside the * municipality geography, plus 2022 president/governor ticket votes. */ -export const loadMunicipalityElectoralBaseline = async ( - payload: Payload, +export const loadMunicipalityElectoralBaseline = ( user: ElectionReader, geography: MunicipalityElectionGeography, ): Promise => { assertCanReadElectionData(user) + return loadMunicipalityElectoralBaselineCached(geography) +} +const queryMunicipalityElectoralBaseline = async ( + payload: Payload, + geography: MunicipalityElectionGeography, +): Promise => { const [candidateVotes, tallies, presidentVotes, governorVotes] = await Promise.all([ payload.find({ collection: 'electionCandidateVote', diff --git a/src/utilities/municipalityMapContract.ts b/src/utilities/municipalityMapContract.ts new file mode 100644 index 00000000..8276e297 --- /dev/null +++ b/src/utilities/municipalityMapContract.ts @@ -0,0 +1,62 @@ +import { HISTORICAL_SERIES_YEARS } from '@/lib/electionResults' +import type { MunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation' +import type { VoteEstimateScenario } from '@/utilities/voteEstimate' + +/** + * Client-safe contract of the municipality map: the bundle shape the server + * assembles and the year/scale vocabulary the client panel renders. Keep this + * module free of Payload/Next-server imports — client components import these + * VALUES, so anything reachable from here ships in the browser bundle + * (`municipalityMapData.ts` is the server-only side). + */ + +export const MUNICIPALITY_MAP_YEARS = [...HISTORICAL_SERIES_YEARS, 2026] as const +export type MunicipalityMapYear = (typeof MUNICIPALITY_MAP_YEARS)[number] + +export type MunicipalityMapScaleMode = 'absolute' | 'percentValid' + +export const MUNICIPALITY_MAP_SCALE_MODES = [ + 'percentValid', + 'absolute', +] as const satisfies readonly MunicipalityMapScaleMode[] + +export const municipalityMapScaleModeLabels: Record = { + absolute: 'Total (votos)', + percentValid: '% dos válidos', +} + +export const municipalityMapYearLabels: Record = { + 2014: '2014 (TSE)', + 2018: '2018 (TSE)', + 2022: '2022 (TSE)', + 2026: '2026 (estimativas)', +} + +export type MunicipalityZoneBreakdownRow = { + slug: string + name: string + votesByYear: Record + votes2026ByScenario: Record +} + +export type MunicipalityMapComparison = { + candidateNumber: number + candidateName: string + /** TSE year (as string) → codarea → (sollaVotes − otherVotes). */ + diffByYear: Record> +} + +export type MunicipalityMapBundle = { + /** year (as string) → codarea → value. 2026 = cenário médio (central). */ + valuesByYear: Record> + /** 2026 totals per estimate scenario (codarea → votes). */ + values2026ByScenario: Record> + /** year (as string) → codarea → votosValidos (federal T1). 2026 reuses 2022. */ + validVotesByYear: Record> + /** IBGE codarea → accessible municipality slugs for map click navigation. */ + municipalitiesByIbgeCode: MunicipalitiesByIbgeCode + /** Zone municipalities in scope (Salvador) with per-year values. */ + zoneBreakdown: MunicipalityZoneBreakdownRow[] + candidateName: string + comparison: MunicipalityMapComparison | null +} diff --git a/src/utilities/municipalityMapData.ts b/src/utilities/municipalityMapData.ts index f56c5ba8..11d91e47 100644 --- a/src/utilities/municipalityMapData.ts +++ b/src/utilities/municipalityMapData.ts @@ -1,18 +1,25 @@ +import 'server-only' + import type { Payload } from 'payload' +import { getMunicipalityFederalBaseline } from '@/lib/bahiaElectionAggregates' import { BASELINE_TICKET_2022, HISTORICAL_SERIES_YEARS } from '@/lib/electionResults' import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog' import type { CampaignUser } from '@/payload-types' +import { loadMunicipalityScope } from '@/utilities/campaignMunicipalityScope' import { municipalityElectionGeography, type MunicipalityElectionGeography, } from '@/utilities/municipalityElectionGeography' import { loadCandidateVotesByCityZone, - loadValidVotesByCityZone, sumVotesForGeography, } from '@/utilities/municipalityElectoralBaseline' -import { buildMunicipalitiesByIbgeCode, type MunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation' +import type { + MunicipalityMapBundle, + MunicipalityMapComparison, +} from '@/utilities/municipalityMapContract' +import { buildMunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation' import { buildMunicipalityListWhere, parseMunicipalityListParams, @@ -25,64 +32,21 @@ import { type VoteEstimateScenario, } from '@/utilities/voteEstimate' import { - aggregatePledgesByMunicipality, emptyMunicipalityPledgeAggregate, resolveMunicipalityStaffVoteTotal, type MunicipalityPledgeAggregate, } from '@/utilities/votePledgeData' export type { MunicipalityMapSlugEntry, MunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation' - -export const PLAZA_MAP_YEARS = [...HISTORICAL_SERIES_YEARS, 2026] as const -export type MunicipalityMapYear = (typeof PLAZA_MAP_YEARS)[number] - -export type MunicipalityMapScaleMode = 'absolute' | 'percentValid' - -export const PLAZA_MAP_SCALE_MODES = [ - 'percentValid', - 'absolute', -] as const satisfies readonly MunicipalityMapScaleMode[] - -export const municipalityMapScaleModeLabels: Record = { - absolute: 'Total (votos)', - percentValid: '% dos válidos', -} - -export const municipalityMapYearLabels: Record = { - 2014: '2014 (TSE)', - 2018: '2018 (TSE)', - 2022: '2022 (TSE)', - 2026: '2026 (estimativas)', -} - -export type MunicipalityZoneBreakdownRow = { - slug: string - name: string - votesByYear: Record - votes2026ByScenario: Record -} - -export type MunicipalityMapComparison = { - candidateNumber: number - candidateName: string - /** TSE year (as string) → codarea → (sollaVotes − otherVotes). */ - diffByYear: Record> -} - -export type MunicipalityMapBundle = { - /** year (as string) → codarea → value. 2026 = cenário médio (central). */ - valuesByYear: Record> - /** 2026 totals per estimate scenario (codarea → votes). */ - values2026ByScenario: Record> - /** year (as string) → codarea → votosValidos (federal T1). 2026 reuses 2022. */ - validVotesByYear: Record> - /** IBGE codarea → accessible municipality slugs for map click navigation. */ - municipalitiesByIbgeCode: MunicipalitiesByIbgeCode - /** Zone municipalities in scope (Salvador/Camaçari) with per-year values. */ - zoneBreakdown: MunicipalityZoneBreakdownRow[] - candidateName: string - comparison: MunicipalityMapComparison | null -} +// Client components import the map vocabulary from the contract module; the +// server side re-exports it so data callers have one import surface. +export type { + MunicipalityMapBundle, + MunicipalityMapComparison, + MunicipalityMapScaleMode, + MunicipalityMapYear, + MunicipalityZoneBreakdownRow, +} from '@/utilities/municipalityMapContract' export type ScopedMunicipality = { id: number @@ -128,25 +92,6 @@ export const scopeMunicipalitiesFromDocs = (docs: ReadonlyArray => { - const result = await payload.find({ - collection: 'municipality', - depth: 0, - limit: 0, - pagination: false, - select: { slug: true, name: true, kind: true, ibgeCode: true, expectedVotes: true }, - where: buildMunicipalityListWhere(state), - user, - overrideAccess: false, - }) - - return scopeMunicipalitiesFromDocs(result.docs) -} - export const buildMunicipalityMapBundleFromMunicipalities = async ( payload: Payload, user: CampaignUser, @@ -159,29 +104,17 @@ export const buildMunicipalityMapBundleFromMunicipalities = async ( const valuesByYear: Record> = {} const validVotesByYear: Record> = {} const zoneVotesBySlug = new Map>() - const sollaVotesByYear = new Map>() - 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) + // Historical years come from the committed artifact (immutable TSE data, + // pre-aggregated per municipality) — zero database work. + for (const year of HISTORICAL_SERIES_YEARS) { const values: Record = {} const validValues: Record = {} for (const municipality of municipalities) { - const votes = sumVotesForGeography(votesByCityZone, municipality.geography) + const baseline = getMunicipalityFederalBaseline(municipality.slug) + const votes = baseline.votesByYear[String(year)] ?? 0 values[municipality.ibgeCode] = (values[municipality.ibgeCode] ?? 0) + votes - const valid = sumVotesForGeography(validByCityZone, municipality.geography) + const valid = baseline.validVotesByYear[String(year)] ?? 0 validValues[municipality.ibgeCode] = (validValues[municipality.ibgeCode] ?? 0) + valid if (municipality.kind === 'zona') { const bySlug = zoneVotesBySlug.get(municipality.slug) ?? {} @@ -269,10 +202,10 @@ export const buildMunicipalityMapBundleFromMunicipalities = async ( year, candidateNumber: compareCandidate, }) - const sollaVotes = sollaVotesByYear.get(year) ?? new Map() const values: Record = {} for (const municipality of municipalities) { - const solla = sumVotesForGeography(sollaVotes, municipality.geography) + const solla = + getMunicipalityFederalBaseline(municipality.slug).votesByYear[String(year)] ?? 0 const other = sumVotesForGeography(otherVotes, municipality.geography) values[municipality.ibgeCode] = (values[municipality.ibgeCode] ?? 0) + (solla - other) } @@ -313,13 +246,15 @@ export const loadMunicipalityMapBundle = async ( if (user.role === 'leader') return null const state = parseMunicipalityListParams(searchParams) - const municipalities = await loadScopedMunicipalities(payload, user, state) + const scope = await loadMunicipalityScope(payload, user, buildMunicipalityListWhere(state)) + const municipalities = scopeMunicipalitiesFromDocs(scope.municipalities) if (municipalities.length === 0) return null - const pledgeAggregates = await aggregatePledgesByMunicipality( + return buildMunicipalityMapBundleFromMunicipalities( payload, - municipalities.map((municipality) => municipality.id), + user, + state, + municipalities, + scope.pledgeAggregates, ) - - return buildMunicipalityMapBundleFromMunicipalities(payload, user, state, municipalities, pledgeAggregates) } diff --git a/src/utilities/municipalityPageData.ts b/src/utilities/municipalityPageData.ts index 0acea564..7ba1ed4b 100644 --- a/src/utilities/municipalityPageData.ts +++ b/src/utilities/municipalityPageData.ts @@ -3,6 +3,7 @@ import type { Payload } from 'payload' import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog' import type { CampaignUser, Municipality } from '@/payload-types' import { isCampaignLeader, isCampaignStaff } from '@/utilities/campaignAccess' +import { loadMunicipalityScope } from '@/utilities/campaignMunicipalityScope' import { buildMunicipalityListWhere, municipalityPageSize, @@ -20,22 +21,10 @@ import { relationshipId } from '@/utilities/relationship' import { loadStateDeputySummaries } from '@/utilities/stateDeputyData' import { type VoteEstimateScenario } from '@/utilities/voteEstimate' import { - aggregatePledgesByMunicipality, rollupMunicipalityStaffVotes, type MunicipalityPledgeAggregate, } from '@/utilities/votePledgeData' -const municipalityListFilteredSelect = { - id: true, - slug: true, - name: true, - kind: true, - ibgeCode: true, - expectedVotes: true, - advisors: true, - priority: true, -} as const - export class MunicipalityNotFoundError extends Error { override name = 'MunicipalityNotFoundError' @@ -79,7 +68,7 @@ export const loadMunicipalityListPageBundle = async ( } } - const [paginatedResult, scopeCount, filteredResult] = await Promise.all([ + const [paginatedResult, scopeCount, staffScope] = await Promise.all([ payload.find({ collection: 'municipality', depth: 0, @@ -97,40 +86,24 @@ export const loadMunicipalityListPageBundle = async ( user, overrideAccess: false, }), - isStaff - ? payload.find({ - collection: 'municipality', - depth: 0, - limit: 0, - pagination: false, - where, - select: municipalityListFilteredSelect, - user, - overrideAccess: false, - }) - : Promise.resolve(null), + // Request-scoped shared load (docs + pledge aggregates in one place). + isStaff ? loadMunicipalityScope(payload, user, where) : Promise.resolve(null), ]) let overview: MunicipalityListOverviewData | null = null let pledgeAggregates = new Map() - if (filteredResult && filteredResult.docs.length > 0) { - pledgeAggregates = await aggregatePledgesByMunicipality( - payload, - filteredResult.docs.map((municipality) => municipality.id), - ) - - if (isStaff) { - const rollup = rollupMunicipalityStaffVotes(filteredResult.docs, pledgeAggregates) - overview = { - municipalityCount: filteredResult.docs.length, - staffVoteTotalByScenario: { ...rollup.staffVoteTotalByScenario }, - pledgeCount: rollup.pledgeCount, - missingEstimateCount: rollup.missingEstimateCount, - withAdvisorCount: filteredResult.docs.filter( - (municipality) => (municipality.advisors ?? []).length > 0, - ).length, - } + if (staffScope && staffScope.municipalities.length > 0) { + pledgeAggregates = staffScope.pledgeAggregates + const rollup = rollupMunicipalityStaffVotes(staffScope.municipalities, pledgeAggregates) + overview = { + municipalityCount: staffScope.municipalities.length, + staffVoteTotalByScenario: { ...rollup.staffVoteTotalByScenario }, + pledgeCount: rollup.pledgeCount, + missingEstimateCount: rollup.missingEstimateCount, + withAdvisorCount: staffScope.municipalities.filter( + (municipality) => (municipality.advisors ?? []).length > 0, + ).length, } } diff --git a/src/utilities/revalidateRequest.ts b/src/utilities/revalidateRequest.ts index ebcff788..10c9e1f9 100644 --- a/src/utilities/revalidateRequest.ts +++ b/src/utilities/revalidateRequest.ts @@ -1,10 +1,15 @@ +import { ELECTION_TSE_CACHE_TAG } from '@/utilities/electionCache' import { getGlobalCacheTag } from '@/utilities/globals' export const REVALIDATE_POSTS_TAG = 'posts' as const export const REVALIDATE_PRIVACY_POLICY_CACHE_TAG = getGlobalCacheTag('privacy-policy') -const ALLOWED_REVALIDATE_TAGS = [REVALIDATE_POSTS_TAG, REVALIDATE_PRIVACY_POLICY_CACHE_TAG] as const +const ALLOWED_REVALIDATE_TAGS = [ + REVALIDATE_POSTS_TAG, + REVALIDATE_PRIVACY_POLICY_CACHE_TAG, + ELECTION_TSE_CACHE_TAG, +] as const export type AllowedRevalidateTag = (typeof ALLOWED_REVALIDATE_TAGS)[number] diff --git a/tests/unit/bahiaElectionAggregates.unit.spec.ts b/tests/unit/bahiaElectionAggregates.unit.spec.ts new file mode 100644 index 00000000..8dac24bd --- /dev/null +++ b/tests/unit/bahiaElectionAggregates.unit.spec.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' + +import { + FEDERAL_BASELINE_CANDIDATE_NUMBER, + federalBaselineMunicipalitySlugs, + getMunicipalityFederalBaseline, +} from '@/lib/bahiaElectionAggregates' +import { BASELINE_TICKET_2022, HISTORICAL_SERIES_YEARS } from '@/lib/electionResults' +import { municipalityCatalog } from '@/lib/municipalityCatalog' + +/** + * Pins the committed election-aggregate artifact to the municipality catalog. + * If either changes shape (new remodel, new TSE scope), regenerate with + * `pnpm build:election-aggregates` — a drift here means the map would render + * zeros silently. + */ +describe('election aggregates artifact', () => { + it('covers exactly the municipality catalog slugs', () => { + const artifactSlugs = new Set(federalBaselineMunicipalitySlugs()) + const catalogSlugs = new Set(municipalityCatalog.map((entry) => entry.slug)) + + expect(artifactSlugs.size).toBe(catalogSlugs.size) + for (const slug of catalogSlugs) { + expect(artifactSlugs.has(slug)).toBe(true) + } + }) + + it('is generated for the baseline ticket candidate', () => { + expect(FEDERAL_BASELINE_CANDIDATE_NUMBER).toBe(BASELINE_TICKET_2022.candidate.candidateNumber) + }) + + it('has every historical year populated with plausible statewide totals', () => { + for (const year of HISTORICAL_SERIES_YEARS) { + let candidateTotal = 0 + let validTotal = 0 + for (const slug of federalBaselineMunicipalitySlugs()) { + const baseline = getMunicipalityFederalBaseline(slug) + const votes = baseline.votesByYear[String(year)] + const valid = baseline.validVotesByYear[String(year)] + expect(votes).toBeTypeOf('number') + expect(valid).toBeTypeOf('number') + expect(votes).toBeGreaterThanOrEqual(0) + expect(valid).toBeGreaterThanOrEqual(votes > 0 ? 1 : 0) + candidateTotal += votes ?? 0 + validTotal += valid ?? 0 + } + // A statewide federal-deputy campaign always lands in the 5-7 digit range. + expect(candidateTotal).toBeGreaterThan(10_000) + expect(validTotal).toBeGreaterThan(candidateTotal) + } + }) + + it('returns an empty baseline for unknown slugs', () => { + const baseline = getMunicipalityFederalBaseline('nao-existe') + expect(baseline.votesByYear).toEqual({}) + expect(baseline.validVotesByYear).toEqual({}) + }) +})