Follow the ring data to its flattened shape, and prefer the owner's join key
The ring arrived as a separate map and is now plain columns on every row: ring, ringNb and rop3. The layout module did not need to change — it takes a ring and a neighbour id and does not care where they were read — but its header described the old shape, and a header that describes a shape nobody ships is worse than no header. The note records the change, because the difference matters to anyone decoding the table by hand. ringNb is the reached row that SET the ring, so the arrow and the radius come from one field by construction and the older links ordering cannot disagree with them. The bounded set now tries rop3 + iso3 before falling back to name and country. rop3 is the data owner's key and the reliable one, but it reaches these rows only if the API document declares it — normalizeRows copies whatever meta.columns lists rather than a fixed set — and that cannot be confirmed away from the live endpoint. The fallback was measured at 994 of 1009 against the province bridge. Either way an unmatched row stays UNKNOWN and is counted where it can be seen, so a key that turns out to be absent surfaces as a visible unknown count rather than as a chart full of groups that merely failed to join.
This commit is contained in:
parent
afc343a112
commit
66ffae8435
2 changed files with 25 additions and 4 deletions
|
|
@ -7,13 +7,17 @@
|
|||
* why the two charts must not look alike.
|
||||
*
|
||||
* THE RINGS ARE DATA, NOT A DERIVATION. Ring membership is computed at build time by the
|
||||
* upg-neighbor build and written into near-neighbors.json as `rings[u] = [ring, reachedRow]`.
|
||||
* upg-neighbor build and written into near-neighbors.json as plain COLUMNS on every row:
|
||||
* `ring` (1-10, 0 meaning reached or none) and `ringNb` (the row index of the reached group
|
||||
* that sets it, -1 for none). An earlier draft of this file described a separate `rings[u]`
|
||||
* map; that was the shape before the data owner flattened it, and the note is kept because
|
||||
* the difference matters to anyone decoding the table by hand.
|
||||
* This module does not re-derive it, and that is deliberate rather than lazy: the ring rule
|
||||
* encodes a contested judgement (whether "dialect" means an ISO code or an Ethnologue-strict
|
||||
* level) that is the data owner's to make. A second implementation here would be a second
|
||||
* answer, and the first symptom would be two charts quietly disagreeing.
|
||||
*
|
||||
* ★ THE ARROW AND THE RADIUS MUST NAME THE SAME NEIGHBOUR. `rings[u][1]` is the reached row
|
||||
* ★ THE ARROW AND THE RADIUS MUST NAME THE SAME NEIGHBOUR. `ringNb` is the reached row
|
||||
* that SET the ring. `links[u][0]` is a different thing — the nearest neighbour on the older
|
||||
* E-scale ordering, sorted by believers and distance. They can disagree. Reading the radius
|
||||
* from one and the arrow from the other draws a group at ring 3 with an arrow pointing at its
|
||||
|
|
|
|||
|
|
@ -47,7 +47,9 @@ onMounted(async () => {
|
|||
const d = await res.json()
|
||||
const ix = Object.fromEntries(d.cols.map((c, i) => [c, i]))
|
||||
const dec = (r, c) => (d.dict[c] ? d.dict[c][r[ix[c]]] : r[ix[c]])
|
||||
const hasRop3 = d.cols.includes('rop3')
|
||||
rows.value = d.rows.map((r) => ({
|
||||
rop3: hasRop3 ? dec(r, 'rop3') : null,
|
||||
name: dec(r, 'name'), iso3: dec(r, 'iso3'), reached: dec(r, 'reached') === 1,
|
||||
}))
|
||||
} catch (e) {
|
||||
|
|
@ -57,11 +59,26 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
// TWO KEYS, PREFERRED IN ORDER, and the fallback is not a nicety.
|
||||
// rop3 + iso3 is the data owner's join key and the reliable one. But rop3 reaches these
|
||||
// rows only if the API document declares it as a column — normalizeRows copies whatever
|
||||
// `meta.columns` lists rather than a fixed set — and that cannot be confirmed without the
|
||||
// live endpoint. So: rop3 when both sides carry it, otherwise (name, iso3), which was
|
||||
// measured at 994/1009 against the province bridge. Whichever key is used, a row that does
|
||||
// not match is UNKNOWN and is counted as such, so a key that turns out to be absent shows
|
||||
// up as a visible unknown count rather than as a chart full of unreached groups.
|
||||
const joined = computed(() => {
|
||||
if (!rows.value) return []
|
||||
const byKey = new Map(rows.value.map((r) => [`${String(r.name).toLowerCase()}|${String(r.iso3).toUpperCase()}`, r]))
|
||||
const nameKey = (n, c) => `n:${String(n).toLowerCase()}|${String(c).toUpperCase()}`
|
||||
const ropKey = (r, c) => `r:${String(r)}|${String(c).toUpperCase()}`
|
||||
const byKey = new Map()
|
||||
for (const r of rows.value) {
|
||||
byKey.set(nameKey(r.name, r.iso3), r)
|
||||
if (r.rop3) byKey.set(ropKey(r.rop3, r.iso3), r)
|
||||
}
|
||||
return (board.filtered?.value || []).map((g) => {
|
||||
const hit = byKey.get(`${String(g.people_group).toLowerCase()}|${String(g.country).toUpperCase()}`)
|
||||
const hit = (g.rop3 ? byKey.get(ropKey(g.rop3, g.country)) : undefined)
|
||||
|| byKey.get(nameKey(g.people_group, g.country))
|
||||
return { id: g.slug || g.people_group, label: g.people_group, reached: hit ? hit.reached : undefined }
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue