doxa-maps/library/colors/doxa-region.js
Doxa Maps 6263c250ec C1a: the affinity-block and doxa-region palettes move to library/colors/
First palette migration from SPEC-LIBRARY-CURATION.md. Source only; studio rebuilds
(rule 5B — library/ plus four other owners' plugins).

WHY THIS ONE FIRST: the duplication was not idle. doxa-research-map's
affinity-block.js carried A018 'Tibetan Peoples' and A019 'Himalayan Peoples' —
live on the pray-tools API — with colours measured to WCAG 1.4.11's 3.0:1 non-text
floor (A013 5.28 unchanged, A018 7.69, A019 3.53; sibling separations 2.61/1.46/1.79).
country-posters' copy covered only A001–A017, so 134 rows (6.4% of 2,107) fell to the
grey fallback #95a5a6 — the "bunch of grey colours" report. exp-shepherd confirmed
that code path headlessly. So the correct copy wins with no judgement call, and the
move FIXES country-posters rather than changing it.

FOUR CONSUMERS, NOT THE TWO THE SPEC COUNTED, and finding that is the sharpest
argument for the migration: research-dashboard/index.js and dashboard-plus-grid/index.js
each imported `../doxa-research-map/src/colors/affinity-block.js` — a cross-plugin
RELATIVE import into another plugin's PRIVATE folder. Both now import @map/colors/.
Only the build found this; no gate names cross-plugin reach.

THE GLOB STAYS in both plugins' index.js, deliberately, and is documented there:
resource.js is still plugin-local pending C1b, so deleting the glob would unregister
it and colour those pins by the soft fallback — the same silent wrong-colour class
being fixed here. Deleting the two STALE FILES is what stops the stale copy winning.

THE REJECTED-CANDIDATE NOTE MOVED ABOVE `export const PALETTE`, and this is a real
constraint rather than tidying: check:one-palette slices from that export to END OF
FILE and matches every hex, despite its own comment claiming it reads "only the
PALETTE export". #AF7AC5 was rejected for A019 at 2.95 — under the accessibility
floor — and mentioned below the export it would have been DEMANDED of every consumer
bundle. The note keeps the reasoning without creating the obligation; the gate bug
itself is reported separately since internal/ is not mine to edit.

The legend needed no change and that is by design: useAffinityBlockLegendData resolves
via getColorStrategy(COLOR_MODES.AFFINITY_BLOCK) — the same strategy the pins use — and
reads labels from the data, so map and legend gain A018/A019 together. Its comments
claiming the strategy lives in a plugin folder were corrected; they now describe where
it actually is.

GATES: check:one-palette PASS — 8 strategies, 16 bundle/palette pairs (baselined at 6
and 12 before the move, so a red would have been mine). check:library-index PASS after
regenerating the derived menu. check:no-free-identifiers PASS. All four consumer
bundles build; their artifacts reverted for studio.
2026-09-26 08:39:53 +00:00

139 lines
4.2 KiB
JavaScript

/**
* DOXA Region Color Strategy
*
* Colors pins based on their DOXA / WAGF region.
*
* Drift note (vs simple-map):
* - simple-map's 'No WAGF Region/Bloc' = '#95a5a6' (grey),
* research = '#1a1a2e' (deep navy). Research wins.
*
* generateColorFromString fallback comes from utils/geoUtils.js — sister
* subagent 1D owns that file. Cross-folder dep flagged.
*/
import { generateColorFromString } from '@map/utils/geoUtils.js'
export const PROPERTY_KEY = 'doxaRegion'
const DEFAULT_COLOR = '#0098FF' // Bright blue for null/undefined region
const FALLBACK_COLOR = '#95a5a6'
/**
* DOXA Region Color Palette — defines colors for the 8 DOXA regions.
*/
export const PALETTE = {
'Africa': '#ff2e2e',
'Asia': '#1a8cff',
'Europe': '#1ee05f',
'Latin America & Caribbean': '#FFEB3B',
'Middle East': '#a83dff',
'No WAGF Region/Bloc': '#1a1a2e',
'North America & Non-Spanish Caribbean': '#00e0bd',
'Oceania': '#ff7f11'
}
export const DOXA_REGION_COLORS = PALETTE
/**
* SLUG-keyed palette — the AUTHORITATIVE map for coloring.
*
* The pray-tools API returns wagf_region as { value: <slug>, label: <localized> }.
* DSM stores the stable slug in properties.doxaRegion (e.g. 'asia', 'middle_east').
* The label is localized per `lang` ("Africa" → "África" → "Afrique"), so keying
* colors on the display name silently breaks in every non-English locale (only
* "Asia" — spelled identically across es/fr — survives). Key on the slug instead.
* Slugs verified against the live API (2026-06-22).
*/
export const SLUG_PALETTE = {
'africa': '#ff2e2e',
'asia': '#1a8cff',
'europe': '#1ee05f',
'latin_america_&_caribbean': '#FFEB3B',
'middle_east': '#a83dff',
'north_america_&_non-spanish_caribbean': '#00e0bd',
'oceania': '#ff7f11',
'na': '#1a1a2e' // No WAGF Region/Bloc
}
/** Color for a region by its stable slug (locale-independent). */
export function getRegionColorBySlug(slug) {
if (!slug) return DEFAULT_COLOR
return SLUG_PALETTE[String(slug).toLowerCase()] || FALLBACK_COLOR
}
/** Extended region aliases for flexible matching */
export const REGION_COLOR_ALIASES = {
'Africa': '#ff2e2e',
'East Asia': '#1a8cff',
'Asia': '#1a8cff',
'Eurasia': '#1ee05f',
'Europe': '#1ee05f',
'Latin America': '#FFEB3B',
'Latin America & Caribbean': '#FFEB3B',
'Caribbean': '#FFEB3B',
'Middle East': '#a83dff',
'North America': '#00e0bd',
'North America (non-spanish)': '#00e0bd',
'North America & Non-Spanish Caribbean': '#00e0bd',
'Pacific': '#ff7f11',
'Oceania': '#ff7f11',
'South Asia': '#ff7f11',
'No WAGF Region/Bloc': '#1a1a2e',
'Unknown': '#0098FF'
}
/**
* Get color for a region — falls back to a deterministic
* generateColorFromString() so unmapped regions still render.
*/
export function getRegionColor(regionName) {
if (!regionName) {
return DEFAULT_COLOR
}
const color = REGION_COLOR_ALIASES[regionName]
if (!color) {
return generateColorFromString(regionName)
}
return color
}
export function getColor(properties) {
// properties.doxaRegion is the stable slug (e.g. 'asia'), NOT the localized
// label — color by slug so it survives language switches.
const slug = properties.doxaRegion || properties._normalized?.doxaRegion
return getRegionColorBySlug(slug)
}
/**
* Build Mapbox color expression for DOXA regions.
*
* Matches on the stable slug stored in `doxaRegion` (e.g. 'asia') so the
* expression keeps coloring correctly when the API returns localized labels.
*/
export function applyColor() {
const colorExpression = ['match', ['get', PROPERTY_KEY]]
Object.entries(SLUG_PALETTE).forEach(([slug, color]) => {
colorExpression.push(slug, color)
})
colorExpression.push(FALLBACK_COLOR)
return colorExpression
}
export const buildColorExpression = applyColor
export default {
name: 'DOXA Region',
propertyKey: PROPERTY_KEY,
palette: PALETTE,
colors: PALETTE,
slugColors: SLUG_PALETTE,
aliases: REGION_COLOR_ALIASES,
fallback: FALLBACK_COLOR,
getColor,
applyColor,
buildColorExpression,
getRegionColor,
getRegionColorBySlug
}