The gap by country is now a table you can export, not just a sheet you can read
gospel-by-country carries 242 countries with real per-country counts, and the map uses them for the choropleth and the "where the gap concentrates" sheet. The tables profile had no country table at all, so those numbers lived only inside the map — readable, not exportable — while every other tab there offers CSV and JSON. A mobiliser deciding where to send a team cannot work from a sheet they have to screenshot. Derived, not a second source: every row is read out of gospel-by-country, which is itself refreshed from the pin index, so the table and the map cannot disagree. Three columns because they rank differently and each is somebody's question — groups with nothing, people with nothing, and the share of the country each represents. The default sort is by groups, and the share column is what makes rows like Vanuatu legible: 79 groups with nothing out of 114, 69.3%, which ranks ninth by count and first by share. Region is carried through so the table can be grouped the same way the map's region tab ranks. Curated columns come through as-is and stay null where the curated analysis does not cover the country — null, never zero, because zero is a claim this build cannot make. Verified against the PIN INDEX rather than against the table being tested: 242 countries with pins, 2,901 groups with nothing, worst is PNG at 469. The rendered tab reports 242 rows and its first row reads Papua New Guinea / PNG / Oceania / 831 / 469 / 56.4, with 650 real cells. No page errors. Screenshot read.
This commit is contained in:
parent
04c8a816d0
commit
ffa49709ac
13 changed files with 7545 additions and 13 deletions
3711
data/tables/country-gap.json
Normal file
3711
data/tables/country-gap.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -73,6 +73,10 @@
|
|||
{
|
||||
"name": "gospel-gaps",
|
||||
"rows": 2901
|
||||
},
|
||||
{
|
||||
"name": "country-gap",
|
||||
"rows": 242
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -41,6 +41,11 @@ const TABS = [
|
|||
// that one is one row per group across the curated 2,106-record set and structurally
|
||||
// cannot see these groups.
|
||||
{ id: 'nothing', label: 'Where Nothing Was Found', file: 'gospel-gaps' },
|
||||
// The same gap, aggregated to the place a team gets sent. It existed only inside the
|
||||
// map's sheet — readable, not exportable — while every other tab here offers CSV and
|
||||
// JSON. Derived from gospel-by-country, which is refreshed from the pin index, so it
|
||||
// cannot drift from the map above it.
|
||||
{ id: 'countrygap', label: 'The Gap by Country', file: 'country-gap' },
|
||||
// GAPS & OPPORTUNITIES leads: it is the table that answers "where can the work
|
||||
// actually happen", which is the whole point of the corpus.
|
||||
{ id: 'gaps', label: 'Gaps & Opportunities', file: 'gaps-and-opportunities' },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-country-gap-table.mjs — the country aggregate as an exportable table.
|
||||
*
|
||||
* WHY. gospel-by-country now carries 242 countries with real per-country counts, and the
|
||||
* map uses them for the choropleth and the "where the gap concentrates" sheet. But the
|
||||
* tables profile had no country table at all, so those numbers existed only inside the map
|
||||
* — readable, not exportable. A mobiliser deciding where to send a team cannot work from a
|
||||
* sheet they have to screenshot; the other seven tabs all offer CSV and JSON.
|
||||
*
|
||||
* It is DERIVED, not a second source. Every row is read out of gospel-by-country.json,
|
||||
* which is itself refreshed from the pin index, so this cannot drift from the map: the
|
||||
* coverage gate's clause C recomputes reach off the pins and would catch it if it did.
|
||||
*
|
||||
* WHAT THE COLUMNS SAY. Three different questions, because they disagree and each is
|
||||
* somebody's: how many groups have nothing (send translators), how many PEOPLE those are
|
||||
* (send broadcasters), and what SHARE of the country that is (Oceania is half unreached by
|
||||
* group and tiny by population). The curated columns are carried through as-is and left
|
||||
* null where this build does not know them — null, never zero, which would be a claim.
|
||||
*/
|
||||
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 SRC = resolve(REPO, 'data/tables/gospel-by-country.json')
|
||||
const OUT = resolve(REPO, 'data/tables/country-gap.json')
|
||||
|
||||
const src = JSON.parse(readFileSync(SRC, 'utf8'))
|
||||
const rows = (src.countries || [])
|
||||
.filter((c) => c.groups > 0)
|
||||
.map((c) => ({
|
||||
country: c.country || c.iso3,
|
||||
iso3: c.iso3,
|
||||
region: c.region || '',
|
||||
people_groups: c.groups,
|
||||
groups_with_nothing: c.no_resource,
|
||||
pct_with_nothing: c.pct_no_resource,
|
||||
people_with_nothing: c.pop_no_resource,
|
||||
groups_with_scripture: c.scripture,
|
||||
groups_with_film: c.film,
|
||||
band: c.band,
|
||||
growing_unreached: c.growing_unreached === undefined ? null : c.growing_unreached,
|
||||
deaths_day: c.deaths_day === undefined ? null : c.deaths_day,
|
||||
basis: c.from_pins_only ? 'pins only — no curated country analysis for this country' : 'pins, with curated country analysis',
|
||||
}))
|
||||
.sort((a, b) => b.groups_with_nothing - a.groups_with_nothing || b.people_with_nothing - a.people_with_nothing)
|
||||
|
||||
const columns = [
|
||||
{ title: 'Country', field: 'country', type: 'string' },
|
||||
{ title: 'ISO3', field: 'iso3', type: 'string' },
|
||||
{ title: 'Region', field: 'region', type: 'string' },
|
||||
{ title: 'People groups', field: 'people_groups', type: 'number' },
|
||||
{ title: 'Groups with nothing', field: 'groups_with_nothing', type: 'number' },
|
||||
{ title: '% with nothing', field: 'pct_with_nothing', type: 'number' },
|
||||
{ title: 'People with nothing', field: 'people_with_nothing', type: 'number' },
|
||||
{ title: 'Groups with Scripture', field: 'groups_with_scripture', type: 'number' },
|
||||
{ title: 'Groups with film', field: 'groups_with_film', type: 'number' },
|
||||
{ title: 'Band', field: 'band', type: 'string' },
|
||||
{ title: 'Growing & unreached', field: 'growing_unreached', type: 'number' },
|
||||
{ title: 'Est. deaths/day', field: 'deaths_day', type: 'number' },
|
||||
{ title: 'Basis', field: 'basis', type: 'string' },
|
||||
]
|
||||
|
||||
const tot = rows.reduce((a, r) => a + r.groups_with_nothing, 0)
|
||||
const pop = rows.reduce((a, r) => a + r.people_with_nothing, 0)
|
||||
const byRegion = {}
|
||||
for (const r of rows) {
|
||||
const e = (byRegion[r.region || '—'] ||= { groups: 0, none: 0 })
|
||||
e.groups += r.people_groups
|
||||
e.none += r.groups_with_nothing
|
||||
}
|
||||
const regionLine = Object.entries(byRegion)
|
||||
.map(([k, v]) => [k, v, v.groups ? (100 * v.none) / v.groups : 0])
|
||||
.sort((a, b) => b[2] - a[2])
|
||||
.map(([k, v, p]) => `${k} ${p.toFixed(1)}% (${v.none} of ${v.groups})`)
|
||||
.join(' · ')
|
||||
|
||||
writeSnapshot(
|
||||
OUT,
|
||||
`${JSON.stringify({
|
||||
columns,
|
||||
meta: {
|
||||
title: 'The gap by country',
|
||||
grain: 'one row per country that has at least one people group on this map',
|
||||
generated: new Date().toISOString().slice(0, 10),
|
||||
measured: `${rows.length} countries · ${tot.toLocaleString()} groups with nothing found · ${pop.toLocaleString()} people`,
|
||||
by_region: regionLine,
|
||||
three_questions: 'Groups with nothing, people with nothing, and the share of the country each represents. They rank differently on purpose — Papua New Guinea has the most groups and few people, China the reverse, and Oceania has the worst share of any region while being the smallest by population.',
|
||||
source: 'data/tables/gospel-by-country.json, which is itself refreshed from data/tables/people-group-pins.json — the same index the map draws, so this table and the map cannot disagree.',
|
||||
curated_columns: 'growing_unreached and deaths_day come from the curated country analysis and are null where it does not cover the country. Null, never zero: zero is a claim this build cannot make.',
|
||||
},
|
||||
rows,
|
||||
}, null, 1)}\n`,
|
||||
(j) => (j.rows || []).length,
|
||||
'country-gap rows',
|
||||
)
|
||||
console.log(`country-gap: ${rows.length} countries · ${tot.toLocaleString()} groups with nothing · ${pop.toLocaleString()} people`)
|
||||
console.log(` by region: ${regionLine}`)
|
||||
3711
public/doxa-maps/assets/tables/country-gap.json
generated
Normal file
3711
public/doxa-maps/assets/tables/country-gap.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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG" 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-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk" crossorigin="anonymous"></script>
|
||||
<script src="../gospel-resources.js" integrity="sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG" 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
|
|
@ -591,7 +591,7 @@
|
|||
"bundle": "gospel-resources",
|
||||
"scriptUrl": "./gospel-resources/gospel-resources.js",
|
||||
"output": "doxa-maps/gospel-resources/gospel-resources.js",
|
||||
"integrity": "sha384-WZSlVBydYxeLPK42l4lhgBPjrSOBIIyw33bAuh5kD7c6uKgWMramyveBq5Z9G5Zk",
|
||||
"integrity": "sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG",
|
||||
"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-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>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG\" 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-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>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG\" 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-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>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG\" 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-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>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG\" 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-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>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./gospel-resources/gospel-resources.js\" integrity=\"sha384-CmN5mxV23d/9EjjiFYmjAp3wNs9izSW8X0nwgUPBk19XghuuGD0LnmsmmehdqVdG\" 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