Oceania is 51% unreached by group, and nothing on this map said so

region was empty on all 242 country rows, so the map had no way to answer "what share of
this part of the world is unreached" — only how many groups a country holds and how many
people live in them.

The source carries UNm49Region, and it was verified before being trusted: only 2, 9, 19,
142 and 150 appear — exactly the five UN M49 top-level regions — every country maps to
exactly ONE of them with 0 conflicts, and the five sum to 12,379, the whole corpus. So
the code resolves to a real name. It is backfilled on every row, not just the new ones,
and all 242 now carry a region with none blank.

The rate is a genuinely different answer from the two already on offer:

  Oceania   51.2% of its 1,325 groups have nothing —   679 groups —   3.1M people
  Africa    30.0% of 3,074                         —   923         —  68.2M
  Asia      19.3% of 5,073                         —   978         — 198.7M
  Americas  11.4% of 2,117                         —   241         —   6.1M
  Europe    10.1% of   790                         —    80         —  11.6M

Oceania is half unreached by group and the smallest population of the five; Asia is the
reverse on both. Ranked by rate, because count and population are what the other two tabs
already say and a third copy of either would be noise. All three are offered rather than
one picked for the reader.

The rows are plain divs, not buttons: a region is not somewhere this map can fly to, and
a control that looks clickable and does nothing is worse than a row that does not pretend.

Verified against an offline computation over the same index: all five regions match
exactly on rate, group count and population. The sheet header is now three-way rather
than showing the country prose in region mode. All five profiles still mount clean.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 15:27:30 +00:00
parent cde152bde7
commit c0a1cec24a
12 changed files with 572 additions and 523 deletions

File diff suppressed because it is too large Load diff

View file

