doxa-maps/library/composables/legends/researchLegendOptions.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

131 lines
6.3 KiB
JavaScript

/**
* researchLegendOptions.js — research-mfe-only options passed to the shared
* useLegendData composable in @map/composables/legends/useLegendData.js.
*
* Houses the research-specific legend variants that don't belong in the shared
* blueprint:
* - legendTypeAliases: research uses singular `doxa-region` interchangeably
* with the canonical plural `doxa-regions`
* - customItemBuilders['language-family']: research-only expand/collapse
* tree variant that delegates row construction to useLanguageFamilyLegendData
*/
import { useLanguageFamilyLegendData } from './useLanguageFamilyLegendData.js'
import { getLanguageFamilyColor } from '../../colors/language-family.js'
import { getColorStrategy, COLOR_MODES } from '../../colors/_registry.js'
// doxa-region is a per-map (research-only) strategy resolved via the registry
// (registered by the research bundle's local src/colors glob). Read LAZILY
// (inside the builder, at call time) so registration has already run — a
// module-load-time read could see an empty palette.
const doxaRegionColors = () => getColorStrategy(COLOR_MODES.DOXA_REGION)?.palette ?? {}
// resource is likewise research-only, and now lives in library/colors/ (C1b) (was plugin-designs/doxa-research-map/src/colors/
// resource.js), registry-resolved. Lazy + GUARDED on propertyKey — getColorStrategy
// falls back to language-family for unregistered modes, and that palette must not
// masquerade as resource colors.
const resourceStrategy = () => {
const strat = getColorStrategy(COLOR_MODES.RESOURCE)
return (strat && strat.propertyKey === 'resourceType') ? strat : null
}
export const RESEARCH_LEGEND_OPTIONS = {
legendTypeAliases: {
'doxa-region': 'doxa-regions'
},
// Religion-family letters to hide from the religion legend. The doxa research
// map's dataset is UNENGAGED people groups — Christianity-following peoples
// shouldn't appear in this lens. Hiding 'C' (Christianity) here means the row
// doesn't render even if a few stray CPR-flagged rows exist in the data.
// Override per-bundle by passing different opts to useLegendData.
excludeReligionFamilies: ['C'],
colorsApi: {
getLanguageFamilyColor,
// Lazy getters: resolved from the registry at ACCESS time (post-registration),
// not at module load — this file is imported by shared legend components.
get resourceColors() { return resourceStrategy()?.palette ?? {} },
get resourceNames() { return resourceStrategy()?.names ?? {} }
},
customItemBuilders: {
'language-family': ({ dataStore }) => {
const features = Object.values(dataStore.sources || {})
.flatMap(src => src?.features || [])
const featuresRef = { value: features }
const { rows: famRows } = useLanguageFamilyLegendData(featuresRef)
return famRows.value.map(r => ({
key: r.key,
label: r.label,
color: r.color,
count: r.peopleGroupCount,
population: r.population,
filterKey: r.key,
// coords + pinIds carry through so Auto-Fly tour and the legend:highlight
// event handler can fit-bounds onto each family during iteration.
coords: r.coords || [],
pinIds: r.pinIds || [],
children: []
}))
},
// Doxa Regions tab — show ALL known regions even if zero people-groups
// currently match (seeded from DOXA_REGION_COLORS), with live counts
// populated from whatever sources are loaded into dataStore.
'doxa-regions': ({ dataStore }) => {
const DOXA_REGION_COLORS = doxaRegionColors() // lazy: registry-resolved at call time
const features = Object.values(dataStore.sources || {})
.flatMap(src => src?.features || [])
// Map lowercase / snake_case region keys (e.g. pray-tools `wagf_region.value`
// = "asia", "latin_america_&_caribbean", "na") onto the canonical
// DOXA_REGION_COLORS keys so we don't end up with duplicate rows for the
// same region. Normalize both sides to lowercase-alphanumeric so "asia"
// and "Asia" collapse, and "latin_america_&_caribbean" matches
// "Latin America & Caribbean".
const normalizeKey = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '')
const REGION_KEY_ALIASES = {}
for (const canonical of Object.keys(DOXA_REGION_COLORS)) {
REGION_KEY_ALIASES[normalizeKey(canonical)] = canonical
}
// pray-tools' "na" (label "N/A") = "No WAGF Region/Bloc"
REGION_KEY_ALIASES['na'] = 'No WAGF Region/Bloc'
const canonicalize = (raw) => {
if (!raw) return ''
const trimmed = String(raw).trim()
return REGION_KEY_ALIASES[normalizeKey(trimmed)] || trimmed
}
const agg = new Map()
for (const item of features) {
const p = item?.properties || item || {}
// pray-tools normalizes wagf_region → doxaRegion; might be {value,label}
let raw = p.doxaRegion ?? p.wagfRegion ?? p._raw?.['WAGF Region']
if (raw && typeof raw === 'object') raw = raw.label || raw.value
const key = canonicalize(raw)
if (!key) continue
const pop = parseInt(p.population ?? p._raw?.Pop ?? 0, 10) || 0
const lat = parseFloat(p.latitude ?? p._raw?.Latitude)
const lng = parseFloat(p.longitude ?? p._raw?.Longitude)
let bucket = agg.get(key)
if (!bucket) { bucket = { count: 0, population: 0, coords: [] }; agg.set(key, bucket) }
bucket.count += 1
bucket.population += pop
if (isFinite(lat) && isFinite(lng)) bucket.coords.push([lng, lat])
}
const rows = []
const seen = new Set()
for (const [name, b] of agg) {
rows.push({
key: name, label: name, color: DOXA_REGION_COLORS[name] || '#999',
count: b.count, population: b.population, filterKey: name,
coords: b.coords, children: []
})
seen.add(name)
}
// Seed remaining known regions with 0 counts so the legend is a stable
// canonical list regardless of which subset of regions the data covers.
for (const name of Object.keys(DOXA_REGION_COLORS)) {
if (seen.has(name)) continue
rows.push({
key: name, label: name, color: DOXA_REGION_COLORS[name],
count: 0, population: 0, filterKey: name, coords: [], children: []
})
}
rows.sort((a, b) => (b.count - a.count) || (b.population - a.population))
return rows
}
}
}