UPG100 packet: the set captions count every group of the list

A group with no record in a set figure's data (no near-neighbour data,
for example) was in none of that figure's numbers, so the captions
added up to less than the list: Laos printed 94 of its 95 groups. Each
caption now names the groups without data ("1 without data", in all 11
languages), so its parts add up to the list. check:packet-variants
asserts the sum both ways.
This commit is contained in:
Doxa Maps 2026-10-03 00:55:43 +00:00
parent 1fa35309af
commit 042d2ec10f
3 changed files with 36 additions and 4 deletions

View file

@ -217,6 +217,22 @@ ok(!/<em>[^<]*<\/em><\/span>/.test(full.slice(full.indexOf('class="card"'))), 't
ok(/10/.test(caps.join(' ')), 'the bounded tally does not state its unknown count — an un-inked category reads as zero in print')
}
// ★ EVERY GROUP OF THE LIST IS COUNTED (2026-10-03): a group with no record in a figure's data is named in
// that figure's caption ("15 without data"), so each caption's parts add up to the list. Both ways: the
// fixture's figures lack records (45 groups; 30 bounded, 24 centered), and complete data prints no such part.
{
const sum = (cap) => [...cap.matchAll(/\d[\d,]*/g)].reduce((a, m) => a + Number(m[0].replace(/,/g, '')), 0)
const capsOf = (html) => [...html.matchAll(/class="set-cap">([^<]*)</g)].map((m) => m[1])
const nd = String(strings.en.sets_no_data).replace('{n}', '').trim()
const short = capsOf(full)
ok(short.length === 2 && short.every((c) => sum(c) === 45 && c.includes(nd)),
`each set caption should add up to the list's 45 groups, naming the ones ${nd}: ${short.join(' | ')}`)
const whole = capsOf(R.buildPacket(zone, groups, new Map(), '<svg/>', funnel, christian, 'full', {
bounded: groups.map((g, i) => ({ id: g.slug, reached: i % 2 === 0 })), centered: groups.map((g, i) => ({ id: g.slug, ring: (i % 10) + 1 })) }))
ok(whole.length === 2 && whole.every((c) => sum(c) === 45 && !c.includes(nd)),
`with a record for every group, each caption should add up to 45 and name none ${nd}: ${whole.join(' | ')}`)
}
// ── THE RING LEGEND: THE AGREED VOCABULARY, AND ONLY THE RINGS THAT HOLD SOMEONE ────────────────
// ★ The names are COMPOSED from band x culture rather than a flat 1..10 table, because "same
// people" (provisional, pending a product decision) appears in rings 1, 4 AND 7 — three cells that a

View file

@ -1632,13 +1632,18 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
+ `<rect x="${(cx - arm / 2).toFixed(1)}" y="${(cy - h / 2 + h * 0.24).toFixed(1)}" width="${arm.toFixed(1)}" height="${bar.toFixed(1)}" rx="1.5"/></g>`;
}
function setsPage(zone, sets) {
function setsPage(zone, sets, listSize) {
// '' WITHOUT DATA, exactly like funnelPage and christianPage — a page that cannot state true
// figures must not appear at all. This is what keeps FULL valid before a host supplies sets.
if (!sets || (!Array.isArray(sets.bounded) && !Array.isArray(sets.centered))) return '';
const bounded = Array.isArray(sets.bounded) ? sets.bounded : [];
const centered = Array.isArray(sets.centered) ? sets.centered : [];
if (!bounded.length && !centered.length) return '';
// ★ EVERY GROUP OF THE LIST IS COUNTED (2026-10-03). A group with no record in a figure's data (no
// near-neighbour data, say) is in none of its figures, so a caption used to add up to less than the
// list: Laos printed "88 placed · 0 at the centre · 6 unplaced", 94 of its 95. The groups missing from
// a figure's data are now named in its caption ("1 without data"), so its parts add up to the list.
const noData = (n) => (Number.isFinite(listSize) && listSize > n ? ` · ${fill(t('sets_no_data'), { n: num(listSize - n) })}` : '');
const panels = [];
let legend = '';
@ -1661,7 +1666,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
${cross(L.cx, L.cy, 44)}
</svg>
<p class="set-cap">${esc(fill(t('sets_bounded_tally'),
{ inside: num(T.inside), outside: num(T.outside), unknown: num(T.unknown) }))}</p>
{ inside: num(T.inside), outside: num(T.outside), unknown: num(T.unknown) }) + noData(bounded.length))}</p>
</div>` });
}
@ -1713,7 +1718,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
${bands.join('')}${dots}${nums.join('')}${cross(0, 0, 40)}
</svg>
<p class="set-cap">${esc(fill(t('sets_centered_tally'),
{ placed: num(bf.placed.length), centre: num(T.centre), unplaced: num(T.unplaced + bf.unplaced.length) }))}</p>
{ placed: num(bf.placed.length), centre: num(T.centre), unplaced: num(T.unplaced + bf.unplaced.length) }) + noData(centered.length))}</p>
</div>` });
const rows = ringLegend(T);
// The legend first says what a ring IS, in plain words, then lists the rings that hold someone.
@ -1880,7 +1885,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
// packet lists the SAME groups as a worksheet (LIST_STYLE); the full packet as profiles.
...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups, photos) : listPages(zone, groups, photos)),
// BACK MATTER: after the profiles (brief A1). Full-detail only.
keep('sets') && setsPage(zone, sets),
keep('sets') && setsPage(zone, sets, Array.isArray(groups) ? groups.length : NaN),
keep('wheel') && wheelPage(),
keep('points') && pointsPage(),
].filter(Boolean);

