Stage the row 130 flip, and caption the chart with a total that does not move

STAGED, NOT FLIPPED. SET_FILL_MODE in GiCenteredSetMap is 'fit' — today's
behaviour, unchanged — with the alternative named beside it so the ruling is one
line and a rebuild. 'fit' is not a placeholder: it is the honest mode for a page
that cannot zoom, so the POSTER should keep it whatever the screen chooses.

The binding constraint is recorded there because it is not obvious from the
numbers: separation can never fall below twice the pin floor (5.2), and an
18.5-unit band therefore holds at most three rows, so no minSep reaches ring 2's
658 groups. minSep 7 places 931 of 2,074; minSep 5 places 1,369; minSep 4 places
1,369 — identical, which is what proves depth rather than separation is binding.
'fill-all' places all 2,074 with none under a label and ring 2 using 18 rows.

THE CAPTION states the TRUE total — the groups that have a ring — not the number
of pins currently drawn. In the dense mode the same groups read as an arc of
colour zoomed out and as separate pins zoomed in, so a caption counting what is
visible would change as the reader moved: true at every instant and misleading
overall. It joins the paragraph that already accounts for the centre, the
no-relation groups and the unmatched ones, so those four numbers reconcile
instead of competing.

centered.shown added in all eleven locales; parity and keys-exist pass, and
hardcoded-english did not rise (257, and that fall is studio's, not mine).
This commit is contained in:
Doxa Maps 2026-09-29 02:40:35 +00:00
parent 8cf7a48060
commit 334b3d6fa1
13 changed files with 48 additions and 12 deletions

View file

@ -17,7 +17,8 @@
"failed": "تعذّر تحميل جدول الجيران، لذلك لم يُرسم هذا المخطط.",
"reached": "موصول إليها بالفعل، لذلك في المركز:",
"unmatched": "غير موجودة في جدول الجيران أصلاً، لذلك لم تُرسم:",
"unplaced": "لم تُوجد أي صلة، لذلك لم تُوضع:"
"unplaced": "لم تُوجد أي صلة، لذلك لم تُوضع:",
"shown": "المجموعات البشرية على الحلقات:"
},
"centre": {
"aria": "عرض",

View file

@ -17,7 +17,8 @@
"failed": "Die Nachbartabelle konnte nicht geladen werden, daher wird dieses Diagramm nicht gezeichnet.",
"reached": "Bereits erreicht, daher in der Mitte:",
"unmatched": "Gar nicht in der Nachbartabelle, daher nicht gezeichnet:",
"unplaced": "Keine Beziehung gefunden, daher nicht eingeordnet:"
"unplaced": "Keine Beziehung gefunden, daher nicht eingeordnet:",
"shown": "Volksgruppen auf den Ringen:"
},
"centre": {
"aria": "Anzeigen",

View file

@ -17,7 +17,8 @@
"failed": "The neighbour table could not be loaded, so this chart is not drawn.",
"reached": "Already reached, so at the centre:",
"unmatched": "Not in the neighbour table at all, so not drawn:",
"unplaced": "No relation found, so not placed:"
"unplaced": "No relation found, so not placed:",
"shown": "People groups on the rings:"
},
"centre": {
"aria": "Show",

View file

@ -17,7 +17,8 @@
"failed": "No se pudo cargar la tabla de vecinos, así que este gráfico no se dibuja.",
"reached": "Ya alcanzados, por eso en el centro:",
"unmatched": "No están en la tabla de vecinos, así que no se dibujan:",
"unplaced": "No se encontró relación, así que no se sitúan:"
"unplaced": "No se encontró relación, así que no se sitúan:",
"shown": "Pueblos en los anillos:"
},
"centre": {
"aria": "Mostrar",

View file

@ -17,7 +17,8 @@
"failed": "La table des voisins n’a pas pu être chargée, ce graphique n’est donc pas tracé.",
"reached": "Déjà atteints, donc au centre :",
"unmatched": "Absents de la table des voisins, donc non tracés :",
"unplaced": "Aucune relation trouvée, donc non placés :"
"unplaced": "Aucune relation trouvée, donc non placés :",
"shown": "Peuples sur les anneaux :"
},
"centre": {
"aria": "Afficher",

View file

@ -17,7 +17,8 @@
"failed": "पड़ोसी तालिका लोड नहीं हो सकी, इसलिए यह चित्र नहीं बनाया गया।",
"reached": "पहले से पहुँचे हुए, इसलिए केंद्र में:",
"unmatched": "पड़ोसी तालिका में हैं ही नहीं, इसलिए नहीं दिखाए गए:",
"unplaced": "कोई संबंध नहीं मिला, इसलिए स्थान नहीं दिया गया:"
"unplaced": "कोई संबंध नहीं मिला, इसलिए स्थान नहीं दिया गया:",
"shown": "वलयों पर जनसमूह:"
},
"centre": {
"aria": "दिखाएँ",

View file

@ -17,7 +17,8 @@
"failed": "Non è stato possibile caricare la tabella dei vicini, quindi questo grafico non viene disegnato.",
"reached": "Già raggiunti, quindi al centro:",
"unmatched": "Non presenti nella tabella dei vicini, quindi non disegnati:",
"unplaced": "Nessuna relazione trovata, quindi non collocati:"
"unplaced": "Nessuna relazione trovata, quindi non collocati:",
"shown": "Popoli sugli anelli:"
},
"centre": {
"aria": "Mostra",

View file

@ -17,7 +17,8 @@
"failed": "Não foi possível carregar a tabela de vizinhos, por isso este gráfico não é desenhado.",
"reached": "Já alcançados, por isso no centro:",
"unmatched": "Não constam da tabela de vizinhos, por isso não são desenhados:",
"unplaced": "Nenhuma relação encontrada, por isso não posicionados:"
"unplaced": "Nenhuma relação encontrada, por isso não posicionados:",
"shown": "Povos nos anéis:"
},
"centre": {
"aria": "Mostrar",

View file

@ -17,7 +17,8 @@
"failed": "Tabelul vecinilor nu a putut fi încărcat, așa că acest grafic nu este desenat.",
"reached": "Deja atinse, așadar în centru:",
"unmatched": "Nu se află deloc în tabelul vecinilor, așadar nu sunt desenate:",
"unplaced": "Nicio relație găsită, așadar neașezate:"
"unplaced": "Nicio relație găsită, așadar neașezate:",
"shown": "Grupuri etnice pe inele:"
},
"centre": {
"aria": "Afișează",

View file

@ -17,7 +17,8 @@
"failed": "Не удалось загрузить таблицу соседей, поэтому эта диаграмма не построена.",
"reached": "Уже достигнуты, поэтому в центре:",
"unmatched": "Вовсе отсутствуют в таблице соседей, поэтому не показаны:",
"unplaced": "Связь не найдена, поэтому не размещены:"
"unplaced": "Связь не найдена, поэтому не размещены:",
"shown": "Народы на кольцах:"
},
"centre": {
"aria": "Показать",

View file

@ -17,7 +17,8 @@
"failed": "无法载入邻族数据表,因此本图未绘制。",
"reached": "已经得着,因此位于中心:",
"unmatched": "根本不在邻族数据表中,因此未绘制:",
"unplaced": "未找到关联,因此未予定位:"
"unplaced": "未找到关联,因此未予定位:",
"shown": "环上的族群:"
},
"centre": {
"aria": "显示",

View file

@ -92,6 +92,11 @@ const prepared = computed(() => {
const layout = computed(() => layoutCenteredSet(prepared.value.groups, { innerR: 30, ringGap: 22 }))
const tally = computed(() => ringTally(layout.value))
// ★ COUNTED FROM THE LAYOUT, NOT FROM WHAT IS DRAWN. layout.placed is every group that HAS a ring,
// which is a fact about the data; the number of pins actually rendered depends on the fill mode and,
// in the dense mode, reads differently at different zooms. Captioning the drawn count would give a
// total that changes as the reader zooms — true at every instant and misleading overall.
const placedTotal = computed(() => layout.value.placed.length)
const radii = computed(() => ringRadii({ innerR: 30, ringGap: 22 }))
// The arrow carries its group's ring so the dimming can tell them apart. inwardArrow returns
// geometry and the neighbour it names, not the ring — reading a.ring off it would have been
@ -201,7 +206,14 @@ useShadowStyles(
:active-ring="activeRing" :panel="panelEl" @select="activeRing = $event"
/>
</div>
<p v-if="tally.centre || tally.unplaced || prepared.unmatched" class="gcs-warn">
<!-- ★ THE POSITIVE TOTAL COMES FIRST, and it is the TRUE one, not a count of what happens to be
drawn at this zoom. In the dense mode the same groups render as an arc of colour when zoomed
out and as separate pins when zoomed in; a caption derived from what is currently visible
would change as the reader moved, which is exactly the way a chart misleads without ever
being wrong. The three clauses after it account for every group NOT on a ring, so the
paragraph reads as one reconciliation rather than four unrelated numbers. -->
<p v-if="placedTotal || tally.centre || tally.unplaced || prepared.unmatched" class="gcs-warn">
<span v-if="placedTotal">{{ T('gospelInternet.centered.shown', 'People groups on the rings:') }} <b dir="ltr">{{ fmt(placedTotal) }}</b>. </span>
<span v-if="tally.centre">{{ T('gospelInternet.centered.reached', 'Already reached, so at the centre:') }} <b dir="ltr">{{ fmt(tally.centre) }}</b>. </span>
<span v-if="tally.unplaced">{{ T('gospelInternet.centered.unplaced', 'No relation found, so not placed:') }} <b dir="ltr">{{ fmt(tally.unplaced) }}</b>. </span>
<span v-if="prepared.unmatched">{{ T('gospelInternet.centered.unmatched', 'Not in the neighbour table at all, so not drawn:') }} <b dir="ltr">{{ fmt(prepared.unmatched) }}</b>.</span>

View file

@ -78,11 +78,24 @@ const layout = computed(() => layoutCenteredSet(props.groups, { innerR: INNER_R,
// evenly by angle and deterministically by id, never inside a label's keep-out arc. Over-full rings
// shrink their pins to a floor before anything is reported unplaced — a visible pin beats a missing
// one — and whatever still cannot fit is reported rather than silently overlapped or dropped.
// ★ THE ONE LINE DRIVER ROW 130 DECIDES. 'fit' (today) keeps every pin separable at the chart's own
// scale and reports what will not fit; on the real default view that is 931 of 2,074 drawn and 1,143
// reported. 'fill-all' draws all 2,074 and lets zoom do the separating — measured: 0 unplaced, every
// pin inside its band, none under a label, ring 2 using 18 rows for its 658 groups.
//
// 'fit' is NOT a placeholder and must stay the default here until the ruling: it is the honest mode
// for a page that cannot zoom, which is why the POSTER should keep it whatever the screen chooses.
// The binding constraint is band DEPTH, not separation — sep can never fall below 2x the pin floor
// (5.2), and an 18.5-unit band therefore holds at most 3 rows, so no minSep reaches ring 2's 658.
// That is why this is a mode and not a tuned number.
const SET_FILL_MODE = 'fit' // ← row 130: change to 'fill-all' for the screen charts
const banded = computed(() => bandFill(layout.value.placed, radii.value, {
ringGap: RING_GAP,
labels: ringLabels.value,
// The chart's own units, not screen pixels: this module cannot know the zoom.
minSep: 7,
mode: SET_FILL_MODE,
}))
const features = computed(() => centeredSetFeatures(