doxa-maps/library/colors/affinity-block.js
Doxa Maps 93ca8dab91
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source d3d3c3f8
2026-09-26 11:59:17 +00:00

190 lines
7.1 KiB
JavaScript

/**
* Affinity Block Color Strategy
*
* Colors pins based on their ROP1 code (A001, A002, etc.).
* Uses ROP1 code as the primary key for consistency across data sources.
*
* ARCHITECTURE DECISION:
* - Use ROP1 codes (A001, A002, etc.) as the PRIMARY KEY
* - Store mappings from ROP1 → Name and ROP1 → Color
* - Avoids issues with typos in affinity bloc names
*/
export const PROPERTY_KEY = 'affinityBlock' // This is the ROP1 code
const DEFAULT_COLOR = '#95a5a6'
/** Primary mapping: ROP1 Code → Affinity Block Name */
export const AFFINITY_BLOCK_NAMES = {
'A001': 'Arab World',
'A002': 'East Asian Peoples',
'A003': 'Eurasian Peoples',
'A004': 'Horn of Africa Peoples',
'A005': 'Jewish',
'A006': 'Malay Peoples',
'A007': 'Persian-Median Peoples',
'A008': 'South Asian Peoples',
'A009': 'North American Peoples',
'A010': 'Pacific Islanders',
'A011': 'Southeast Asian Peoples',
'A012': 'Sub-Saharan Peoples',
'A013': 'Tibetan-Himalayan Peoples',
'A014': 'Turkic Peoples',
'A015': 'Latin-Caribbean Americans',
'A017': 'Deaf',
// A018/A019 are LIVE on the pray-tools API but were absent here, so 134 rows (6.4% of
// 2,107, measured 2026-09-22) fell through to the grey fallback and rendered as
// uncoloured rows in the semantic tree legend — the "bunch of grey colours" reported.
// Names are the API's OWN rop1 labels, not invented: A018 -> "Tibetan Peoples"
// (102 rows), A019 -> "Himalayan Peoples" (32 rows).
'A018': 'Tibetan Peoples',
'A019': 'Himalayan Peoples'
}
/** Primary mapping: ROP1 Code → Color */
// ★ REJECTED CANDIDATE, RECORDED ABOVE THE PALETTE DELIBERATELY: #AF7AC5 was considered for A019
// and scored 2.95 against the Material Light surface, just under WCAG 1.4.11's 3.0:1 non-text floor.
// It lives here rather than beside A019 because check:one-palette slices from `export const PALETTE`
// to END OF FILE and matches every hex it finds -- so a rejected colour mentioned BELOW the export
// would be demanded of every consumer bundle, and the gate would fail on a colour we refused on
// accessibility grounds. Keeping the note above the export preserves the reasoning without
// creating that obligation. (The gate's own comment says it reads "only the PALETTE export"; its
// implementation does not, which is reported separately.)
export const PALETTE = {
'A001': '#E74C3C', // Arab World - Red/Orange
'A002': '#3498DB', // East Asian Peoples - Blue
'A003': '#2ECC71', // Eurasian Peoples - Green
'A004': '#F39C12', // Horn of Africa Peoples - Orange
'A005': '#9B59B6', // Jewish - Purple
'A006': '#1ABC9C', // Malay Peoples - Teal
'A007': '#E67E22', // Persian-Median Peoples - Dark orange
'A008': '#34495E', // South Asian Peoples - Dark blue-grey
'A009': '#95A5A6', // North American Peoples - Grey
'A010': '#5DADE2', // Pacific Islanders - Light blue
'A011': '#16A085', // Southeast Asian Peoples - Sea green
'A012': '#C0392B', // Sub-Saharan Peoples - Deep red
'A013': '#8E44AD', // Tibetan-Himalayan Peoples - Purple
'A014': '#D35400', // Turkic Peoples - Burnt orange
'A015': '#27AE60', // Latin-Caribbean Americans - Forest green
'A017': '#000000', // Deaf - Black (universal accessibility)
// ── A018 / A019: the Tibetan-Himalayan SPLIT ────────────────────────────────
// These two are not new peoples — they are the finer-grained split of A013
// 'Tibetan-Himalayan Peoples'. They therefore keep A013's PURPLE identity and vary
// only in value, so the three read as one family at a glance instead of as three
// unrelated hues. Chosen by measurement against the Material Light surface #f3f3f1
// (WCAG 2.1 SC 1.4.11 non-text minimum 3.0:1):
// A013 #8E44AD 5.28 (unchanged) A018 #6C3483 7.69 A019 #A569BD 3.53
// Sibling separation so the three stay tellable apart: A018-vs-A019 2.61,
// A013-vs-A018 1.46, A013-vs-A019 1.79.
'A018': '#6C3483', // Tibetan Peoples - deep purple (A013 family, darker)
'A019': '#A569BD' // Himalayan Peoples - light purple (A013 family, lighter)
}
export const AFFINITY_BLOCK_COLORS = PALETTE
/** Reverse lookup: Name → ROP1 Code */
export const AFFINITY_BLOCK_NAME_TO_CODE = Object.entries(AFFINITY_BLOCK_NAMES).reduce((acc, [code, name]) => {
acc[name] = code
return acc
}, {})
/** Legacy: Name-based color lookup (for backward compatibility) */
export const AFFINITY_BLOCK_COLORS_BY_NAME = Object.entries(AFFINITY_BLOCK_NAMES).reduce((acc, [code, name]) => {
acc[name] = AFFINITY_BLOCK_COLORS[code]
return acc
}, {})
export const NAMES = AFFINITY_BLOCK_NAMES
export function getAffinityBlockName(code) {
return AFFINITY_BLOCK_NAMES[code] || 'Unknown'
}
export function getAffinityBlockColorByCode(code) {
return AFFINITY_BLOCK_COLORS[code] || DEFAULT_COLOR
}
/**
* Get color for an affinity block (supports both ROP1 code and name).
*/
export function getAffinityBlockColor(blockNameOrCode) {
if (!blockNameOrCode) return DEFAULT_COLOR
if (typeof blockNameOrCode === 'string' && /^A\d{3}$/.test(blockNameOrCode)) {
return AFFINITY_BLOCK_COLORS[blockNameOrCode] || DEFAULT_COLOR
}
return AFFINITY_BLOCK_COLORS_BY_NAME[blockNameOrCode] || DEFAULT_COLOR
}
export function getAffinityBlocksArray() {
return Object.entries(AFFINITY_BLOCK_NAMES).map(([code, name]) => ({
code,
name,
color: AFFINITY_BLOCK_COLORS[code]
}))
}
export function getAffinityBlockData(code) {
return {
code,
name: AFFINITY_BLOCK_NAMES[code] || 'Unknown',
color: AFFINITY_BLOCK_COLORS[code] || DEFAULT_COLOR
}
}
/**
* Strategy-aligned getter — uniform name across all per-strategy modules.
* Expects properties.affinityBlock to contain ROP1 code (e.g., "A007").
*/
export function getColor(properties) {
const rop1Code = properties.affinityBlock || properties._raw?.ROP1
return getAffinityBlockColorByCode(rop1Code)
}
/**
* Get display name for a feature.
*/
export function getName(properties) {
const rop1Code = properties.affinityBlock || properties._raw?.ROP1
return getAffinityBlockName(rop1Code)
}
/**
* Build Mapbox color expression for affinity blocks.
* Uses ROP1 code (A001, A002, etc.) as the match key.
*/
export function applyColor() {
const colorExpression = ['match', ['get', PROPERTY_KEY]]
Object.entries(AFFINITY_BLOCK_COLORS).forEach(([code, color]) => {
colorExpression.push(code, color)
})
colorExpression.push(DEFAULT_COLOR)
return colorExpression
}
export const buildColorExpression = applyColor
// Backward-compatibility aliases
export const affinityBlockCodeToName = AFFINITY_BLOCK_NAMES
export const affinityBlockCodeToColor = AFFINITY_BLOCK_COLORS
export const affinityBlockNameToCode = AFFINITY_BLOCK_NAME_TO_CODE
export const affinityBlockColors = AFFINITY_BLOCK_COLORS_BY_NAME
export const affinityBlockCodeLookup = AFFINITY_BLOCK_NAMES
export default {
name: 'Affinity Block',
propertyKey: PROPERTY_KEY,
palette: PALETTE,
colors: PALETTE,
names: NAMES,
getColor,
applyColor,
buildColorExpression,
getName,
getAffinityBlockName,
getAffinityBlockColor,
getAffinityBlockColorByCode,
getAffinityBlocksArray,
getAffinityBlockData
}