doxa-maps/library/colors/wagf-region.js
Doxa Maps 370446390f C1b: the resource palette moves to library/colors/, and the local colour globs go with it
Completes C1. Source only; studio rebuilds (rule 5B). Cut on top of C1a because it
depends on that move. Rebuild scope: country-posters, doxa-research-map,
research-dashboard, dashboard-plus-grid, gospel-resources.

resource.js existed as TWO identical plugin-local copies with no library original,
while library composables resolved it through the registry — so the library depended
on a plugin registering a module the library itself documented. Now it lives in
library/colors/ like the other eight, both copies are gone, and src/colors/ holds no
.js in either plugin. The local `registerStrategies(import.meta.glob('./src/colors/*.js'))`
in both index.js files is therefore DELETED: it matched nothing after the move, and a
glob that matches nothing reads as a deliberate override. Their now-unused
registerStrategies imports went with them. That also completes the shepherd's original
C1 instruction, which C1a had to defer for exactly this reason.

★ MY OWN PREDICTION HERE WAS WRONG, and the measurement is the only reason this is
clean. I predicted check:one-palette would FAIL after the move, reasoning that the
dashboards carry 6 of resource's 8 hexes (because 6 are SHARED with affinity-block and
language-family, which they legitimately ship) while the 2 unique ones — #20c997
jesusFilm and #2c3e50 noResources — are absent, and the gate fails a PARTIAL palette.
The gate PASSES: 9 strategies, 18 pairs. Once the module is in library/colors/,
_registry.js bundles it into every consumer, so all 8 hexes are present BY CONSTRUCTION.
I had reasoned about the pre-move bundle and assumed the rebuild would not change it.

That also corrects the diagnosis I gave earlier. simpleresearch was right that there
was no disagreement, but not that the 6 were coincidental substrings in minified
output: they are real hexes, genuinely present, shared across three strategies. The
gate cannot attribute a hex to a strategy — which is a real limitation worth knowing,
just not one that bites here.

Stale comments corrected rather than left to rot: five references across
useLegendData, researchLegendOptions, useWagfRegionsLegendData and my own
wagf-region.js header all claimed these palettes live in a plugin folder. One
parenthetical keeps the old path deliberately, as history.

GATES: check:one-palette PASS 9/18 (baselined 6/12 before C1a). check:library-index
PASS after regenerating the derived menu. check:no-free-identifiers PASS. All six
affected bundles build; their artifacts reverted.
2026-09-26 08:51:39 +00:00

133 lines
7.2 KiB
JavaScript
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.

/**
* WAGF region color strategy — colors pins by the region they belong to.
*
* ★ THE VALUES ARE THE RESEARCH MAP'S, ON THE DRIVER'S INSTRUCTION (2026-09-24). He compared the
* two surfaces himself: "when i look on teh color ffor th regiosn tab of the simpel research map i
* don tsee the you using htes colors on the reigosn dashbarod hwen you shoudl". Two surfaces
* disagreeing about what Africa's colour means is a product defect whichever palette is prettier,
* and he chose which one wins. Source of truth:
* library/colors/doxa-region.js (it lived at plugin-designs/doxa-research-map/src/colors/ until C1a), which the research dashboard and
* three other consumers already paint from.
*
* WHAT THE CHANGE COST, measured rather than glossed (dataviz validate_palette.js, light mode,
* surface #fcfcfb — the same tool and surface the previous palette was stepped against):
* Lightness band FAIL #1ee05f 0.793 · #FFEB3B 0.928 · #00e0bd 0.810 sit above the 0.43–0.77 band
* Chroma floor PASS all 7 >= 0.1
* CVD separation PASS worst adjacent #FFEB3B<->#1ee05f dE 8.7 protan (>= 8), tritan 14.7
* Normal vision PASS worst adjacent dE 21.0
* Contrast WARN under 3:1 vs the surface: #1ee05f 1.72 · #FFEB3B 1.19 · #00e0bd 1.65 · #ff7f11 2.47
* The palette this replaced passed all five. So the trade is real and it is recorded here rather
* than in a commit message nobody will read next year.
*
* WHY THE CONTRAST WARN IS SURVIVABLE HERE, and it is a reason, not a dismissal: that check
* obligates relief through a secondary encoding, and both surfaces already carry one — the chooser
* renders a LEGEND of labelled chips in these exact colours, and the dashboard renders the same
* seven regions as a TABLE with names and figures. Identity never rests on hue alone on either.
* The LIGHTNESS FAIL is the part with no relief: three of these read faintly as PINS on a light
* basemap. Darkening those three by ~10% L would enter the band while keeping the hue
* recognisable, and that is the Driver's call to make, not mine to make quietly — asked, not done.
*
* 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'
// DX-life --color-brand-lighter: an unknown region reads as chrome, not as an eighth region.
// ★ DELIBERATELY NOT THEIRS. doxa-region.js uses '#0098FF' for null/undefined and keys
// 'No WAGF Region/Bloc' to '#1a1a2e'. Neither is a REGION colour — they are what an absent value
// looks like — and the Driver's instruction was about the seven regions. A bright blue for
// "unknown" would also read as a region on a map whose every other pin is a region. Every row in
// this feed carries a wagf_region, so this value is almost never painted; it is chrome on purpose.
const DEFAULT_COLOR = '#6d766e'
/** 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. THE RESEARCH MAP'S VALUES, so the two surfaces agree — see the header.
* Mapped from its label-keyed table: Africa #ff2e2e, Asia #1a8cff, Europe #1ee05f,
* Latin America & Caribbean #FFEB3B, Middle East #a83dff,
* North America & Non-Spanish Caribbean #00e0bd, Oceania #ff7f11.
* The order is still the fixed alphabetical one (colour follows the entity, never its rank), which
* is also the order the adjacent-pair CVD result in the header was measured in.
*/
export const PALETTE = {
'africa': '#ff2e2e',
'asia': '#1a8cff',
'europe': '#1ee05f',
'latin_america_&_caribbean': '#FFEB3B',
'middle_east': '#a83dff',
'north_america_&_non-spanish_caribbean': '#00e0bd',
'oceania': '#ff7f11',
}
/** 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,
}