Merge remote-tracking branch 'origin/main' into rega/research-gallery-embed
# Conflicts: # public/doxa-maps/index.html
This commit is contained in:
commit
debb67338a
24 changed files with 93239 additions and 120 deletions
46370
data/tables/doxa-people-groups.json
Normal file
46370
data/tables/doxa-people-groups.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -39,7 +39,12 @@ const page = await (await browser.newContext({ viewport: { width: 1280, height:
|
|||
let current = '(page load)'
|
||||
const seen = {}
|
||||
const note = (kind, v) => { const s = (seen[current] = seen[current] || { errors: [], failed: [] }); s[kind].push(v) }
|
||||
page.on('pageerror', (e) => note('errors', String(e.message || e).slice(0, 160)))
|
||||
// Keep the top of the STACK, not just the message: a message like "reading 'getRootNode'" names no
|
||||
// caller, and the owner cannot fix it without knowing which call site threw (asked 2026-09-25).
|
||||
page.on('pageerror', (e) => {
|
||||
const frames = String(e.stack || '').split('\n').slice(1, 5).map((l) => l.trim().replace(/https?:\/\/[^/]+/, '').replace(/\?v=[0-9a-f]+/, '')).join(' <- ')
|
||||
note('errors', String(e.message || e).slice(0, 160) + (frames ? ' [' + frames + ']' : ''))
|
||||
})
|
||||
page.on('response', (r) => {
|
||||
if (r.status() < 400) return
|
||||
let host = ''
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "كل",
|
||||
"tallyIs": "هو شعب واحد —",
|
||||
"tallyOf": "منها،",
|
||||
"tallyLink": "لديها رابط"
|
||||
"tallyLink": "لديها رابط",
|
||||
"doxa0": "لدى الجميع شيء",
|
||||
"doxa1": "1–4 بلا شيء",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 لديهم موارد",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "شعوب دوكسا على هذه الخريطة",
|
||||
"notPlaced": "ضمن القائمة، بلا موقع بعد",
|
||||
"outOfScope": "لا توجد شعوب دوكسا — غير ملوَّنة"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "أكبر الشعوب",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Jeder",
|
||||
"tallyIs": "ist EINE Volksgruppe —",
|
||||
"tallyOf": "davon,",
|
||||
"tallyLink": "mit einem Link"
|
||||
"tallyLink": "mit einem Link",
|
||||
"doxa0": "alle haben etwas",
|
||||
"doxa1": "1–4 ohne",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 erreicht",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Doxa-Volksgruppen auf dieser Karte",
|
||||
"notPlaced": "auf der Liste, noch nicht verortet",
|
||||
"outOfScope": "keine Doxa-Volksgruppen — nicht eingefärbt"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Größte Gruppen",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Each",
|
||||
"tallyIs": "is ONE people group —",
|
||||
"tallyOf": "of them,",
|
||||
"tallyLink": "with a link"
|
||||
"tallyLink": "with a link",
|
||||
"doxa0": "all have something",
|
||||
"doxa1": "1–4 with nothing",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 reached",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Doxa people groups on this map",
|
||||
"notPlaced": "on the roster, not yet placed",
|
||||
"outOfScope": "no Doxa people groups — not coloured"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Largest groups",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Cada",
|
||||
"tallyIs": "es UN pueblo —",
|
||||
"tallyOf": "de ellos,",
|
||||
"tallyLink": "con un enlace"
|
||||
"tallyLink": "con un enlace",
|
||||
"doxa0": "todos tienen algo",
|
||||
"doxa1": "1–4 sin nada",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 alcanzados",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Pueblos de Doxa en este mapa",
|
||||
"notPlaced": "en la lista, aún sin ubicar",
|
||||
"outOfScope": "sin pueblos de Doxa — sin color"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Pueblos más grandes",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Chaque",
|
||||
"tallyIs": "est UN peuple —",
|
||||
"tallyOf": "d'entre eux,",
|
||||
"tallyLink": "avec un lien"
|
||||
"tallyLink": "avec un lien",
|
||||
"doxa0": "tous ont quelque chose",
|
||||
"doxa1": "1–4 sans rien",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 atteints",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Peuples Doxa sur cette carte",
|
||||
"notPlaced": "sur la liste, pas encore localisés",
|
||||
"outOfScope": "aucun peuple Doxa — non coloré"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Peuples les plus nombreux",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "हर",
|
||||
"tallyIs": "एक जनसमूह है —",
|
||||
"tallyOf": "में से,",
|
||||
"tallyLink": "के पास लिंक है"
|
||||
"tallyLink": "के पास लिंक है",
|
||||
"doxa0": "सभी के पास कुछ है",
|
||||
"doxa1": "1–4 के पास कुछ नहीं",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 तक पहुँच",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "इस मानचित्र पर दोक्सा जनसमूह",
|
||||
"notPlaced": "सूची में, अभी स्थान नहीं",
|
||||
"outOfScope": "कोई दोक्सा जनसमूह नहीं — रंग नहीं"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "सबसे बड़े जनसमूह",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Ogni",
|
||||
"tallyIs": "è UN popolo —",
|
||||
"tallyOf": "di questi,",
|
||||
"tallyLink": "con un link"
|
||||
"tallyLink": "con un link",
|
||||
"doxa0": "tutti hanno qualcosa",
|
||||
"doxa1": "1–4 senza nulla",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 raggiunti",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Popoli Doxa su questa mappa",
|
||||
"notPlaced": "in elenco, non ancora collocati",
|
||||
"outOfScope": "nessun popolo Doxa — non colorato"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Popoli più numerosi",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Cada",
|
||||
"tallyIs": "é UM povo —",
|
||||
"tallyOf": "deles,",
|
||||
"tallyLink": "com um link"
|
||||
"tallyLink": "com um link",
|
||||
"doxa0": "todos têm algo",
|
||||
"doxa1": "1–4 sem nada",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 alcançados",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Povos Doxa neste mapa",
|
||||
"notPlaced": "na lista, ainda sem localização",
|
||||
"outOfScope": "sem povos Doxa — sem cor"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Maiores povos",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Fiecare",
|
||||
"tallyIs": "este UN popor —",
|
||||
"tallyOf": "dintre ele,",
|
||||
"tallyLink": "cu un link"
|
||||
"tallyLink": "cu un link",
|
||||
"doxa0": "toate au ceva",
|
||||
"doxa1": "1–4 fără nimic",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 atinse",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Popoare Doxa pe această hartă",
|
||||
"notPlaced": "pe listă, încă nelocalizate",
|
||||
"outOfScope": "niciun popor Doxa — necolorat"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Cele mai mari popoare",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "Каждая",
|
||||
"tallyIs": "— это ОДИН народ,",
|
||||
"tallyOf": "из них",
|
||||
"tallyLink": "со ссылкой"
|
||||
"tallyLink": "со ссылкой",
|
||||
"doxa0": "у всех что-то есть",
|
||||
"doxa1": "1–4 без ресурсов",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 достигнуты",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "Народы Doxa на этой карте",
|
||||
"notPlaced": "в списке, пока без координат",
|
||||
"outOfScope": "нет народов Doxa — не окрашено"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "Крупнейшие народы",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,19 @@
|
|||
"tallyEach": "每个",
|
||||
"tallyIs": "代表一个族群 —",
|
||||
"tallyOf": "个族群中,",
|
||||
"tallyLink": "个有链接"
|
||||
"tallyLink": "个有链接",
|
||||
"doxa0": "全部都有资源",
|
||||
"doxa1": "1–4 个一无所有",
|
||||
"doxa5": "5–14",
|
||||
"doxa15": "15–39",
|
||||
"doxa40": "40+",
|
||||
"cvd1": "1–4 个已触及",
|
||||
"cvd5": "5–14",
|
||||
"cvd15": "15–39",
|
||||
"cvd40": "40+",
|
||||
"onMap": "本地图上的 Doxa 族群",
|
||||
"notPlaced": "在名单上,尚未定位",
|
||||
"outOfScope": "没有 Doxa 族群 — 不着色"
|
||||
},
|
||||
"tabs": {
|
||||
"groups": "最大的族群",
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@
|
|||
* the key doubles as a filter so the gap can be looked at on its own, and a shared URL
|
||||
* reopens the group you were looking at.
|
||||
*
|
||||
* The country shading is an AVERAGE across a country's groups and the dots are
|
||||
* individual groups, which is why a red dot can sit on a green country — that case is
|
||||
* the point of the map, not a rendering fault.
|
||||
* The country shading is a COUNT of Doxa's groups with nothing found — never a share,
|
||||
* because 106 of their 175 countries hold one or two groups and a percentage over that
|
||||
* denominator is noise. The dots are individual groups, which is why a dark dot can sit
|
||||
* on a light country — that case is the point of the map, not a rendering fault. A
|
||||
* country Doxa does not work is left uncoloured, not drawn as covered.
|
||||
*
|
||||
* > design note 2026-08-01: "mapping out the gospel resources is super important for me
|
||||
* > … key city posters, cities with internet, gospel resources — all these are
|
||||
|
|
@ -112,52 +114,74 @@ const err = ref(null)
|
|||
const mode = ref('gap')
|
||||
|
||||
// Bands, not a continuous ramp: a reader can hold four categories, not a gradient.
|
||||
/**
|
||||
* ═══ THE COLOUR SCALE, AND WHY IT COUNTS INSTEAD OF DIVIDING ════════════════════════════
|
||||
*
|
||||
* The old ramp painted a country by the SHARE of its groups with nothing found. Measured
|
||||
* against Doxa's roster that is not a statistic, it is noise: 106 of their 175 countries
|
||||
* hold one or two groups, and the United States holds three — Coquille, Pequots, Umpqua.
|
||||
* One of three without a resource is 33%, so the whole United States rendered amber next
|
||||
* to India, which holds 855. The map read as "most of the world is in crisis" because of
|
||||
* arithmetic, not because of the world. Swapping in a prettier ramp would have kept the
|
||||
* lie and made it handsomer.
|
||||
*
|
||||
* So the scale is a COUNT: how many of Doxa's groups in this country have nothing found.
|
||||
* A count cannot be inflated by a small denominator, it is what a team would actually plan
|
||||
* around, and it puts India, Sudan and Laos where they belong.
|
||||
*
|
||||
* OUT OF SCOPE IS NOT ZERO. A country with no Doxa groups gets NO FILL at all. Painting it
|
||||
* "green/covered" claimed a finding about a place this dataset says nothing about, which is
|
||||
* the same class of error as drawing an unmeasured figure as a zero.
|
||||
*
|
||||
* THE RAMP. Single family, light to dark, on the house light palette — sage for "every Doxa
|
||||
* group here has something", then four steps of clay deepening with the count. No yellow:
|
||||
* a mid-spectrum yellow over a pale basemap is what made the old map look jaundiced, and it
|
||||
* also carries the least luminance contrast of any hue against #f3f3f1. Sage and clay sit
|
||||
* far apart in hue AND in lightness, so the scale survives being read without colour.
|
||||
*/
|
||||
const NO_DOXA_FILL = 'rgba(0,0,0,0)'
|
||||
const BANDS = [
|
||||
/* labelKey, not label: the band names are rendered through t() so they translate with
|
||||
the rest of the chrome. The id and the colour are data and stay as they are. */
|
||||
{ id: 'critical', labelKey: 'gospel.legend.band60', color: '#b42318' },
|
||||
{ id: 'high', labelKey: 'gospel.legend.band30', color: '#e07a2b' },
|
||||
{ id: 'partial', labelKey: 'gospel.legend.bandSome', color: '#e8c15a' },
|
||||
{ id: 'covered', labelKey: 'gospel.legend.bandAll', color: '#10b981' },
|
||||
{ id: 'd0', labelKey: 'gospel.legend.doxa0', color: '#92b195', min: 0, max: 0 },
|
||||
{ id: 'd1', labelKey: 'gospel.legend.doxa1', color: '#e8d9cd', min: 1, max: 4 },
|
||||
{ id: 'd5', labelKey: 'gospel.legend.doxa5', color: '#cfa388', min: 5, max: 14 },
|
||||
{ id: 'd15', labelKey: 'gospel.legend.doxa15', color: '#b06f57', min: 15, max: 39 },
|
||||
{ id: 'd40', labelKey: 'gospel.legend.doxa40', color: '#7d3b30', min: 40, max: Infinity },
|
||||
]
|
||||
// Same four colours read the other way round: green now means "most groups have
|
||||
// something found", so the eye is not asked to re-learn the palette per mode.
|
||||
/* Coverage mode asks the opposite question over the same roster: how many Doxa groups here
|
||||
DO have something. Same count logic, same reason, mirrored ramp. */
|
||||
const COVER_BANDS = [
|
||||
{ id: 'c-high', labelKey: 'gospel.legend.cov80', color: '#10b981' },
|
||||
{ id: 'c-mid', labelKey: 'gospel.legend.cov50', color: '#e8c15a' },
|
||||
{ id: 'c-low', labelKey: 'gospel.legend.cov1', color: '#e07a2b' },
|
||||
{ id: 'c-none', labelKey: 'gospel.legend.cov0', color: '#b42318' },
|
||||
{ id: 'c1', labelKey: 'gospel.legend.cvd1', color: '#e8d9cd', min: 1, max: 4 },
|
||||
{ id: 'c5', labelKey: 'gospel.legend.cvd5', color: '#a8c4a9', min: 5, max: 14 },
|
||||
{ id: 'c15', labelKey: 'gospel.legend.cvd15', color: '#6f9673', min: 15, max: 39 },
|
||||
{ id: 'c40', labelKey: 'gospel.legend.cvd40', color: '#3b463d', min: 40, max: Infinity },
|
||||
]
|
||||
const bandFor = (list, n) => list.find((b) => n >= b.min && n <= b.max) || null
|
||||
/** A country's fill. Null tally = Doxa does not work here = no fill, never a colour. */
|
||||
const featureColor = (iso3) => {
|
||||
const t = doxaByCountry.value[iso3]
|
||||
if (!t) return NO_DOXA_FILL
|
||||
const n = mode.value === 'gap' ? t.none : t.groups - t.none
|
||||
const b = bandFor(mode.value === 'gap' ? BANDS : COVER_BANDS, n)
|
||||
return b ? b.color : NO_DOXA_FILL
|
||||
}
|
||||
const bandColor = (b) => (BANDS.find((x) => x.id === b) || {}).color || NO_DOXA_FILL
|
||||
|
||||
const activeBands = computed(() => (mode.value === 'gap' ? BANDS : COVER_BANDS))
|
||||
|
||||
// PIN TIERS reuse the band hexes above rather than declaring a fourth palette, so a pin
|
||||
// and the country under it never disagree about what "nothing found" looks like. Read
|
||||
// straight off BANDS so editing a band moves the pins with it.
|
||||
// PIN TIERS ARE THEIR OWN SCALE NOW, and deliberately so. They used to index into BANDS,
|
||||
// which was safe while both scales measured the same thing. They no longer do: a band is a
|
||||
// COUNT of Doxa groups in a country, a pin is what ONE group HAS. Indexing a count ramp by
|
||||
// a tier would have painted "nothing found" in the colour of "1-4 groups" — two different
|
||||
// facts wearing one colour, which is the disagreement that comment was written to prevent.
|
||||
// Dark green reads as complete, sage as partial, oxblood as empty; all three sit apart from
|
||||
// the clay country ramp in hue and in lightness, so a pin never dissolves into its country.
|
||||
const PIN_COLOR = {
|
||||
2: BANDS[3].color, // Scripture AND film in their own language
|
||||
1: BANDS[2].color, // one of the two
|
||||
0: BANDS[0].color, // nothing found in the pools checked — the pin that matters most
|
||||
2: '#3b463d', // Scripture AND film in their own language
|
||||
1: '#92b195', // one of the two
|
||||
0: '#7d3b30', // nothing found in the pools checked — the pin that matters most
|
||||
}
|
||||
|
||||
|
||||
const bandColor = (b) => (BANDS.find((x) => x.id === b) || {}).color || '#9aa2ad'
|
||||
// Share of a country's groups with ANY resource found, derived from the same counts
|
||||
// the gap band uses — so the two modes can never disagree about the same country.
|
||||
const coverPct = (d) => {
|
||||
const g = Number(d?.groups) || 0
|
||||
if (!g) return null
|
||||
return ((g - (Number(d?.no_resource) || 0)) / g) * 100
|
||||
}
|
||||
const coverColor = (d) => {
|
||||
const p = coverPct(d)
|
||||
if (p === null) return '#9aa2ad'
|
||||
if (p >= 80) return '#10b981'
|
||||
if (p >= 50) return '#e8c15a'
|
||||
if (p > 0) return '#e07a2b'
|
||||
return '#b42318'
|
||||
}
|
||||
const featureColor = (d) => (mode.value === 'gap' ? bandColor(d.band) : coverColor(d))
|
||||
|
||||
const byIso = computed(() => {
|
||||
const m = {}
|
||||
for (const r of rows.value) m[r.iso3] = r
|
||||
|
|
@ -174,6 +198,9 @@ const pgLoading = ref(false)
|
|||
const pgErr = ref(null)
|
||||
const selectedGroup = ref(null) // set = the people-group detail is showing
|
||||
const pinRows = shallowRef([]) // [name, lat, lng, iso3, code, population, tier]
|
||||
const doxaTotal = ref(0) // every group on Doxa's roster, placed or not
|
||||
const doxaUnplaced = ref(0) // on the roster, no coordinates in this build yet
|
||||
const doxaByCountry = shallowRef({})
|
||||
const pinCodes = ref({})
|
||||
const pinBridges = ref({}) // '<iso639-3>|<ISO3 country>' -> the in-country bridge, if any
|
||||
const pinCount = ref(0)
|
||||
|
|
@ -790,7 +817,7 @@ function openGroup(g) { selectedGroup.value = g }
|
|||
function closeSheet() { selected.value = null; selectedGroup.value = null }
|
||||
|
||||
async function load() {
|
||||
const [data, adm0, pinData] = await Promise.all([
|
||||
const [data, adm0, pinData, doxaData] = await Promise.all([
|
||||
fetch(`${assetsBase.value}/tables/gospel-by-country.json`).then((r) => r.json()),
|
||||
// world-adm0 now INCLUDES India, so there is one ADM0 file and no patched
|
||||
// variant. It used to omit IND (the 100-list splits India into 10 provincial
|
||||
|
|
@ -804,12 +831,40 @@ async function load() {
|
|||
// than behind a tap. URLs are normalised into `codes` by ISO 639-3, so the 12,379
|
||||
// pins carry a code rather than 12,379 copies of the same jesusfilm.org link.
|
||||
fetch(`${assetsBase.value}/tables/people-group-pins.json`).then((r) => r.json()),
|
||||
/* ═══ THE MAP DRAWS DOXA'S ROSTER, NOT EVERY PEOPLE GROUP ON EARTH ═══════════════
|
||||
* people-group-pins.json stays exactly as it is — 12,379 groups, 8,437 verified
|
||||
* links, the whole GRN sweep — because it is the RESEARCH RECORD and the resource
|
||||
* index every sheet reads. What changed is what gets DRAWN. Doxa works the ~2,100
|
||||
* groups on pray.doxa.life, and a map of everyone buries the ones they can reach.
|
||||
* Nothing is lost; the wider set simply stops being dumped on the map. */
|
||||
fetch(`${assetsBase.value}/tables/doxa-people-groups.json`).then((r) => r.json()),
|
||||
])
|
||||
rows.value = data.countries || []
|
||||
pinCodes.value = pinData.codes || {}
|
||||
pinBridges.value = pinData.bridges || {}
|
||||
pinRows.value = pinData.pins || []
|
||||
|
||||
/* Same seven-slot shape the rest of this file already speaks, so narrowing the roster
|
||||
changes WHICH pins exist and nothing about how they are drawn, clicked or filtered. */
|
||||
const roster = (doxaData.rows || [])
|
||||
doxaTotal.value = roster.length
|
||||
pinRows.value = roster
|
||||
.filter((r) => r.mapped && r.lat != null && r.lng != null)
|
||||
.map((r) => [r.name, r.lat, r.lng, r.iso3, r.iso639_3 || '', r.population || 0, r.tier ?? 0])
|
||||
pinCount.value = pinRows.value.length
|
||||
doxaUnplaced.value = doxaTotal.value - pinCount.value
|
||||
|
||||
/* Countries Doxa actually works, and how many of their groups have nothing. A country
|
||||
outside this set is NOT "zero" — it is out of scope, and painting it at all was the
|
||||
misleading part: the United States holds three Doxa groups, so one without a resource
|
||||
read as 33% and turned the whole country amber. */
|
||||
const tally = {}
|
||||
for (const r of roster) {
|
||||
const t = (tally[r.iso3] ||= { groups: 0, none: 0, people: 0 })
|
||||
t.groups += 1
|
||||
t.people += r.population || 0
|
||||
if (r.mapped && r.tier === 0) t.none += 1
|
||||
}
|
||||
doxaByCountry.value = tally
|
||||
return adm0
|
||||
}
|
||||
|
||||
|
|
@ -827,8 +882,10 @@ function paint(map, adm0) {
|
|||
properties: {
|
||||
iso3: iso,
|
||||
band: d?.band || 'none',
|
||||
color: d ? featureColor(d) : 'rgba(0,0,0,0)',
|
||||
hasData: !!d,
|
||||
/* Keyed by ISO3, not by the old country row: scope now comes from Doxa's roster,
|
||||
and a country they do not work gets no fill rather than a "covered" colour. */
|
||||
color: featureColor(iso),
|
||||
hasData: !!doxaByCountry.value[iso],
|
||||
},
|
||||
geometry: f.geometry,
|
||||
}
|
||||
|
|
@ -1037,9 +1094,9 @@ useShadowStyles(
|
|||
// of it rather than run under it. 46px = 32px button + 3px inset + clearance.
|
||||
'@media(max-width:480px){.gm-legend{width:auto;right:46px}}' +
|
||||
'.is-dark .gm-legend{background:rgba(19,32,25,.95);color:#e5e7eb}' +
|
||||
'.gm-title{font-weight:700;font-size:12.5px;margin-bottom:5px}' +
|
||||
'.gm-title{font-weight:700;font-size:15.5px;margin-bottom:5px}' +
|
||||
'.gm-keys{display:flex;flex-wrap:wrap;gap:5px 12px}' +
|
||||
'.gm-key{display:flex;align-items:center;gap:5px;font-size:11px;white-space:nowrap}' +
|
||||
'.gm-key{display:flex;align-items:center;gap:5px;font-size:14px;white-space:nowrap}' +
|
||||
// This one label is a phrase, not a legend word, so it is allowed to wrap rather than
|
||||
// be clipped by the nowrap above.
|
||||
'.gm-key.wide{white-space:normal;text-align:left;line-height:1.25}' +
|
||||
|
|
@ -1049,17 +1106,17 @@ useShadowStyles(
|
|||
'.gm-pinkey{margin-top:7px;padding-top:6px;border-top:1px solid rgba(0,0,0,.12)}' +
|
||||
'.is-dark .gm-pinkey{border-top-color:rgba(255,255,255,.16)}' +
|
||||
'.gm-keys-btn{gap:4px 8px}' +
|
||||
'.gm-kbtn{border:1px solid transparent;background:transparent;color:inherit;font:inherit;font-size:11px;' +
|
||||
'.gm-kbtn{border:1px solid transparent;background:transparent;color:inherit;font:inherit;font-size:14px;' +
|
||||
'padding:2px 6px;border-radius:999px;cursor:pointer;min-height:24px;display:inline-flex;align-items:center;gap:5px}' +
|
||||
'.gm-kbtn:hover{background:#e9ebe6;border-color:#d1d4cd}' +
|
||||
'.gm-kbtn.on{background:#3b463d;border-color:#3b463d;color:#fff}' +
|
||||
'.gm-kbtn b{font-variant-numeric:tabular-nums;opacity:.75;font-weight:700}' +
|
||||
'.gm-filtered{margin-top:5px;font-size:10.5px;line-height:1.45;color:#3b463d}' +
|
||||
'.gm-filtered{margin-top:5px;font-size:13.5px;line-height:1.45;color:#3b463d}' +
|
||||
'.is-dark .gm-filtered{color:#e5e7eb}' +
|
||||
'.gm-kclear{border:1px solid #d1d4cd;background:#fff;color:#3b463d;font:600 10.5px inherit;' +
|
||||
'padding:3px 8px;border-radius:6px;cursor:pointer;margin-left:4px;min-height:24px}' +
|
||||
'.gm-pinkey-h{font-size:11px;font-weight:600;margin-bottom:4px}' +
|
||||
'.gm-caveat{margin-top:5px;font-size:10.5px;opacity:.72;line-height:1.4}' +
|
||||
'.gm-pinkey-h{font-size:14px;font-weight:600;margin-bottom:4px}' +
|
||||
'.gm-caveat{margin-top:5px;font-size:13.5px;opacity:.72;line-height:1.4}' +
|
||||
'.gm-prose-note{opacity:.9;font-weight:600}' +
|
||||
// DETAIL: a bottom sheet on a phone, a corner card on desktop.
|
||||
// ON A PHONE THE SHEET IS THE PAGE, and the shared pin hint sits on top of it. The hint
|
||||
|
|
@ -1078,29 +1135,29 @@ useShadowStyles(
|
|||
'@media(min-width:700px){.gm-sheet{left:auto;right:14px;bottom:14px;width:330px;border-radius:12px}}' +
|
||||
'.is-dark .gm-sheet{background:#132019;color:#e5e7eb}' +
|
||||
'.gm-sh-head{display:flex;align-items:center;gap:10px;margin-bottom:8px}' +
|
||||
'.gm-sh-name{font-weight:800;font-size:15px}' +
|
||||
'.gm-sh-name{font-weight:800;font-size:18px}' +
|
||||
'.gm-x{margin-left:auto;border:1px solid rgba(0,0,0,.16);background:transparent;color:inherit;' +
|
||||
'font:600 12px inherit;padding:6px 12px;border-radius:8px;cursor:pointer;min-height:34px}' +
|
||||
'.gm-stat{display:flex;justify-content:space-between;gap:12px;padding:4px 0;' +
|
||||
'border-bottom:1px solid rgba(0,0,0,.07);font-variant-numeric:tabular-nums}' +
|
||||
'.is-dark .gm-stat{border-bottom-color:rgba(255,255,255,.09)}' +
|
||||
'.gm-stat b{font-weight:700}' +
|
||||
'.gm-note{margin-top:9px;font-size:11px;line-height:1.5;opacity:.75}' +
|
||||
'.gm-err{position:absolute;top:50%;left:0;right:0;text-align:center;font-size:13px;opacity:.7}' +
|
||||
'.gm-note{margin-top:9px;font-size:14px;line-height:1.5;opacity:.75}' +
|
||||
'.gm-err{position:absolute;top:50%;left:0;right:0;text-align:center;font-size:16px;opacity:.7}' +
|
||||
// MODE TOGGLE — sits in the legend, because the legend is what it changes.
|
||||
'.gm-fold{width:100%;margin:0 0 6px;border:1px solid #d1d4cd;background:#f3f3f1;color:#3b463d;font:600 11px inherit;padding:6px 10px;border-radius:8px;cursor:pointer;min-height:32px}' +
|
||||
'.gm-fold:hover{background:#e9ebe6}' +
|
||||
'.gm-search{position:relative;margin-bottom:6px}' +
|
||||
'.gm-q{width:100%;box-sizing:border-box;border:1px solid #d1d4cd;border-radius:8px;background:#fff;color:#3b463d;font:12px inherit;padding:7px 26px 7px 9px;min-height:32px}' +
|
||||
'.gm-q:focus{outline:2px solid #92b195;outline-offset:1px}' +
|
||||
'.gm-qx{position:absolute;right:4px;top:50%;transform:translateY(-50%);border:0;background:transparent;color:#6d766e;font-size:16px;line-height:1;cursor:pointer;padding:4px 6px;min-height:24px}' +
|
||||
'.gm-qx{position:absolute;right:4px;top:50%;transform:translateY(-50%);border:0;background:transparent;color:#6d766e;font-size:19px;line-height:1;cursor:pointer;padding:4px 6px;min-height:24px}' +
|
||||
'.gm-qres{margin:-2px 0 7px;border:1px solid #d1d4cd;border-radius:8px;background:#fff;overflow:hidden}' +
|
||||
'.gm-qrow{display:flex;align-items:baseline;gap:6px;width:100%;text-align:left;border:0;background:transparent;font:inherit;font-size:11px;color:#3b463d;padding:6px 8px;cursor:pointer;min-height:30px;border-bottom:1px solid #eceee9}' +
|
||||
'.gm-qrow{display:flex;align-items:baseline;gap:6px;width:100%;text-align:left;border:0;background:transparent;font:inherit;font-size:14px;color:#3b463d;padding:6px 8px;cursor:pointer;min-height:30px;border-bottom:1px solid #eceee9}' +
|
||||
'.gm-qrow:last-child{border-bottom:0}' +
|
||||
'.gm-qrow:hover{background:#f3f3f1}' +
|
||||
'.gm-qn{font-weight:700}' +
|
||||
'.gm-ql{opacity:.7}' +
|
||||
'.gm-qnone,.gm-qmore{padding:6px 8px;font-size:10.5px;color:#6d766e}' +
|
||||
'.gm-qnone,.gm-qmore{padding:6px 8px;font-size:13.5px;color:#6d766e}' +
|
||||
'.gm-modes{display:flex;gap:6px;margin-bottom:6px}' +
|
||||
'.gm-mode{font:600 10.5px inherit;padding:4px 10px;border-radius:999px;cursor:pointer;' +
|
||||
'border:1px solid rgba(0,0,0,.16);background:transparent;color:inherit;min-height:26px}' +
|
||||
|
|
@ -1108,7 +1165,7 @@ useShadowStyles(
|
|||
'.is-dark .gm-mode{border-color:rgba(255,255,255,.22)}' +
|
||||
'.is-dark .gm-mode.on{background:#10b981;border-color:#10b981;color:#04241a}' +
|
||||
// RESOURCE LIST inside the sheet.
|
||||
'.gm-scope{margin:7px 0 0;font-size:11.5px;line-height:1.45;color:#3b463d}' +
|
||||
'.gm-scope{margin:7px 0 0;font-size:14.5px;line-height:1.45;color:#3b463d}' +
|
||||
'.gm-scope b{font-weight:700}' +
|
||||
'.is-dark .gm-scope{color:#e5e7eb}' +
|
||||
// Same frame as the sign-language note: a caveat, visually separate from the chips so it
|
||||
|
|
@ -1123,26 +1180,26 @@ useShadowStyles(
|
|||
'.gm-gtab.on{background:#92b195;border-color:#92b195;color:#fff}' +
|
||||
'.is-dark .gm-gtab{background:transparent;border-color:rgba(255,255,255,.22);color:#e5e7eb}' +
|
||||
'.is-dark .gm-gtab.on{background:#92b195;color:#10241a}' +
|
||||
'.gm-pg-rel{display:block;margin-top:2px;font-size:10px;color:#6d766e;font-style:italic}' +
|
||||
'.gm-pg-rel{display:block;margin-top:2px;font-size:13px;color:#6d766e;font-style:italic}' +
|
||||
'.is-dark .gm-pg-rel{color:#9fb3a4}' +
|
||||
'.gm-rel{margin:8px 0 0;padding:8px 10px;border:1px solid #d1d4cd;border-left:3px solid #92b195;' +
|
||||
'border-radius:6px;background:#fff;font-size:11.5px;line-height:1.5}' +
|
||||
'.gm-rel code{font-size:11px;background:#f3f3f1;padding:1px 4px;border-radius:3px}' +
|
||||
'border-radius:6px;background:#fff;font-size:14.5px;line-height:1.5}' +
|
||||
'.gm-rel code{font-size:14px;background:#f3f3f1;padding:1px 4px;border-radius:3px}' +
|
||||
'.is-dark .gm-rel{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);color:#e5e7eb}' +
|
||||
'.gm-sign{margin:8px 0 0;padding:8px 10px;border:1px solid #d1d4cd;border-left:3px solid #6d766e;' +
|
||||
'border-radius:8px;background:#f3f3f1;color:#3b463d;font-size:11.5px;line-height:1.5}' +
|
||||
'border-radius:8px;background:#f3f3f1;color:#3b463d;font-size:14.5px;line-height:1.5}' +
|
||||
'.is-dark .gm-sign{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);color:#e5e7eb}' +
|
||||
'.gm-pray{display:block;margin:8px 0 0;padding:9px 11px;border-radius:8px;text-decoration:none;' +
|
||||
'background:#3b463d;color:#fff;font:600 12.5px inherit;text-align:center}' +
|
||||
'.gm-pray:hover,.gm-pray:focus-visible{background:#2f382f}' +
|
||||
'.gm-pray-n{display:block;margin-top:2px;font-weight:500;font-size:11px;opacity:.85}' +
|
||||
'.gm-tablink{display:block;margin:10px 0 2px;padding:9px 12px;border:1px solid #d1d4cd;border-left:3px solid #92b195;border-radius:8px;background:#f3f3f1;color:#3b463d;font-weight:600;font-size:12.5px;text-decoration:none;min-height:36px;box-sizing:border-box}' +
|
||||
'.gm-pray-n{display:block;margin-top:2px;font-weight:500;font-size:14px;opacity:.85}' +
|
||||
'.gm-tablink{display:block;margin:10px 0 2px;padding:9px 12px;border:1px solid #d1d4cd;border-left:3px solid #92b195;border-radius:8px;background:#f3f3f1;color:#3b463d;font-weight:600;font-size:15.5px;text-decoration:none;min-height:36px;box-sizing:border-box}' +
|
||||
'.gm-tablink:hover{background:#e9ebe6}' +
|
||||
'.is-dark .gm-tablink{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);color:#e5e7eb}' +
|
||||
'.gm-stat-alt span{opacity:.72}' +
|
||||
'.gm-sec{margin-top:12px}' +
|
||||
'.gm-sec-h{font-weight:800;font-size:12px;letter-spacing:.02em;margin-bottom:6px}' +
|
||||
'.gm-load{padding:10px 0;font-size:11.5px;opacity:.65}' +
|
||||
'.gm-sec-h{font-weight:800;font-size:15px;letter-spacing:.02em;margin-bottom:6px}' +
|
||||
'.gm-load{padding:10px 0;font-size:14.5px;opacity:.65}' +
|
||||
// .gm-pg is now the SHELL only. The resource rows are a <div> shell (so the chips
|
||||
// inside can be real anchors) while the no-resource rows are still a <button>; both
|
||||
// must look identical, so the shell carries the box and .gm-pg-btn/.gm-pg-open carry
|
||||
|
|
@ -1157,8 +1214,8 @@ useShadowStyles(
|
|||
// look, so the row still reads as one tap-target for the name + language line.
|
||||
'.gm-pg-open{display:block;width:100%;text-align:left;padding:0;margin:0;border:0;' +
|
||||
'background:transparent;color:inherit;font:inherit;cursor:pointer}' +
|
||||
'.gm-pg-n{display:block;font-weight:700;font-size:12.5px}' +
|
||||
'.gm-pg-l{display:block;font-size:11px;opacity:.75;margin-top:1px}' +
|
||||
'.gm-pg-n{display:block;font-weight:700;font-size:15.5px}' +
|
||||
'.gm-pg-l{display:block;font-size:14px;opacity:.75;margin-top:1px}' +
|
||||
'.gm-code{display:inline-block;font:600 10px ui-monospace,SFMono-Regular,Menlo,monospace;' +
|
||||
'padding:1px 5px;border-radius:4px;background:rgba(0,0,0,.09);margin-left:5px;vertical-align:1px}' +
|
||||
'.is-dark .gm-code{background:rgba(255,255,255,.12)}' +
|
||||
|
|
@ -1167,7 +1224,7 @@ useShadowStyles(
|
|||
// entry can be "Aligned Bible (translationCore-Create-BCS) - Door43 - Portions of both",
|
||||
// which simply ran off the edge of a 330px sheet and could not be read at all. Long chips
|
||||
// wrap instead; max-width keeps them inside the panel either way.
|
||||
'.gm-chip{font-size:10px;padding:2px 7px;border-radius:999px;white-space:nowrap;max-width:100%;' +
|
||||
'.gm-chip{font-size:13px;padding:2px 7px;border-radius:999px;white-space:nowrap;max-width:100%;' +
|
||||
'background:rgba(0,0,0,.06);text-decoration:none;color:inherit;border:1px solid transparent}' +
|
||||
'.gm-chip.wrap{white-space:normal;overflow-wrap:anywhere;line-height:1.35}' +
|
||||
'.is-dark .gm-chip{background:rgba(255,255,255,.08)}' +
|
||||
|
|
@ -1183,11 +1240,11 @@ useShadowStyles(
|
|||
// "unknown") must keep looking inert, because it is.
|
||||
'a.gm-chip{cursor:pointer}' +
|
||||
'a.gm-chip:hover,a.gm-chip:focus-visible{text-decoration:underline}' +
|
||||
'.gm-none{font-size:11px;opacity:.7}' +
|
||||
'.gm-none{font-size:14px;opacity:.7}' +
|
||||
'.gm-back{border:1px solid rgba(0,0,0,.16);background:transparent;color:inherit;' +
|
||||
'font:600 11px inherit;padding:5px 11px;border-radius:8px;cursor:pointer;min-height:30px}' +
|
||||
'.is-dark .gm-back{border-color:rgba(255,255,255,.22)}' +
|
||||
'.gm-more{font-size:11px;opacity:.66;margin-top:2px}',
|
||||
'.gm-more{font-size:14px;opacity:.66;margin-top:2px}',
|
||||
'doxa-gospel-map',
|
||||
)
|
||||
</script>
|
||||
|
|
@ -1327,10 +1384,12 @@ useShadowStyles(
|
|||
two datasets: the fill came from a curated set that had one group for Papua New
|
||||
Guinea while the map drew 831 pins there, so 135 of 170 countries were coloured
|
||||
by something the dots on top of them contradicted. -->
|
||||
<b>A dot and the country under it mean different things.</b> The country is an AVERAGE
|
||||
across its groups; a dot is that ONE group's own language. So a red dot can sit on a green
|
||||
country — that is the case worth finding, not a rendering fault. Both are counted from the
|
||||
same index, so the colour and the dots can no longer disagree.
|
||||
<b>A dot and the country under it mean different things.</b> A dot is ONE of Doxa's
|
||||
people groups and its own language. A country is a COUNT of those groups — how many of
|
||||
Doxa's are there with nothing found — never a percentage: 106 of these 175 countries hold
|
||||
one or two groups, and a share over a denominator of two says nothing. A country Doxa does
|
||||
not work is left UNCOLOURED rather than drawn as covered, because this dataset has no
|
||||
finding about it either way.
|
||||
<br>“None found” = not in the pools checked (eBible, PNG.Bible, JESUS Film, Door43,
|
||||
Faith Comes By Hearing, Scripture Earth and Global Recordings Network). GRN is reached
|
||||
through their 5fish site, and that sweep is now <b>finished for every group in the gap</b>
|
||||
|
|
@ -1342,7 +1401,7 @@ useShadowStyles(
|
|||
<template v-if="mode === 'coverage'">
|
||||
A “found” colour is equally bounded: it counts what these pools list, not everything that exists.
|
||||
</template>
|
||||
<br>Either way the colour is only a country average — tap a country to see the actual resources, group by group.
|
||||
<br>Tap a country to see the actual resources, group by group.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
/**
|
||||
* fetch-doxa-people-groups.mjs — the 2,107 people groups DOXA actually prays for.
|
||||
*
|
||||
* ═══ WHY THIS EXISTS ════════════════════════════════════════════════════════════════════
|
||||
* This map was built on the full 12,379-pin people-groups dataset, and the Driver's reading
|
||||
* of it was blunt and correct: it is MISLEADING for the job it is meant to do. Doxa does not
|
||||
* work 12,379 groups. They work the ~2,100 on pray.doxa.life, and a map that shows every
|
||||
* group on earth buries the ones they can actually reach — and paints countries by a gap
|
||||
* that is not theirs.
|
||||
*
|
||||
* ═══ THE MEASUREMENT THAT SETTLES THE COLOUR ARGUMENT ═══════════════════════════════════
|
||||
* Measured 2026-09-25 against the live API:
|
||||
* 2,107 Doxa people groups across 175 countries
|
||||
* India 855 · China 154 · Sudan 105 · Brazil 103 · Laos 95
|
||||
* BUT 106 of those 175 countries hold only ONE OR TWO Doxa groups
|
||||
* and the United States holds THREE — Coquille, Pequots, Umpqua
|
||||
* The old choropleth paints a country by the PERCENTAGE of its groups with nothing found.
|
||||
* Over a denominator of three, one group without a resource is 33% and the whole United
|
||||
* States turns amber. That is the "mainly yellow map" complaint, and it is not a palette
|
||||
* problem underneath — it is a statistic that means nothing at n=3 being drawn as if it
|
||||
* meant something. A prettier ramp over the same arithmetic would be a prettier lie.
|
||||
*
|
||||
* ═══ WHAT THIS DOES AND DOES NOT KEEP ═══════════════════════════════════════════════════
|
||||
* Nothing found for the other 10,000 groups is thrown away — every index, every verified
|
||||
* link and every sweep result stays exactly where it is in data/tables/. This file adds the
|
||||
* DOXA ROSTER as its own table so the map can be narrowed to it without losing the research
|
||||
* behind it. The wider tables remain the record; the map stops being the place they are
|
||||
* dumped.
|
||||
*
|
||||
* ═══ THE JOIN, AND WHAT IT COSTS ════════════════════════════════════════════════════════
|
||||
* The API carries no coordinates and no ISO 639-3 code, so both come from the pin index by
|
||||
* (name, country) — the same key scripts/fetch-pray-index.mjs already uses, and the one join
|
||||
* on this map that is deliberately not by language, because you pray for a people in a
|
||||
* place. Measured: 1,724 of 2,107 match a pin and EVERY ONE of those carries a language
|
||||
* code. 383 do not match and are recorded with `mapped: false` rather than dropped — they
|
||||
* are Doxa's groups whether or not this build can place them, and a roster that silently
|
||||
* loses 18% of them would be its own kind of lie.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { writeSnapshot } from './_snapshot-guard.mjs'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const REPO = resolve(HERE, '../../..')
|
||||
const PINS = resolve(REPO, 'data/tables/people-group-pins.json')
|
||||
const OUT = resolve(REPO, 'data/tables/doxa-people-groups.json')
|
||||
const API = 'https://pray.doxa.life/api/people-groups/list'
|
||||
const SITE = 'https://pray.doxa.life'
|
||||
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'
|
||||
|
||||
const res = await fetch(API, { headers: { 'user-agent': UA }, signal: AbortSignal.timeout(90000) })
|
||||
if (!res.ok) {
|
||||
console.error(`DOXA-PG: ABORT — the roster API answered HTTP ${res.status}. Refusing to write a partial roster.`)
|
||||
process.exit(1)
|
||||
}
|
||||
const { posts } = await res.json()
|
||||
if (!Array.isArray(posts) || posts.length < 1000) {
|
||||
console.error(`DOXA-PG: ABORT — the roster came back with ${posts?.length ?? 0} entries. That is not the roster.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const { codes, pins } = JSON.parse(readFileSync(PINS, 'utf8'))
|
||||
/**
|
||||
* ═══ THE NAME JOIN, AND THE ONE VARIATION IT IS SAFE TO ABSORB ══════════════════════════
|
||||
*
|
||||
* An exact name match left 383 of Doxa's groups unplaced — 18% of their roster invisible on
|
||||
* the map and absent from every count. Reading the misses rather than guessing at them, one
|
||||
* class is a spelling convention and nothing more: the roster writes the qualifier FIRST and
|
||||
* the pin dataset writes it LAST, around a comma.
|
||||
*
|
||||
* Acre Arara <-> Arara, Acre
|
||||
* Ashkuni Nuristani <-> Nuristani Ashkuni
|
||||
* Banka Samogho <-> Samogho, Banka
|
||||
*
|
||||
* Same words, same country, different order. Matching on the SORTED TOKEN SET absorbs
|
||||
* exactly that and nothing looser. Measured: 94 recovered this way plus 4 that only needed
|
||||
* diacritics folded, and ZERO ambiguous — every one resolved to a single pin inside its own
|
||||
* country.
|
||||
*
|
||||
* WHAT IT REFUSES, and why that matters more than what it accepts. Fuzzy matching would
|
||||
* "recover" far more and would be wrong. Doxa lists Akha in Laos; the pins carry Akha Jipea,
|
||||
* Akha Jaweuy and Akha Piso — a parent against three sub-varieties, and picking one would
|
||||
* invent a language for a people. Doxa lists Akeu in China; the nearest pin is Ake, a
|
||||
* different language. So: same country, identical token set, and UNIQUE. Two candidates is a
|
||||
* refusal, not a coin toss. 285 have no candidate at all and stay unplaced, which is the
|
||||
* honest state — they need a source this build does not have, not a cleverer string match.
|
||||
*/
|
||||
const fold = (n) => String(n || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim()
|
||||
const tokenSet = (n) => fold(n).split(' ').filter(Boolean).sort().join(' ')
|
||||
const key = (n, c) => `${fold(n)}|${String(c || '').toUpperCase()}`
|
||||
const tkey = (n, c) => `${tokenSet(n)}|${String(c || '').toUpperCase()}`
|
||||
const pinBy = new Map()
|
||||
const pinByTokens = new Map()
|
||||
for (let i = 0; i < pins.length; i++) {
|
||||
const k = key(pins[i][0], pins[i][3])
|
||||
if (!pinBy.has(k)) pinBy.set(k, [])
|
||||
pinBy.get(k).push(i)
|
||||
const t = tkey(pins[i][0], pins[i][3])
|
||||
if (!pinByTokens.has(t)) pinByTokens.set(t, [])
|
||||
pinByTokens.get(t).push(i)
|
||||
}
|
||||
|
||||
const val = (f) => (f && typeof f === 'object' ? f.value : f) || ''
|
||||
const lab = (f) => (f && typeof f === 'object' ? f.label : f) || ''
|
||||
|
||||
let mapped = 0, ambiguous = 0, unmapped = 0, reordered = 0
|
||||
const rows = posts.map((q) => {
|
||||
const iso3 = val(q.country_code)
|
||||
let how = 'exact'
|
||||
let hit = pinBy.get(key(q.name, iso3))
|
||||
if (!hit) {
|
||||
const t = pinByTokens.get(tkey(q.name, iso3))
|
||||
/* Unique or nothing. A second candidate means the roster name and the pin name are not
|
||||
the same people, and there is no tie to break that would not be a guess. */
|
||||
if (t && t.length === 1) { hit = t; how = 'reordered'; reordered++ }
|
||||
else if (t && t.length > 1) ambiguous++
|
||||
} else if (hit.length > 1) ambiguous++
|
||||
const p = hit ? pins[hit[0]] : null
|
||||
if (p) mapped++; else { unmapped++; how = '' }
|
||||
const code = p ? p[4] || '' : ''
|
||||
const c = code ? codes[code] : null
|
||||
return {
|
||||
name: q.name,
|
||||
slug: q.slug,
|
||||
pray_url: `${SITE}/${q.slug}?source=doxalife`,
|
||||
iso3,
|
||||
country: lab(q.country_code),
|
||||
region: lab(q.wagf_region),
|
||||
block: lab(q.wagf_block),
|
||||
rop1: lab(q.rop1),
|
||||
religion: lab(q.religion),
|
||||
population: Number(q.population) || 0,
|
||||
people_praying: Number(q.people_praying) || 0,
|
||||
people_committed: Number(q.people_committed) || 0,
|
||||
adopted_by_churches: Number(q.adopted_by_churches) || 0,
|
||||
/* From the pin index, by (name, country). Empty where this build cannot place them. */
|
||||
mapped: !!p,
|
||||
/* How this row found its pin: exact name, or the same words in the other order. Empty
|
||||
when it did not. Recorded so a reader can audit the join instead of trusting it. */
|
||||
matched_by: how,
|
||||
lat: p ? p[1] : null,
|
||||
lng: p ? p[2] : null,
|
||||
iso639_3: code,
|
||||
language: c ? c.l || '' : '',
|
||||
tier: p ? p[6] : null,
|
||||
}
|
||||
})
|
||||
|
||||
const byCountry = {}
|
||||
for (const r of rows) byCountry[r.iso3] = (byCountry[r.iso3] || 0) + 1
|
||||
const thin = Object.values(byCountry).filter((n) => n <= 2).length
|
||||
|
||||
writeSnapshot(
|
||||
OUT,
|
||||
`${JSON.stringify({
|
||||
_note:
|
||||
'The people groups DOXA prays for, from pray.doxa.life. THIS is what the gospel map ' +
|
||||
'draws — not the full 12,379-group dataset, which stays in people-group-pins.json as ' +
|
||||
'the research record. mapped:false means the roster carries them but this build has no ' +
|
||||
'coordinates for them yet; they are kept, never dropped.',
|
||||
source: API,
|
||||
generated: new Date().toISOString().slice(0, 10),
|
||||
counts: {
|
||||
groups: rows.length,
|
||||
countries: Object.keys(byCountry).length,
|
||||
mapped,
|
||||
unmapped,
|
||||
ambiguousNameCountryKeys: ambiguous,
|
||||
matchedByReorder: reordered,
|
||||
countriesWithTwoOrFewer: thin,
|
||||
},
|
||||
rows,
|
||||
}, null, 1)}\n`,
|
||||
(j) => (j.rows || []).length,
|
||||
'Doxa people group(s)',
|
||||
)
|
||||
console.log(`DOXA-PG: ${rows.length} group(s) across ${Object.keys(byCountry).length} countries`)
|
||||
console.log(` placed on the map: ${mapped} no coordinates yet: ${unmapped} ambiguous, refused: ${ambiguous}`)
|
||||
console.log(` of those placed, ${reordered} matched on the same words in the other order`)
|
||||
console.log(` countries holding 2 or fewer Doxa groups: ${thin} — a percentage over that denominator is not a measurement`)
|
||||
console.log(`wrote ${OUT}`)
|
||||
46370
public/doxa-maps/assets/tables/doxa-people-groups.json
generated
Normal file
46370
public/doxa-maps/assets/tables/doxa-people-groups.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <gospel-resources-map> custom element. -->
|
||||
<script src="../gospel-resources.js" integrity="sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
12
public/doxa-maps/manifest.json
generated
12
public/doxa-maps/manifest.json
generated
|
|
@ -596,7 +596,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "doxa-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD",
|
||||
"integrity": "sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA",
|
||||
"elements": [
|
||||
"gospel-resources-map"
|
||||
],
|
||||
|
|
@ -616,7 +616,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/charts/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"charts\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -630,7 +630,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/data-tables/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"data-tables\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -644,7 +644,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/gospel-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"gospel-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -658,7 +658,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -672,7 +672,7 @@
|
|||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./gospel-resources/resources-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-P6ZUc86rqFozVyhG7fXGOkRz6bj423JECbKJDYor5VwmnFJK1mG+smLK/bEx6ydD\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-fuJG/0dsGRtcKsAupwc+foQcZCiYKtxBrOhVS+Z05kqOXCzPLnL0VWLIxgUljTEA\" crossorigin=\"anonymous\"></script>\n<gospel-resources-map profile-config='{\"profile\":\"resources-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></gospel-resources-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue