436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become the operational unit; roles renamed to coordinator/advisor/leader with advisors scoped to administered Praças. Leadership is one record per person across many Praças/organizations; vote pledges split leader-declared vs staff-estimated (estimates never serialized to leaders). Adds organizations, demand workflow with staff-only cost/receipts, action-plan results with deputy presence, manual political trend, multi-candidate comparison and the diverging red/white/blue map mode. Migration 20260721_020109_remodel_plazas deliberately resets the campaign vertical in production (public site untouched); test suite ported to the new model (unit 187 / int 306 / e2e 9 green).
49 lines
2.4 KiB
TypeScript
49 lines
2.4 KiB
TypeScript
/** Light muted rose → campaign primary #c51414 */
|
|
export const choroplethGradientStart = { r: 254, g: 226, b: 226 } as const
|
|
export const choroplethGradientEnd = { r: 197, g: 20, b: 20 } as const
|
|
|
|
export const choroplethGradientCss = `linear-gradient(to right, rgb(${choroplethGradientStart.r} ${choroplethGradientStart.g} ${choroplethGradientStart.b}), rgb(${choroplethGradientEnd.r} ${choroplethGradientEnd.g} ${choroplethGradientEnd.b}))`
|
|
|
|
/** Sequential fill for campaign choropleth maps (primary red scale). */
|
|
export const choroplethFillColor = (value: number, max: number): string => {
|
|
if (value <= 0 || max <= 0) return '#f4f4f5'
|
|
|
|
const ratio = Math.min(1, value / max)
|
|
const start = choroplethGradientStart
|
|
const end = choroplethGradientEnd
|
|
const r = Math.round(start.r + (end.r - start.r) * ratio)
|
|
const g = Math.round(start.g + (end.g - start.g) * ratio)
|
|
const b = Math.round(start.b + (end.b - start.b) * ratio)
|
|
return `rgb(${r}, ${g}, ${b})`
|
|
}
|
|
|
|
export const choroplethMaxValue = (values: Record<string, number>): number => {
|
|
const entries = Object.values(values).filter((value) => value > 0)
|
|
return entries.length > 0 ? Math.max(...entries) : 0
|
|
}
|
|
|
|
/**
|
|
* Diverging comparison scale (product decision 2026-07-20): campaign red where
|
|
* Solla leads, white at the tie, blue where the compared candidate leads.
|
|
*/
|
|
export const divergingBlueEnd = { r: 30, g: 64, b: 175 } as const
|
|
|
|
const white = { r: 255, g: 255, b: 255 } as const
|
|
|
|
export const divergingFillColor = (diff: number, maxAbs: number): string => {
|
|
if (maxAbs <= 0 || diff === 0) return `rgb(${white.r}, ${white.g}, ${white.b})`
|
|
|
|
const ratio = Math.min(1, Math.abs(diff) / maxAbs)
|
|
const end = diff > 0 ? choroplethGradientEnd : divergingBlueEnd
|
|
const r = Math.round(white.r + (end.r - white.r) * ratio)
|
|
const g = Math.round(white.g + (end.g - white.g) * ratio)
|
|
const b = Math.round(white.b + (end.b - white.b) * ratio)
|
|
return `rgb(${r}, ${g}, ${b})`
|
|
}
|
|
|
|
export const divergingGradientCss = `linear-gradient(to right, rgb(${divergingBlueEnd.r} ${divergingBlueEnd.g} ${divergingBlueEnd.b}), rgb(255 255 255), rgb(${choroplethGradientEnd.r} ${choroplethGradientEnd.g} ${choroplethGradientEnd.b}))`
|
|
|
|
export const choroplethMaxAbsValue = (values: Record<string, number>): number => {
|
|
const entries = Object.values(values).map((value) => Math.abs(value))
|
|
return entries.length > 0 ? Math.max(...entries) : 0
|
|
}
|