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

64 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 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
}
/** Share of valid votes per geography (0–1). Omits entries with no valid denominator or zero votes. */
export const computeValidVoteShares = (
votes: Record<string, number>,
validVotes: Record<string, number>,
): Record<string, number> => {
const shares: Record<string, number> = {}
for (const [code, voteCount] of Object.entries(votes)) {
const valid = validVotes[code]
if (valid > 0 && voteCount > 0) {
shares[code] = voteCount / valid
}
}
return shares
}