A validated WAGF region palette, where the one-palette gate requires it

Groundwork for the no-selection chooser: for the seven regions to read AS
regions the moment the map loads, pins have to be colored by region, and no
existing strategy does that (_registry.js and religion.js both reference a
doxa-region.js that is not in this tree).

PLACEMENT IS NOT A PREFERENCE HERE. check:one-palette requires every hex a
consumer paints with to come from a strategy module in library/colors/,
discovered by _registry.js. That gate exists because a dashboard composable
once declared PRIVATE palettes the map had never seen — two surfaces then
disagreed about what a color meant, and one keyed color by ARRAY INDEX over a
count-sorted list, so a religion CHANGED COLOR as you filtered. A lookup table
inside my component is the exact shape that caused it.

COLOR FOLLOWS THE ENTITY, NEVER ITS RANK: the seven slots are assigned in a
fixed ALPHABETICAL order of the region's own value, deliberately not by size.
Asia has 1309 people groups today and Oceania 24; ordering by count would mean
a cross-filter that moves the counts repaints the survivors. Alphabetical is
arbitrary but STABLE, which is the property that matters.

THE PALETTE WAS VALIDATED, NOT EYEBALLED. Run through the dataviz validator and
re-stepped twice until every check passed with NO warning: lightness band PASS ·
chroma floor PASS (an earlier teal #0e7490 FAILED at 0.094, reading gray) · CVD
separation PASS at dE 8.6 deutan for the worst adjacent pair (6–8 is a floor
band that would have been legal only with secondary encoding) · normal-vision
floor PASS at dE 30.6 · contrast vs surface PASS, all seven >= 3:1 (an earlier
amber #f59e0b WARNed at 2.09:1, and the guidance is explicit that a contrast
warning is not dismissable).

Two notes on running that validator, because both cost time. Copied to a new
filename it silently does nothing and exits 0 — its CLI guard requires
process.argv[1] to end in "validate_palette.js", so a rename turns it into a
no-op that looks like a pass. And it is an ES module, so it needs a directory
with {"type":"module"} rather than a bare `node file.js`.

The hex ORDER in the file IS the validated order: the CVD check measures
ADJACENT pairs, so the legend must render regions in that same fixed order for
the result to keep holding. Noted in the file so a future reorder re-validates.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 02:24:08 +00:00
parent b9f6ecf12e
commit 3b6cfc69b5

View file

@ -0,0 +1,94 @@
/**
* WAGF region color strategy — colors pins by the region they belong to.
*
* WHY THIS EXISTS. The regions element's no-selection state is a CHOOSER: the Driver's brief was
* "some type of map that would show the regions and countries and let you select one". For the
* seven regions to read AS regions the moment the map loads, the pins have to be colored by
* region — which is a categorical encoding no existing strategy provides. (`_registry.js` and
* `religion.js` both mention a `doxa-region.js`; it does not exist in this tree.)
*
* WHY IT LIVES IN library/colors/ AND NOT IN THE PLUGIN. Not a preference — check:one-palette
* requires it. That gate exists because a dashboard composable once declared PRIVATE palettes the
* map had never seen, and two surfaces then disagreed about what a color meant; worse, one keyed
* color by ARRAY INDEX over a count-sorted list, so a religion CHANGED COLOR as you filtered.
* Every hex a consumer paints with must come from a strategy module here, discovered by
* `_registry.js`. A lookup table inside the component is the exact shape that caused the bug.
*
* COLOR FOLLOWS THE ENTITY, NEVER ITS RANK. The seven slots below are assigned in a FIXED
* ALPHABETICAL ORDER of the region's own `value`, deliberately NOT in count order: Asia has 1309
* people groups and Oceania 24 today, and if the palette were ordered by size then a cross-filter
* that changed the counts would repaint the survivors. Alphabetical is arbitrary but STABLE, which
* is the property that matters.
*
* THE PALETTE WAS VALIDATED, NOT CHOSEN BY EYE. Run through the dataviz validator
* (scripts/validate_palette.js, light mode, surface #fcfcfb) and re-stepped twice until every
* check passed with no warning:
* Lightness band PASS all 7 inside L 0.43–0.77
* Chroma floor PASS all 7 >= 0.1 (an earlier teal #0e7490 FAILED at 0.094 — read gray)
* CVD separation PASS worst adjacent #dc2626 <-> #15803d dE 8.6 deutan (>= 8; 6–8 is a floor
* band that would have been legal only with secondary encoding)
* Normal vision PASS worst adjacent dE 30.6
* Contrast PASS all 7 >= 3:1 (an earlier amber #f59e0b WARNed at 2.09:1)
* The order of the hexes below IS the validated order — the CVD check measures ADJACENT pairs, so
* a legend must render the regions in this same fixed order for that result to still hold.
*
* Field on the Mapbox feature: `region` (the wagf_region VALUE, e.g. 'latin_america_&_caribbean').
*/
export const PROPERTY_KEY = 'region'
const DEFAULT_COLOR = '#6d766e' // DX-life --color-brand-lighter: an unknown region reads as chrome
/** Region value → display label, as the prayer API labels them. Used by the legend. */
export const WAGF_REGIONS = {
'africa': 'Africa',
'asia': 'Asia',
'europe': 'Europe',
'latin_america_&_caribbean': 'Latin America & Caribbean',
'middle_east': 'Middle East',
'north_america_&_non-spanish_caribbean': 'North America & Non-Spanish Caribbean',
'oceania': 'Oceania',
}
/** Region value → color, in the validated order. Do not reorder without re-running the validator. */
export const PALETTE = {
'africa': '#2563eb',
'asia': '#d97706',
'europe': '#7c3aed',
'latin_america_&_caribbean': '#15803d',
'middle_east': '#dc2626',
'north_america_&_non-spanish_caribbean': '#0891b2',
'oceania': '#be185d',
}
/** The fixed render order — the same order the palette was validated in. */
export const REGION_ORDER = Object.keys(PALETTE)
/** Per-feature color resolver, for the JS layer. */
export function getColor(properties) {
const v = properties?.region || properties?._raw?.wagf_region?.value || properties?._raw?.wagf_region
return PALETTE[v] || DEFAULT_COLOR
}
/** Mapbox match expression colored by region value. */
export function applyColor() {
const match = ['match', ['coalesce', ['get', 'region'], '']]
for (const key of REGION_ORDER) match.push(key, PALETTE[key])
match.push(/* default */ DEFAULT_COLOR)
return match
}
// Registry-level alias, mirroring religion.js / language-family.js.
export const buildColorExpression = applyColor
export default {
name: 'WAGF Region',
propertyKey: PROPERTY_KEY,
palette: PALETTE,
regions: WAGF_REGIONS,
order: REGION_ORDER,
getColor,
applyColor,
buildColorExpression,
fallback: DEFAULT_COLOR,
}