View file

@ -263,6 +263,7 @@
"sets_bounded_title": "Reached, and not",
"sets_centered_title": "Distance from a neighbour",
"sets_bounded_tally": "{inside} inside · {outside} outside · {unknown} unknown",
"sets_no_data": "{n} without data",
"sets_centered_tally": "{placed} placed · {centre} at the centre · {unplaced} unplaced",
"ring_band_dialect": "Same dialect",
"ring_band_language": "Same language",
@ -608,6 +609,7 @@
"sets_bounded_title": "Alcanzados y no alcanzados",
"sets_centered_title": "Distancia a un vecino",
"sets_bounded_tally": "{inside} dentro · {outside} fuera · {unknown} desconocido",
"sets_no_data": "{n} sin datos",
"sets_centered_tally": "{placed} ubicados · {centre} en el centro · {unplaced} sin ubicar",
"ring_band_dialect": "Mismo dialecto",
"ring_band_language": "Misma lengua",
@ -939,6 +941,7 @@
"sets_bounded_title": "Atteints, ou non",
"sets_centered_title": "Distance d’un voisin",
"sets_bounded_tally": "{inside} à l’intérieur · {outside} à l’extérieur · {unknown} inconnu",
"sets_no_data": "{n} sans données",
"sets_centered_tally": "{placed} placés · {centre} au centre · {unplaced} non placés",
"ring_band_dialect": "Même dialecte",
"ring_band_language": "Même langue",
@ -1267,6 +1270,7 @@
"sets_bounded_title": "Alcançados e não alcançados",
"sets_centered_title": "Distância de um vizinho",
"sets_bounded_tally": "{inside} dentro · {outside} fora · {unknown} desconhecido",
"sets_no_data": "{n} sem dados",
"sets_centered_tally": "{placed} posicionados · {centre} no centro · {unplaced} sem posição",
"ring_band_dialect": "Mesmo dialeto",
"ring_band_language": "Mesma língua",
@ -1598,6 +1602,7 @@
"sets_bounded_title": "Erreicht und nicht erreicht",
"sets_centered_title": "Abstand zu einem Nachbarn",
"sets_bounded_tally": "{inside} innen · {outside} außen · {unknown} unbekannt",
"sets_no_data": "{n} ohne Daten",
"sets_centered_tally": "{placed} platziert · {centre} in der Mitte · {unplaced} ohne Platz",
"ring_band_dialect": "Gleicher Dialekt",
"ring_band_language": "Gleiche Sprache",
@ -1926,6 +1931,7 @@
"sets_bounded_title": "Raggiunti e non raggiunti",
"sets_centered_title": "Distanza da un vicino",
"sets_bounded_tally": "{inside} dentro · {outside} fuori · {unknown} sconosciuto",
"sets_no_data": "{n} senza dati",
"sets_centered_tally": "{placed} collocati · {centre} al centro · {unplaced} non collocati",
"ring_band_dialect": "Stesso dialetto",
"ring_band_language": "Stessa lingua",
@ -2254,6 +2260,7 @@
"sets_bounded_title": "Atinse și neatinse",
"sets_centered_title": "Distanța față de un vecin",
"sets_bounded_tally": "{inside} înăuntru · {outside} în afară · {unknown} necunoscut",
"sets_no_data": "{n} fără date",
"sets_centered_tally": "{placed} plasate · {centre} în centru · {unplaced} neplasate",
"ring_band_dialect": "Același dialect",
"ring_band_language": "Aceeași limbă",
@ -2583,6 +2590,7 @@
"sets_bounded_title": "Достигнутые и нет",
"sets_centered_title": "Расстояние до соседа",
"sets_bounded_tally": "{inside} внутри · {outside} снаружи · {unknown} неизвестно",
"sets_no_data": "{n} без данных",
"sets_centered_tally": "{placed} размещено · {centre} в центре · {unplaced} без места",
"ring_band_dialect": "Тот же диалект",
"ring_band_language": "Тот же язык",
@ -2921,6 +2929,7 @@
"sets_bounded_title": "وصلتهم البشارة ومن لم تصلهم",
"sets_centered_title": "المسافة إلى جار",
"sets_bounded_tally": "{inside} في الداخل · {outside} في الخارج · {unknown} غير معروف",
"sets_no_data": "{n} بلا بيانات",
"sets_centered_tally": "{placed} موضوعون · {centre} في المركز · {unplaced} بلا موضع",
"ring_band_dialect": "اللهجة نفسها",
"ring_band_language": "اللغة نفسها",
@ -3249,6 +3258,7 @@
"sets_bounded_title": "पहुँचे और नहीं पहुँचे",
"sets_centered_title": "पड़ोसी से दूरी",
"sets_bounded_tally": "{inside} भीतर · {outside} बाहर · {unknown} अज्ञात",
"sets_no_data": "{n} बिना आँकड़ों के",
"sets_centered_tally": "{placed} स्थित · {centre} केंद्र में · {unplaced} अस्थित",
"ring_band_dialect": "वही बोली",
"ring_band_language": "वही भाषा",
@ -3575,6 +3585,7 @@
"sets_bounded_title": "已触及与未触及",
"sets_centered_title": "与邻近族群的距离",
"sets_bounded_tally": "{inside} 圈内 · {outside} 圈外 · {unknown} 未知",
"sets_no_data": "{n} 无数据",
"sets_centered_tally": "{placed} 已定位 · {centre} 在中心 · {unplaced} 未定位",
"ring_band_dialect": "同一方言",
"ring_band_language": "同一语言",