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.
231 lines
No EOL
8.2 KiB
JavaScript
231 lines
No EOL
8.2 KiB
JavaScript
/**
|
|
* useWagfRegionsLegendData — 3-tier WAGF Regions legend for SemanticTreeLegend.
|
|
*
|
|
* Hierarchy: WAGF Region → WAGF Block → Country
|
|
*
|
|
* region node id: 'region:asia'
|
|
* block node id: 'block:asia__south_east_asia'
|
|
* country node id: 'country:asia__south_east_asia__MY'
|
|
*
|
|
* DSM-normalized properties used:
|
|
* properties.doxaRegion = wagf_region.value (slug, e.g. 'asia')
|
|
* properties.wagfRegionLabel = wagf_region.label (e.g. 'Asia')
|
|
* properties.wagfBlock = wagf_block.value (slug, e.g. 'south_east_asia')
|
|
* properties.wagfBlockLabel = wagf_block.label (e.g. 'South East Asia')
|
|
* properties.countryIso = country_code.value (alpha-3, e.g. 'MYS')
|
|
* properties.countryIsoLabel = country_code.label (e.g. 'Malaysia')
|
|
*
|
|
* Pin filter expressions (Mapbox GL) — filter pins by normalized property:
|
|
* region: ['==', ['get', 'doxaRegion'], regionSlug]
|
|
* block: ['==', ['get', 'wagfBlock'], blockSlug]
|
|
* country: ['==', ['get', 'countryIso'], isoAlpha3]
|
|
*
|
|
* Polygon highlight — iso_3166_1_alpha_3 on Mapbox country-boundaries-v1:
|
|
* node.isoCodes carries alpha-3 codes for research-map.vue to use in:
|
|
* ['match', ['get', 'iso_3166_1_alpha_3'], isoCodes, 0.7, 0.1]
|
|
*/
|
|
|
|
import { computed } from 'vue'
|
|
|
|
// ── Region color palette ──────────────────────────────────────────────────────
|
|
// Keyed on the STABLE slug (wagf_region.value), NOT the localized label. The API
|
|
// localizes the label per `lang` ("Africa" → "África" → "Afrique"), so a
|
|
// label-keyed lookup falls back to grey for every region except "Asia" (spelled
|
|
// identically across locales) once the user switches language. The slug is
|
|
// locale-independent. Mirrors SLUG_PALETTE in the research bundle's
|
|
// library/colors/doxa-region.js (moved there in C1a).
|
|
const REGION_COLORS = {
|
|
'africa': '#ff2e2e',
|
|
'asia': '#1a8cff',
|
|
'europe': '#1ee05f',
|
|
'latin_america_&_caribbean': '#FFEB3B',
|
|
'middle_east': '#a83dff',
|
|
'north_america_&_non-spanish_caribbean': '#00e0bd',
|
|
'oceania': '#ff7f11',
|
|
}
|
|
const FALLBACK_COLOR = '#95a5a6'
|
|
|
|
function regionColor(slug) {
|
|
return REGION_COLORS[String(slug || '').toLowerCase()] || FALLBACK_COLOR
|
|
}
|
|
|
|
function readProps(item) { return item?.properties || item || {} }
|
|
function readPop(p) {
|
|
const n = Number(p.population ?? p.populationNum ?? p._raw?.population ?? 0)
|
|
return Number.isFinite(n) ? n : 0
|
|
}
|
|
|
|
function readRegionSlug(p) {
|
|
return String(p.doxaRegion || p.wagfRegion || p._raw?.wagf_region?.value || '').trim()
|
|
}
|
|
function readRegionLabel(p) {
|
|
const raw = p._raw?.wagf_region
|
|
return String(
|
|
p.wagfRegionLabel || p.doxaRegionLabel ||
|
|
(raw && typeof raw === 'object' ? raw.label : '') ||
|
|
p.doxaRegion || ''
|
|
).trim()
|
|
}
|
|
|
|
function readBlockSlug(p) {
|
|
return String(p.wagfBlock || p._raw?.wagf_block?.value || '').trim()
|
|
}
|
|
function readBlockLabel(p) {
|
|
const raw = p._raw?.wagf_block
|
|
return String(
|
|
p.wagfBlockLabel ||
|
|
(raw && typeof raw === 'object' ? raw.label : '') ||
|
|
p.wagfBlock || ''
|
|
).trim()
|
|
}
|
|
|
|
function readCountryIso(p) {
|
|
// Alpha-3 from country_code field (e.g. 'MYS') — matches Mapbox iso_3166_1_alpha_3
|
|
return String(p.countryIso || p._raw?.country_code?.value || '').trim().toUpperCase()
|
|
}
|
|
function readCountryName(p) {
|
|
const raw = p._raw?.country_code
|
|
return String(
|
|
p.countryIsoLabel ||
|
|
(raw && typeof raw === 'object' ? raw.label : '') ||
|
|
p.countryIso || ''
|
|
).trim()
|
|
}
|
|
|
|
export function useWagfRegionsLegendData(peopleGroupsRef) {
|
|
/**
|
|
* aggregated: Map<regionSlug → { label, color, blocks: Map<blockSlug → { label, countries }> }>
|
|
*/
|
|
const aggregated = computed(() => {
|
|
const list = (peopleGroupsRef && peopleGroupsRef.value) || []
|
|
const regions = new Map()
|
|
|
|
for (const item of list) {
|
|
const p = readProps(item)
|
|
const regionSlug = readRegionSlug(p)
|
|
const regionLabel = readRegionLabel(p) || regionSlug || '— Unknown —'
|
|
const blockSlug = readBlockSlug(p)
|
|
const blockLabel = readBlockLabel(p) || blockSlug || '— Unknown —'
|
|
const iso = readCountryIso(p)
|
|
const countryName = readCountryName(p) || iso || '— Unknown —'
|
|
const pop = readPop(p)
|
|
|
|
// Skip unclassified entries (N/A, None, empty)
|
|
if (!regionSlug || regionSlug === 'na' || regionSlug === 'none') continue
|
|
|
|
let region = regions.get(regionSlug)
|
|
if (!region) {
|
|
region = { slug: regionSlug, label: regionLabel, count: 0, pop: 0, blocks: new Map() }
|
|
regions.set(regionSlug, region)
|
|
}
|
|
region.count += 1
|
|
region.pop += pop
|
|
|
|
if (!blockSlug || blockSlug === 'na') continue
|
|
|
|
let block = region.blocks.get(blockSlug)
|
|
if (!block) {
|
|
block = { slug: blockSlug, label: blockLabel, count: 0, pop: 0, countries: new Map() }
|
|
region.blocks.set(blockSlug, block)
|
|
}
|
|
block.count += 1
|
|
block.pop += pop
|
|
|
|
if (!iso) continue
|
|
|
|
let country = block.countries.get(iso)
|
|
if (!country) {
|
|
country = { iso, name: countryName, count: 0, pop: 0 }
|
|
block.countries.set(iso, country)
|
|
}
|
|
country.count += 1
|
|
country.pop += pop
|
|
}
|
|
|
|
return regions
|
|
})
|
|
|
|
/**
|
|
* regionsTree — SemanticTreeLegend node shape.
|
|
* { id, label, color, count, pop, filter, isoCodes, children }
|
|
* `isoCodes` (alpha-3, matched on iso_3166_1_alpha_3) drives Mapbox polygon highlight directly.
|
|
*/
|
|
const regionsTree = computed(() => {
|
|
const regions = aggregated.value
|
|
const out = []
|
|
|
|
for (const region of regions.values()) {
|
|
// Color by the stable slug (locale-independent), not the localized label.
|
|
const color = regionColor(region.slug)
|
|
const regionIsoCodes = []
|
|
const blockNodes = []
|
|
|
|
const sortedBlocks = [...region.blocks.values()]
|
|
.sort((a, b) => (b.pop - a.pop) || a.label.localeCompare(b.label))
|
|
|
|
for (const block of sortedBlocks) {
|
|
const blockIsoCodes = []
|
|
const countryNodes = []
|
|
|
|
const sortedCountries = [...block.countries.values()]
|
|
.sort((a, b) => (b.pop - a.pop) || a.name.localeCompare(b.name))
|
|
|
|
for (const c of sortedCountries) {
|
|
blockIsoCodes.push(c.iso)
|
|
regionIsoCodes.push(c.iso)
|
|
countryNodes.push({
|
|
id: `country:${region.slug}__${block.slug}__${c.iso}`,
|
|
label: c.name,
|
|
color,
|
|
count: c.count,
|
|
pop: c.pop,
|
|
filter: ['==', ['get', 'countryIso'], c.iso],
|
|
isoCodes: [c.iso],
|
|
children: [],
|
|
})
|
|
}
|
|
|
|
blockNodes.push({
|
|
id: `block:${region.slug}__${block.slug}`,
|
|
label: `${block.label} (Bloc)`,
|
|
color,
|
|
count: block.count,
|
|
pop: block.pop,
|
|
filter: ['==', ['get', 'wagfBlock'], block.slug],
|
|
isoCodes: blockIsoCodes,
|
|
children: countryNodes,
|
|
})
|
|
}
|
|
|
|
out.push({
|
|
id: `region:${region.slug}`,
|
|
label: `${region.label} (Region)`,
|
|
color,
|
|
count: region.count,
|
|
pop: region.pop,
|
|
filter: ['==', ['get', 'doxaRegion'], region.slug],
|
|
isoCodes: [...new Set(regionIsoCodes)],
|
|
children: blockNodes,
|
|
})
|
|
}
|
|
|
|
out.sort((a, b) => (b.pop - a.pop) || a.label.localeCompare(b.label))
|
|
return out
|
|
})
|
|
|
|
function highlight(node) {
|
|
if (typeof window === 'undefined' || typeof window.CustomEvent !== 'function') return
|
|
if (!node) {
|
|
window.dispatchEvent(new CustomEvent('legend:highlight', { detail: { kind: null } }))
|
|
return
|
|
}
|
|
const id = String(node.id || '')
|
|
const detail = { kind: null, label: node.label, filterExpr: node.filter }
|
|
if (id.startsWith('region:')) detail.kind = 'region'
|
|
else if (id.startsWith('block:')) detail.kind = 'block'
|
|
else if (id.startsWith('country:')) detail.kind = 'country'
|
|
window.dispatchEvent(new CustomEvent('legend:highlight', { detail }))
|
|
}
|
|
|
|
return { aggregated, regionsTree, highlight }
|
|
} |