O coropleto do Inicio era quase monocromatico porque 0-100% dos validos comprime uma disputa cujo topo estadual e 4,71%. Tres escalas relativas ao proprio candidato, todas em classes discretas: quantis (novo default), padrao proprio (LQ) e posicao no municipio, ao lado de "% dos validos" (B11) e "Total (votos)". Artefato v3: federalRankByIbgeCode com a colocacao entre todos os candidatos com votos > 0 na geografia (2014/2018/2022), chaveado por ibgeCode porque e o que o mapa desenha. Fica no artefato commitado, nao em query de request, que varreria ~100 mil linhas por ano para um dado imutavel. 534 -> 623 KB (orcamento 700 KB). LQ virou 5 classes nas ancoras do E10, nao diverging: o plano pedia as duas coisas e se contradizia. Vermelho x azul continua significando "Solla x adversario" e segue exclusivo do compare. computeAggregateTerritorialClass classifica um grupo de slugs como um territorio so, somando insumos e rodando o mesmo classificador (LQ e razao; a media de razoes nao e a razao das somas) - o E12 herda o helper. TERRITORIAL_CLASS_ANCHORS mudou para src/lib/territorialClassAnchors.ts para nao arrastar o artefato TSE ao bundle do cliente. Quantis sobre o escopo de acesso do usuario, degradando para 3 classes abaixo de 10 municipios com a legenda declarando a contagem efetiva. Rank indisponivel em 2026 (dado TSE); LQ em 2026 reusa o padrao estadual de 2022. Bolhas opcionais por validos projetados (area proporcional ao valor, cor da classe E10, interactive: false). 417 marcadores medidos antes de otimizar - nao derrubaram o frame, entao o corte para top-K nao foi feito. Sem migration, sem collection, sem server action; bundle client de /campanha byte a byte igual.
236 lines
15 KiB
TypeScript
236 lines
15 KiB
TypeScript
/**
|
||
* E18 — curated documentation of the campaign-intelligence numbers the product
|
||
* derives, rendered by `/campanha/conceitos` and linked from the tooltips that
|
||
* expose each number.
|
||
*
|
||
* Content lives here, in code, next to the formulas it describes (E8:
|
||
* `municipalityPotential.ts` / `goalCoverage.ts`) so a change to a formula and
|
||
* the change to its explanation land in the same review — a Payload collection
|
||
* would let the copy drift silently. Every entry must describe what the code
|
||
* actually computes; when a formula changes, this text changes with it.
|
||
*
|
||
* v1 covers the E8 "conta da cadeira" numbers; E10 added dominância relativa
|
||
* and a classe territorial; B13 added the two map scales that are not just a
|
||
* re-rendering of an existing number (quantis and posição no município — LQ is
|
||
* already documented as dominância relativa). Each later slice of the
|
||
* intelligence program (E11 sugestões, E12 TI, E13 giros, E14 níveis) appends
|
||
* its own entries as part of its own delivery.
|
||
*/
|
||
|
||
export type CampaignConceptId =
|
||
| 'votos-validos-projetados'
|
||
| 'teto-do-campo'
|
||
| 'captura'
|
||
| 'share-intracampo'
|
||
| 'roll-off'
|
||
| 'dominancia-relativa'
|
||
| 'classe-territorial'
|
||
| 'quantis-do-mapa'
|
||
| 'posicao-no-municipio'
|
||
| 'meta'
|
||
| 'cobertura-da-meta'
|
||
|
||
export type CampaignConceptCategoryId = 'base' | 'diagnostico' | 'meta'
|
||
|
||
export type CampaignIntelligenceConcept = {
|
||
id: CampaignConceptId
|
||
categoryID: CampaignConceptCategoryId
|
||
title: string
|
||
/** One sentence: what the number measures. */
|
||
oneLiner: string
|
||
/** How it is calculated, in plain text (never rendered as math notation). */
|
||
formula: string
|
||
/** Hand-written illustration; never computed from live data. */
|
||
example?: string
|
||
/** What decision the number supports — including when it supports none. */
|
||
whyItMatters: string
|
||
/** Where in `/campanha` the number is shown. */
|
||
whereItAppears: string
|
||
}
|
||
|
||
export const CAMPAIGN_CONCEPT_CATEGORIES: ReadonlyArray<{
|
||
id: CampaignConceptCategoryId
|
||
label: string
|
||
}> = [
|
||
{ id: 'base', label: 'Base do cálculo' },
|
||
{ id: 'diagnostico', label: 'Diagnóstico' },
|
||
{ id: 'meta', label: 'Meta e cobertura' },
|
||
]
|
||
|
||
export const campaignIntelligenceConcepts: ReadonlyArray<CampaignIntelligenceConcept> = [
|
||
{
|
||
id: 'votos-validos-projetados',
|
||
categoryID: 'base',
|
||
title: 'Votos válidos projetados',
|
||
oneLiner: 'Quantos votos válidos a disputa de deputado federal deve ter no município em 2026.',
|
||
formula:
|
||
'válidos projetados = (válidos 2014 + válidos 2018 + 2 × válidos 2022) ÷ 4, sobre o 1º turno de deputado federal.',
|
||
example:
|
||
'Um município com 20.000 válidos em 2014, 22.000 em 2018 e 24.000 em 2022 projeta (20.000 + 22.000 + 2 × 24.000) ÷ 4 = 22.500.',
|
||
whyItMatters:
|
||
'É a régua de tamanho do município: entra no teto do campo projetado e, por consequência, na meta sugerida. 2022 pesa o dobro por ser a eleição mais próxima. É uma fórmula fixa e auditável, não uma previsão estatística — o produto não faz modelo de votos.',
|
||
whereItAppears:
|
||
'Não aparece como número na tela; sustenta o teto do campo projetado e a meta sugerida.',
|
||
},
|
||
{
|
||
id: 'teto-do-campo',
|
||
categoryID: 'base',
|
||
title: 'Teto do campo (projetado)',
|
||
oneLiner:
|
||
'Quantos votos o campo (PT e aliados de esquerda) pode alcançar no município em 2026.',
|
||
formula:
|
||
'teto 2022 = votos do presidencial do campo no município (1º turno de 2022). Teto projetado = teto 2022 × (válidos projetados ÷ válidos de 2022).',
|
||
example:
|
||
'Teto de 12.000 votos em 2022 num município que projeta crescer 5% de válidos vira um teto projetado de 12.600.',
|
||
whyItMatters:
|
||
'A disputa majoritária mostra o tamanho real do campo no município — muito maior do que a eleição fragmentada de deputado federal consegue revelar. É o denominador da captura e o peso usado para decompor a meta estadual. Usa só 2022: a majoritária de 2014 e 2018 já foi importada, mas ainda não entra nestas contas.',
|
||
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Teto do campo (proj.)").',
|
||
},
|
||
{
|
||
id: 'captura',
|
||
categoryID: 'diagnostico',
|
||
title: 'Captura (2022)',
|
||
oneLiner: 'Quanto do teto do campo o Jorge Solla efetivamente conquistou no município em 2022.',
|
||
formula: 'captura = votos de Solla para deputado federal em 2022 ÷ teto do campo de 2022.',
|
||
example: '1.200 votos de Solla sobre um teto de 12.000 = 10% de captura.',
|
||
whyItMatters:
|
||
'Separa "município pequeno" de "município onde o campo é grande e a campanha não chegou". Captura baixa com teto alto é espaço a ocupar; captura alta é base a defender. É só diagnóstico: não entra no cálculo da meta, para não premiar o que já está consolidado nem punir onde ainda não se trabalhou.',
|
||
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Captura (2022)").',
|
||
},
|
||
{
|
||
id: 'share-intracampo',
|
||
categoryID: 'diagnostico',
|
||
title: 'Share intracampo',
|
||
oneLiner:
|
||
'Dos votos que o campo recebeu para deputado federal naquele ano, a fração que foi para Jorge Solla.',
|
||
formula:
|
||
'share intracampo = votos de Solla no ano ÷ votos de todos os partidos do campo para deputado federal no mesmo ano.',
|
||
whyItMatters:
|
||
'É a leitura do fogo amigo: o denominador é o próprio campo na mesma disputa, então o número mostra a divisão interna do voto de esquerda no município, ano a ano (2014, 2018, 2022). Diferente da captura, cujo denominador é o teto presidencial de 2022. O campo é uma lista curada de siglas por ano (2022 e 2026: PT, PC do B, PV), não a coligação registrada no TSE — coligações proporcionais juntam aliados sem afinidade só para somar quociente.',
|
||
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Share intracampo").',
|
||
},
|
||
{
|
||
id: 'roll-off',
|
||
categoryID: 'diagnostico',
|
||
title: 'Roll-off (2022)',
|
||
oneLiner:
|
||
'Quanto voto em branco e nulo a disputa de deputado federal atraiu a mais que a presidencial, no mesmo comparecimento.',
|
||
formula:
|
||
'roll-off = (brancos + nulos de deputado federal em 2022) − (brancos + nulos do presidencial em 2022). O percentual mostra esse saldo sobre o comparecimento de deputado federal.',
|
||
whyItMatters:
|
||
'Mede quanto eleitor comparece, vota para presidente e desiste de escolher deputado. Roll-off alto indica eleitorado disponível para quem chegar com nome e razão para votar — mas é diagnóstico de contexto, não um reservatório de votos garantidos. Também usa só 2022.',
|
||
whereItAppears: 'Card "Conta da cadeira", no detalhe do município ("Roll-off (2022)").',
|
||
},
|
||
{
|
||
id: 'dominancia-relativa',
|
||
categoryID: 'diagnostico',
|
||
title: 'Dominância relativa',
|
||
oneLiner:
|
||
'Quanto o desempenho do Jorge Solla no município se afasta do padrão dele no estado inteiro.',
|
||
formula:
|
||
'dominância = (votos de Solla ÷ votos válidos do município, em 2022) ÷ (votos de Solla ÷ votos válidos da Bahia, em 2022). Acima de 1 é acima do padrão dele; abaixo de 1 é abaixo.',
|
||
example:
|
||
'Um município onde ele fez 4% dos válidos, quando o padrão estadual dele é 2%, tem dominância 2 — "aqui rendemos o dobro do nosso normal". Onde ele fez 1,2%, a dominância é 0,6: "aqui rendemos 40% abaixo do nosso padrão".',
|
||
whyItMatters:
|
||
'Em deputado federal, o percentual absoluto engana: 3% dos válidos parece pouco e pode ser o melhor município da campanha. A comparação útil é com o próprio padrão, não com 50% + 1 — que é conta de eleição majoritária. É a leitura que separa "pequeno" de "fraco".',
|
||
whereItAppears:
|
||
'Como o "por quê" da classe, no card "Conta da cadeira", na capa do dossiê e na coluna "Classe" da lista de municípios.',
|
||
},
|
||
{
|
||
id: 'classe-territorial',
|
||
categoryID: 'diagnostico',
|
||
title: 'Classe do município',
|
||
oneLiner: 'Uma palavra para o que o município pede: Reduto, Expansão, Manutenção ou Marginal.',
|
||
formula:
|
||
'Reduto = dominância 2 ou mais. Marginal = dominância abaixo de 0,5 com pouco voto do campo sem captura (abaixo da mediana dos 435 municípios). Expansão = dominância abaixo de 0,5 mas com campo grande sem captura, ou município do bloco que concentra metade da votação dele. Manutenção = o meio, entre 0,5 e 2. Sem base = município sem série do TSE.',
|
||
example:
|
||
'Dominância 0,3 com 9.000 votos do campo sem captura é Expansão: o campo está lá e a campanha não chegou. A mesma dominância com 300 votos do campo disponíveis é Marginal — não compensa a perna.',
|
||
whyItMatters:
|
||
'Responde onde a perna vai na semana: defender reduto dormente ou abrir rede em expansão. A classe é sugestão, não sentença — ela nunca aparece sozinha, sempre com os dois fatores que a produziram, e a mesa pode decidir contra ela. Os cortes exatos (2 e 0,5) são ilustrativos: valem até o backtest contra 2014–2022 calibrá-los.',
|
||
whereItAppears:
|
||
'Card "Conta da cadeira" e capa do dossiê, no detalhe do município, e a coluna "Classe" da lista de municípios (com filtro e ordenação).',
|
||
},
|
||
{
|
||
id: 'quantis-do-mapa',
|
||
categoryID: 'diagnostico',
|
||
title: 'Quantis (escala do mapa)',
|
||
oneLiner:
|
||
'Divide os municípios em cinco faixas com o mesmo número de municípios cada, da menor votação para a maior.',
|
||
formula:
|
||
'Ordena os municípios do seu escopo que tiveram votos e corta a lista em cinco partes iguais em QUANTIDADE de municípios. Cada faixa da legenda mostra o menor e o maior valor real que caíram nela. Com menos de 10 municípios em tela a escala cai para três faixas, e a legenda avisa.',
|
||
example:
|
||
'Com 415 municípios, cada faixa tem cerca de 83. A faixa mais escura pode ir de 267 a 27.264 votos — as distâncias entre faixas não são iguais, o número de municípios é.',
|
||
whyItMatters:
|
||
'Numa disputa de deputado federal, uma escala de 0 a 100% pinta a Bahia inteira de uma cor só: o melhor município dele dá menos de 5% dos válidos de lá. O quantil sempre usa a paleta inteira, então o mapa volta a discriminar. O preço é que a cor é relativa ao conjunto em tela — não compare a cor de uma carteira de assessor com a cor do mapa estadual.',
|
||
whereItAppears: 'Mapa dos Municípios, no Início — é a escala padrão.',
|
||
},
|
||
{
|
||
id: 'posicao-no-municipio',
|
||
categoryID: 'diagnostico',
|
||
title: 'Posição no município',
|
||
oneLiner:
|
||
'Em que lugar o Jorge Solla ficou entre todos os candidatos a deputado federal votados naquele município.',
|
||
formula:
|
||
'Ordena todos os candidatos a deputado federal por votos no município (1º turno do ano escolhido) e lê a colocação dele. Vale para a cidade inteira: em Salvador, as 19 zonas dividem a mesma posição, porque o mapa desenha a cidade e não a zona.',
|
||
example:
|
||
'5.005 votos em Vitória da Conquista o colocaram em 6º entre 663 candidatos votados lá.',
|
||
whyItMatters:
|
||
'É a leitura de competição que falta nas outras escalas: dominância compara ele com ele mesmo, e a posição compara ele com quem disputa a mesma cadeira ali. Ser 1º num município pequeno é uma relação política concreta; ser 40º num município grande diz que o voto está pulverizado. Só existe para anos com resultado do TSE — 2026 não tem posição.',
|
||
whereItAppears: 'Mapa dos Municípios, no Início (escala "Posição no município").',
|
||
},
|
||
{
|
||
id: 'meta',
|
||
categoryID: 'meta',
|
||
title: 'Meta e meta sugerida',
|
||
oneLiner: 'Quantos votos a campanha quer no município no cenário escolhido.',
|
||
formula:
|
||
'meta = estimativa da mesa para o cenário (pessimista, média ou otimista), quando existe; sem estimativa, usa a meta sugerida. Meta sugerida = meta estadual × (teto do campo projetado do município ÷ soma dos tetos projetados dos 435 municípios).',
|
||
example:
|
||
'Com meta estadual de 150.000 votos, um município que responde por 2% da soma dos tetos projetados recebe uma meta sugerida de 3.000.',
|
||
whyItMatters:
|
||
'A estimativa da mesa sempre vence a sugestão: quem conhece o município decide. A meta sugerida existe para que nenhum dos 435 municípios fique sem régua enquanto a mesa não preencher. A decomposição é proporcional só ao teto projetado — captura e share ficam de fora de propósito, como diagnóstico. A meta estadual (e a margem de segurança) é editada em Metas da campanha.',
|
||
whereItAppears:
|
||
'Card "Conta da cadeira" ("Meta usada na conta") e o campo de votos estimados no detalhe e na lista de municípios.',
|
||
},
|
||
{
|
||
id: 'cobertura-da-meta',
|
||
categoryID: 'meta',
|
||
title: 'Cobertura da meta',
|
||
oneLiner:
|
||
'Quanto da meta já está coberto por compromissos de lideranças — não por expectativa da mesa.',
|
||
formula:
|
||
'cobertura = comprometido ÷ meta. Comprometido = soma dos votos das lideranças do município (estimativa do cenário quando existe, senão o número declarado pela liderança). Déficit = meta − comprometido.',
|
||
example:
|
||
'Meta de 3.000 com 1.200 votos comprometidos em lideranças = 40% de cobertura, déficit de 1.800.',
|
||
whyItMatters:
|
||
'É a diferença entre querer e ter: a expectativa da mesa define a meta, mas nunca conta como compromisso — se contasse, todo município com estimativa preenchida apareceria coberto sem uma única liderança por trás. O déficit descoberto é o que ordena onde a campanha precisa de mais rede.',
|
||
whereItAppears:
|
||
'Início (staff), visão geral e coluna "Cobertura da meta" na lista de municípios, e o card "Conta da cadeira" no detalhe.',
|
||
},
|
||
]
|
||
|
||
export const campaignConceptsByCategory = (
|
||
categoryID: CampaignConceptCategoryId,
|
||
): CampaignIntelligenceConcept[] =>
|
||
campaignIntelligenceConcepts.filter((concept) => concept.categoryID === categoryID)
|
||
|
||
export const CAMPAIGN_CONCEPTS_PATH = '/campanha/conceitos'
|
||
|
||
/** Anchor href for a documented concept — typed so a renamed id can't leave a dead link behind. */
|
||
export const campaignConceptHref = (id: CampaignConceptId): string =>
|
||
`${CAMPAIGN_CONCEPTS_PATH}#${id}`
|
||
|
||
/**
|
||
* `oneLiner` indexed by id, built once at module init. Callers (e.g. the B22
|
||
* column-header tooltips) quote the same sentence the glossary page shows
|
||
* instead of writing a second, driftable one. The cast is the single place
|
||
* that assumes every `CampaignConceptId` has a matching concept — the
|
||
* "unique ids" test above pins that assumption for the whole union, so
|
||
* `campaignConceptOneLiner` itself stays a plain, assertion-free lookup.
|
||
*/
|
||
const campaignConceptOneLinerByID = Object.fromEntries(
|
||
campaignIntelligenceConcepts.map((concept) => [concept.id, concept.oneLiner]),
|
||
) as Record<CampaignConceptId, string>
|
||
|
||
export const campaignConceptOneLiner = (id: CampaignConceptId): string =>
|
||
campaignConceptOneLinerByID[id]
|