Let the key filter the map, so the gap can be looked at on its own

The map could say what one group has, on tap. It could not answer the question it
exists for — show me only the ones with nothing — so the 3,444 groups that still have
no resource were spread through 12,379 dots and could not be seen as a pattern.

THE KEY IS THE FILTER. A reader who has just worked out what a colour means is exactly
the reader who wants to see only that colour, so the legend entries became the controls
rather than adding a second row beneath them. Each carries its live count: 4,711 with
Scripture and film, 4,224 with some resource, 3,444 with nothing — summing to 12,379,
which is the check that the three buckets partition the set rather than overlap it.

FILTERED ON THE LAYER, NOT THE SOURCE. setFilter is a GL-side predicate, so all 12,379
features stay on the GPU and nothing is re-parsed or re-uploaded on a toggle. Rebuilding
the FeatureCollection per click would re-upload a megabyte to redraw a subset it already
holds.

Clicking an active filter clears it, and the state line names what is hidden with a
"Show all 12,379" out — a filtered map that does not say it is filtered is how a reader
concludes the data is missing.

Verified on the served tree: rendered features 12,379 -> 3,444 on the "nothing found"
filter, getFilter returns ["==",["get","tier"],0], and the only tier visible afterwards
is 0. All three buttons expose an accessible name and aria-pressed. The filtered view
shows the remaining gap concentrated in the Sahel, the Horn, north India and Nepal,
Myanmar, Indonesia and the Philippines.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 04:22:40 +00:00
parent f9808d0c8b
commit fc04f14a52
8 changed files with 82 additions and 33 deletions

View file

@ -146,6 +146,22 @@ const pinCodes = ref({}) // ISO 639-3 -> { l, s, f, a, o, ck, un }
const pinCount = ref(0)
// The legend quotes these, so they can never drift from what the layer is drawing.
// PIN FILTER. The map answers "what does this group have" on tap, but it could not answer
// "show me only the ones with nothing" — which is the question the map exists for. Filtering
// the LAYER rather than rebuilding the source: setFilter is a GL-side predicate, so 12,379
// features stay on the GPU and nothing is re-parsed or re-uploaded on a toggle.
const pinFilter = ref('all') // all | none | some | both
function setPinFilter(v) {
pinFilter.value = v
const m = mapRef.value
if (!m || !m.getLayer('gr-pins')) return
m.setFilter('gr-pins',
v === 'all' ? null
: v === 'none' ? ['==', ['get', 'tier'], 0]
: v === 'some' ? ['==', ['get', 'tier'], 1]
: ['==', ['get', 'tier'], 2])
}
const pinTally = computed(() => {
const t = [0, 0, 0]
for (const r of pinRows.value) t[r[6]]++
@ -473,6 +489,16 @@ useShadowStyles(
'border:1.4px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.18);vertical-align:-1px}' +
'.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;' +
'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}' +
'.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}' +
// DETAIL: a bottom sheet on a phone, a corner card on desktop.
@ -580,10 +606,33 @@ useShadowStyles(
Each <i class="gm-dot" style="background:#6d766e"></i> is ONE people group —
{{ fmt(pinTally.total) }} of them, {{ fmt(pinTally.linked) }} with a link
</div>
<div class="gm-keys">
<span class="gm-key"><i class="gm-dot" :style="{ background: PIN_COLOR[2] }"></i>Scripture <em>and</em> film</span>
<span class="gm-key"><i class="gm-dot" :style="{ background: PIN_COLOR[1] }"></i>some resource found</span>
<span class="gm-key"><i class="gm-dot" :style="{ background: PIN_COLOR[0] }"></i>nothing found</span>
<!-- The key IS the filter: a reader who has just understood what a colour means is
exactly the reader who wants to see only that colour. Two controls would be one
too many. -->
<div class="gm-keys gm-keys-btn">
<button type="button" class="gm-key gm-kbtn" :class="{ on: pinFilter === 'both' }"
@click="setPinFilter(pinFilter === 'both' ? 'all' : 'both')"
:aria-pressed="pinFilter === 'both'">
<i class="gm-dot" :style="{ background: PIN_COLOR[2] }"></i>Scripture <em>and</em> film
<b>{{ fmt(pinTally.both) }}</b>
</button>
<button type="button" class="gm-key gm-kbtn" :class="{ on: pinFilter === 'some' }"
@click="setPinFilter(pinFilter === 'some' ? 'all' : 'some')"
:aria-pressed="pinFilter === 'some'">
<i class="gm-dot" :style="{ background: PIN_COLOR[1] }"></i>some resource found
<b>{{ fmt(pinTally.partial) }}</b>
</button>
<button type="button" class="gm-key gm-kbtn" :class="{ on: pinFilter === 'none' }"
@click="setPinFilter(pinFilter === 'none' ? 'all' : 'none')"
:aria-pressed="pinFilter === 'none'">
<i class="gm-dot" :style="{ background: PIN_COLOR[0] }"></i>nothing found
<b>{{ fmt(pinTally.none) }}</b>
</button>
</div>
<div v-if="pinFilter !== 'all'" class="gm-filtered">
Showing only <b>{{ pinFilter === 'none' ? 'groups with nothing found'
: pinFilter === 'some' ? 'groups with some resource' : 'groups with Scripture and film' }}</b>.
<button type="button" class="gm-kclear" @click="setPinFilter('all')">Show all {{ fmt(pinTally.total) }}</button>
</div>
</div>
<div class="gm-caveat">

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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht" 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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht" 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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht" 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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t" crossorigin="anonymous"></script>
<script src="../gospel-resources.js" integrity="sha384-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht" 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

@ -556,7 +556,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "doxa-maps/gospel-resources/gospel-resources.js",
"integrity": "sha384-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t",
"integrity": "sha384-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht",
"elements": [
"gospel-resources-map"
],
@ -572,7 +572,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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t\" 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-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht\" 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>"
}
},
{
@ -586,7 +586,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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t\" 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-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht\" 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>"
}
},
{
@ -600,7 +600,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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t\" 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-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht\" 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>"
}
},
{
@ -614,7 +614,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-DJOPWgxv0aJTTqWf05/FWiCVqQURbKIlqPP+rlyMLFZmYm8UzzQBrMACWHOPDj5t\" 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-DpU1SWn4u94XMNElOTNoASob58FSPB7BTxE522CQEVO+cyFgnwXgNoD6O55tfkht\" 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>"
}
}
]