@ -307,6 +307,29 @@ const gapList = computed(() => {
// Papua New Guinea has 469 groups with nothing but only 2.2M people, China has 125 groups
// and 150.3M. Ranking by either one alone hides the other question.
const gapBy = ref('groups')
// A THIRD AXIS: the RATE. The country tab ranks by how many groups a country holds and
// shows population beside it; neither answers "what share of this place is unreached".
// Measured, the three disagree completely and each is somebody's question:
// Oceania 51.2% of its groups have nothing — the worst rate anywhere — but 3.1M people
// Asia 19.3%, and 198.7M people with nothing — by far the largest population
// Africa 30.0%, 923 groups — the most after Asia
// Ranked by rate here, because count and population are already answered by the other two
// tabs and a third copy of either would be noise.
const gapRegions = computed(() => {
if (!showGapList.value) return []
const by = {}
for (const r of pinRows.value) {
const reg = byIso.value[r[3]]?.region || ''
if (!reg) continue
const e = (by[reg] = by[reg] || { region: reg, groups: 0, none: 0, pop: 0 })
e.groups++
if (r[6] === 0) { e.none++; e.pop += r[5] || 0 }
}
return Object.values(by)
.map((e) => ({ ...e, pct: e.groups ? Math.round((1000 * e.none) / e.groups) / 10 : 0 }))
.sort((a, b) => b.pct - a.pct)
})
const gapCountries = computed(() => {
if (!showGapList.value) return []
const by = {}
@ -1016,13 +1039,19 @@ useShadowStyles(
<!-- ITEM 0 — the ranked gap: the largest groups with nothing found anywhere. -->
<template v-if="showGapList && !selected">
<div class="gm-sh-head">
<span class="gm-sh-name">{{ gapBy === 'countries' ? 'Where the gap concentrates' : 'Largest groups with nothing found' }}</span>
<span class="gm-sh-name">{{ gapBy === 'groups' ? 'Largest groups with nothing found' : 'Where the gap concentrates' }}</span>
<button type="button" class="gm-x" @click="closeGapList">Close</button>
</div>
<p class="gm-note" style="margin-top:0">
{{ fmt(pinTally.none) }} people groups have no Scripture, audio, film, Bible video or
Door43 resource in the pools checked — {{ fmt(gapPeople) }} people.
<template v-if="gapBy === 'countries'">
<template v-if="gapBy === 'regions'">
The five UN regions, ranked by the SHARE of their groups with nothing — the one
question the other two tabs cannot answer. Oceania is half unreached by group
and small by population; Asia is the reverse. Rate, count and population each
point somewhere different, and all three are here rather than one chosen for you.
</template>
<template v-else-if="gapBy === 'countries'">
The top 40 countries by how many of those groups they hold; tap one to open it.
The two columns disagree on purpose — Papua New Guinea has the most groups and
few people, China the reverse — and which one matters depends on whether you are
@ -1045,9 +1074,21 @@ useShadowStyles(
@click="gapBy = 'groups'">Largest groups</button>
<button type="button" class="gm-gtab" :class="{ on: gapBy === 'countries' }"
@click="gapBy = 'countries'">By country</button>
<button type="button" class="gm-gtab" :class="{ on: gapBy === 'regions' }"
@click="gapBy = 'regions'">By region</button>
</div>
<template v-if="gapBy === 'countries'">
<!-- Not buttons: a region is not a place this map can fly to, and a control that
looks clickable and does nothing is worse than a plain row. -->
<template v-if="gapBy === 'regions'">
<div v-for="r in gapRegions" :key="r.region" class="gm-pg">
<span class="gm-pg-n">{{ r.region }}</span>
<span class="gm-pg-l"><b>{{ r.pct }}%</b> of its {{ fmt(r.groups) }} groups have nothing
· {{ fmt(r.none) }} groups · {{ fmt(r.pop) }} people</span>
</div>
</template>
<template v-else-if="gapBy === 'countries'">
<button v-for="c in gapCountries" :key="c.iso3" type="button" class="gm-pg gm-pg-btn"
@click="closeGapList(); openCountry(c.iso3)">
<span class="gm-pg-n">{{ c.country }}</span>

View file

@ -39,7 +39,9 @@ const j = JSON.parse(readFileSync(OUT, 'utf8'))
// Real names, from the same source the pins are built from. A row labelled "CAN" would
// render "CAN" as the country sheet's title; people-groups-slim carries CountryName and
// UNm49Region per group, so there is no reason to show a code.
const M49 = { 2: 'Africa', 9: 'Oceania', 19: 'Americas', 142: 'Asia', 150: 'Europe' }
const nameOf = {}
const regionOf = {}
try {
const raw = JSON.parse(readFileSync(resolve(REPO, 'data/people-groups/people-groups-slim.json'), 'utf8'))
const rows = Array.isArray(raw) ? raw : Object.values(raw).find(Array.isArray)
@ -47,8 +49,12 @@ try {
const i = r.ISOAlpha3
if (!i) continue
if (!nameOf[i] && r.CountryName) nameOf[i] = String(r.CountryName).trim()
// UNm49Region is a NUMERIC region code (19, 142, ...), not a name. Rendering "19" as a
// region is worse than rendering nothing, and every existing row carries region: ''.
// UNm49Region is a NUMERIC code, and the five that appear are exactly the UN M49
// top-level regions. Verified before trusting it: only 2/9/19/142/150 occur, every
// country maps to exactly ONE of them (0 conflicts across 12,379 groups), and the five
// sum to 12,379 — the whole corpus. So the code is resolvable to a real name, and "19"
// on its own is not.
if (!regionOf[i] && r.UNm49Region) regionOf[i] = M49[String(r.UNm49Region).trim()] || ''
}
} catch { /* names are a nicety; the bands are the point */ }
@ -80,7 +86,7 @@ for (const [iso3, e] of Object.entries(agg)) {
j.countries.push({
iso3,
country: nameOf[iso3] || iso3,
region: '',
region: regionOf[iso3] || '',
groups: 0, no_resource: 0, pop_no_resource: 0, scripture: 0, film: 0,
growing_unreached: null, deaths_day: null, growth: null,
pct_no_resource: 0, band: 'covered',
@ -103,6 +109,8 @@ for (const c of j.countries) {
c.film = e.film
c.pct_no_resource = pct
c.band = b
// Backfilled on every row, not only new ones: region was empty on all 170 originals.
if (!c.region && regionOf[c.iso3]) c.region = regionOf[c.iso3]
}
j.stats = {

File diff suppressed because it is too large Load diff

View file

@ -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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -591,7 +591,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT",
"integrity": "sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk",
"elements": [
"gospel-resources-map"
],
@ -611,7 +611,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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT\" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk\" 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>"
}
},
{
@ -625,7 +625,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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT\" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk\" 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>"
}
},
{
@ -639,7 +639,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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT\" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk\" 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>"
}
},
{
@ -653,7 +653,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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT\" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk\" 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>"
}
},
{
@ -667,7 +667,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-9ouuqJx7txluDdcQQCm7xvtmSGJh25LsGgNo7kBB63YtXQNYKrg5hkOoHOJsl5PT\" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk\" 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>"
}
}
]