Drop three resurrected library components, and re-home a doc comment
BoundedSet.vue, CenteredSetTarget.vue and VisionFunnel.vue came back through the
handoff merges, byte-identical to what c5c01dead deleted on purpose on 09-28
("Delete 7 unreachable library components"). That commit also added
check:no-dead-library precisely so they could not return. Verified before
removing: 0 files import charts/BoundedSet.vue, charts/CenteredSetTarget.vue or
charts/VisionFunnel.vue, and packetCharts.js references none of them, so nothing
is left dangling. check:no-dead-library passes with them gone.
A first count said BoundedSet had 6 importers. It has none: those files import
layoutBoundedSet from charts/bounded-set.js, a different module, and the grep
matched the substring.
packetCharts.js: the doc block describing scatterCells sat above topCells' own
block, two functions away from what it documents. Moved to sit directly above
scatterCells. No code change.
This commit is contained in:
parent
1db5e25bec
commit
6f08763bad
4 changed files with 21 additions and 357 deletions
|
|
@ -1,69 +0,0 @@
|
|||
<script setup>
|
||||
/**
|
||||
* BoundedSet — a circle with REACHED groups inside it and unreached outside.
|
||||
*
|
||||
* Promoted from gospel-internet's GiBoundedSet, which keeps the fetching and the board state; this
|
||||
* takes already-joined groups as a prop so any consumer can draw the same figure. The layout itself
|
||||
* is unchanged and shared — library/components/charts/boundedSet.js, one implementation.
|
||||
*
|
||||
* ★ UNKNOWN IS ITS OWN CATEGORY AND IS COUNTED IN INK. A group whose reached-ness did not join is
|
||||
* not drawn outside the circle, because "outside" means unreached and we do not know that. It is
|
||||
* reported instead. This matters more in print than on screen: a printed page has no hover, and a
|
||||
* category the reader cannot see is a category the reader will assume is zero.
|
||||
*
|
||||
* ★ NO SEED PROP, DELIBERATELY (gospel). Position is hashed from the group's own id, so a group
|
||||
* lands in the same place on every render, in every process, forever — two printings of one packet
|
||||
* are identical and a reader comparing last month's sheet to this month's sees real movement rather
|
||||
* than reshuffling. A seed would silently rescramble every dot while the data was unchanged.
|
||||
*
|
||||
* ★ AND THE BOUNDARY GAP IS NOT NEGOTIABLE (gospel): inside points stay within 0.92r, outside
|
||||
* beyond 1.12r, because a dot sitting ON the line is the one thing a bounded set cannot express.
|
||||
* To fit a page, shrink the whole figure — never compress the gap.
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { layoutBoundedSet, boundedSetTally } from './boundedSet.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** [{ id, label, reached }] — reached undefined means UNKNOWN, which is not the same as false. */
|
||||
groups: { type: Array, required: true },
|
||||
radius: { type: Number, default: 120 },
|
||||
spread: { type: Number, default: 260 },
|
||||
/** Every string is passed in; this component writes no prose. */
|
||||
labels: { type: Object, default: () => ({}) },
|
||||
dotRadius: { type: Number, default: 2.2 },
|
||||
})
|
||||
|
||||
const layout = computed(() => layoutBoundedSet(props.groups, { r: props.radius, spread: props.spread }))
|
||||
const tally = computed(() => boundedSetTally(layout.value))
|
||||
const box = computed(() => Math.round(props.spread * 1.08))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure class="bset">
|
||||
<svg class="bset__svg" :viewBox="`${-box} ${-box} ${box * 2} ${box * 2}`" role="img" :aria-label="labels.aria || ''">
|
||||
<circle class="bset__ring" cx="0" cy="0" :r="radius" />
|
||||
<circle v-for="p in layout.outside" :key="p.id" class="bset__out" :cx="p.x" :cy="p.y" :r="dotRadius" />
|
||||
<circle v-for="p in layout.inside" :key="p.id" class="bset__in" :cx="p.x" :cy="p.y" :r="dotRadius" />
|
||||
</svg>
|
||||
<figcaption class="bset__cap">
|
||||
{{ labels.caption || '' }}
|
||||
<b dir="ltr">{{ tally.inside }} / {{ tally.outside }}</b>
|
||||
</figcaption>
|
||||
<!-- Printed, not hidden behind a v-if on truthiness alone: when unknown is 0 the line is
|
||||
omitted, but whenever there IS an unknown it is named and counted. -->
|
||||
<p v-if="tally.unknown" class="bset__warn">
|
||||
{{ labels.unknown || '' }} <b dir="ltr">{{ tally.unknown }}</b>
|
||||
</p>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bset { margin: 0; display: flex; flex-direction: column; align-items: center; color: var(--pk-ink, #3b463d); }
|
||||
.bset__svg { max-width: 100%; }
|
||||
.bset__ring { fill: none; stroke: var(--pk-ink, #3b463d); stroke-width: 1.5; }
|
||||
.bset__in { fill: var(--pk-in, #92b195); }
|
||||
.bset__out { fill: var(--pk-out, #6d766e); opacity: 0.65; }
|
||||
.bset__cap { margin-block-start: 8px; font: 400 13px Georgia, serif; text-align: center; max-width: 46ch; }
|
||||
.bset__warn { font: 400 12px Georgia, serif; color: var(--pk-out, #6d766e); margin-block-start: 4px; }
|
||||
@media print { .bset__in, .bset__out, .bset__ring { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }
|
||||
</style>
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
<script setup>
|
||||
/**
|
||||
* CenteredSetTarget — the cross at the centre, every unreached group on a ring by how close it is
|
||||
* to a REACHED neighbour, arrows pointing inward.
|
||||
*
|
||||
* Promoted from gospel-internet's GiCenteredSet; the layout is unchanged and shared
|
||||
* (library/components/charts/centeredSet.js). This takes joined groups as a prop and every label
|
||||
* from the caller, so the packet and the board draw the same figure from the same code.
|
||||
*
|
||||
* ★ FOUR THINGS THAT LOOK LIKE DETAIL AND ARE NOT (gospel's invariants, kept in their words):
|
||||
*
|
||||
* ring 0 IS REACHED, and it IS the centre. It is not unplaced, and drawing it as missing would say
|
||||
* the opposite of what it means.
|
||||
*
|
||||
* ring -1 IS "no relation found" AND IS NOT RING 10. The outermost ring is a measurement; no
|
||||
* relation is the absence of one. Collapsing them prints a group as "furthest" when the truth is
|
||||
* "we could not place it" — 11 rows fleet-wide, and every one of them would be a false statement.
|
||||
*
|
||||
* THE ARROW AND THE RADIUS MUST NAME THE SAME NEIGHBOUR. Both come from `rings`, never from
|
||||
* links[u][0], which is the older E-scale ordering and can disagree — that draws a ring-3 group with
|
||||
* an arrow at its ring-8 neighbour and nothing on the page says so. inwardArrow returns null when no
|
||||
* neighbour is recorded, and a null arrow is simply not drawn: an arrow is a CLAIM about a
|
||||
* relationship, so a guessed one is a fabricated claim.
|
||||
*
|
||||
* EVERY RING IS LISTED, INCLUDING EMPTY ONES. Under a filter several of the nine legitimately go to
|
||||
* zero, and a decoder that omits its empty rows stops being a decoder. Presence by identity, never
|
||||
* by count — the same rule the zone legend follows.
|
||||
*
|
||||
* ★ AND NO SEED: position is hashed from the group's own id, so two printings of one packet are
|
||||
* identical and a reader comparing sheets sees real movement rather than reshuffling.
|
||||
*
|
||||
* ★ A NOTE FOR WHOEVER BUILDS `labels.rings` — DO NOT PASS groupings.label AS A NAME. Measured on
|
||||
* the published file: those labels run 7 to 206 characters, and the long ones are the zone's MEMBER
|
||||
* LIST, not its name. Tibet's reads "Tibet Autonomous Region + Sichuan Province + Xinjiang Uyghur
|
||||
* Autonomous Region + Gansu Province + ..." — only 2 of the 14 carry no "+" list at all (Gujarāt,
|
||||
* and "Scattered / Diaspora"). Used as a heading on a printed sheet that sets a 206-character title.
|
||||
* The field is a DEFINITION and is genuinely useful where there is room to say what a zone contains;
|
||||
* for a name, humanise the slug, which is uniform by construction and tops out around 34 characters.
|
||||
* gospel found this and carries the two separately; this component asserts nothing about the words,
|
||||
* so the whole hazard lives at the call site, which is exactly why it is written down here.
|
||||
*
|
||||
* ★ THE AGREED RING VOCABULARY, for a caller building `labels.rings`. Settled between gospel and
|
||||
* exp and derived from exp's ringFor() rather than hand-numbered, so the numbering cannot drift
|
||||
* from the code that produces it. The screen legend already renders these words, so a printed sheet
|
||||
* using them cannot disagree with the screen:
|
||||
*
|
||||
* 1 Same dialect, same people 6 Same language, different culture
|
||||
* 2 Same dialect, near culture 7 Same family, same people
|
||||
* 3 Same dialect, different culture 8 Same family, near culture
|
||||
* 4 Same language, same people 9 Same family, different culture
|
||||
* 5 Same language, near culture 10 Nearby only
|
||||
*
|
||||
* ⚠ ONE CELL IS PROVISIONAL: "people" vs the brief's "culture" in column 1 is pending a product
|
||||
* decision. If "culture" is kept, exactly THREE cells change — 1, 4 and 7 — and nothing else: not
|
||||
* the numbering, not the columns, not the colours. That is why this map is written here as
|
||||
* VOCABULARY and is NOT a default: a default would bake a provisional word into every consumer that
|
||||
* forgot to pass one, and the component would become the thing that settled an open product
|
||||
* question. The same reason ShareCalendar refuses to name its own metric.
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { layoutCenteredSet, ringTally, inwardArrow, ringRadii, RING_MIN, RING_MAX } from './centeredSet.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** [{ id, label, ring, neighborId }] — ring 1..10 placed, 0 reached/centre, -1 no relation. */
|
||||
groups: { type: Array, required: true },
|
||||
innerR: { type: Number, default: 26 },
|
||||
ringGap: { type: Number, default: 24 },
|
||||
showArrows: { type: Boolean, default: true },
|
||||
/** labels.rings is a map of ring number -> name, agreed once with the screen legend. */
|
||||
labels: { type: Object, default: () => ({}) },
|
||||
dotRadius: { type: Number, default: 2.2 },
|
||||
})
|
||||
|
||||
const opts = computed(() => ({ innerR: props.innerR, ringGap: props.ringGap }))
|
||||
const layout = computed(() => layoutCenteredSet(props.groups, opts.value))
|
||||
const tally = computed(() => ringTally(layout.value))
|
||||
const radii = computed(() => ringRadii(opts.value))
|
||||
const arrows = computed(() => (props.showArrows
|
||||
? layout.value.placed.map((p) => inwardArrow(p, opts.value)).filter(Boolean)
|
||||
: []))
|
||||
const box = computed(() => Math.round((props.innerR + RING_MAX * props.ringGap) * 1.12))
|
||||
/** Every ring, empty ones included. */
|
||||
const rows = computed(() => radii.value.map(({ ring }) => ({
|
||||
ring,
|
||||
name: (props.labels.rings && props.labels.rings[ring]) || String(ring),
|
||||
count: tally.value.counts.get(ring) || 0,
|
||||
})))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure class="cset">
|
||||
<svg class="cset__svg" :viewBox="`${-box} ${-box} ${box * 2} ${box * 2}`" role="img" :aria-label="labels.aria || ''">
|
||||
<circle v-for="r in radii" :key="r.ring" class="cset__ring" cx="0" cy="0" :r="r.r" />
|
||||
<line v-for="(a, i) in arrows" :key="'a' + i" class="cset__arrow" :x1="a.x1" :y1="a.y1" :x2="a.x2" :y2="a.y2" />
|
||||
<circle v-for="p in layout.placed" :key="p.id" class="cset__dot" :cx="p.x" :cy="p.y" :r="dotRadius" />
|
||||
<!-- The cross. Drawn last so nothing overlaps it. -->
|
||||
<path class="cset__cross" :d="`M 0 ${-innerR * 0.55} V ${innerR * 0.55} M ${-innerR * 0.38} 0 H ${innerR * 0.38}`" />
|
||||
</svg>
|
||||
<figcaption class="cset__cap">{{ labels.caption || '' }} <b dir="ltr">{{ tally.placed }}</b></figcaption>
|
||||
<!-- centre, unplaced and unmatched are SEPARATE lines and never folded into a ring. -->
|
||||
<p v-if="tally.centre || tally.unplaced" class="cset__warn">
|
||||
<span v-if="tally.centre">{{ labels.reached || '' }} <b dir="ltr">{{ tally.centre }}</b>. </span>
|
||||
<span v-if="tally.unplaced">{{ labels.noRelation || '' }} <b dir="ltr">{{ tally.unplaced }}</b>.</span>
|
||||
</p>
|
||||
<ul class="cset__decoder">
|
||||
<li v-for="r in rows" :key="r.ring"><b dir="ltr">{{ r.ring }}</b> {{ r.name }} <b dir="ltr">{{ r.count }}</b></li>
|
||||
</ul>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cset { margin: 0; display: flex; flex-direction: column; align-items: center; color: var(--pk-ink, #3b463d); }
|
||||
.cset__svg { max-width: 100%; }
|
||||
.cset__ring { fill: none; stroke: var(--pk-rule, #d8d3c4); stroke-width: 1; }
|
||||
.cset__arrow { stroke: var(--pk-out, #6d766e); stroke-width: 0.5; opacity: 0.55; }
|
||||
.cset__dot { fill: var(--pk-out, #6d766e); }
|
||||
.cset__cross { stroke: var(--pk-ink, #3b463d); stroke-width: 2.5; fill: none; }
|
||||
.cset__cap { margin-block-start: 8px; font: 400 13px Georgia, serif; text-align: center; max-width: 46ch; }
|
||||
.cset__warn { font: 400 12px Georgia, serif; color: var(--pk-out, #6d766e); margin-block-start: 4px; }
|
||||
.cset__decoder { list-style: none; margin: 6px 0 0; padding: 0; font: 400 11px Georgia, serif; columns: 2; }
|
||||
@media print { .cset__dot, .cset__ring, .cset__cross, .cset__arrow { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }
|
||||
</style>
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
<script setup>
|
||||
/**
|
||||
* VisionFunnel — the inverted triangle: all peoples → unreached → frontier, and where a list sits.
|
||||
*
|
||||
* ★ STEPS AND OVERLAYS ARE DIFFERENT THINGS, and this is the whole design.
|
||||
* A funnel's rhetoric is containment: of all peoples these are unreached, of those these are
|
||||
* frontier. That reading is only honest while each step is a SUBSET of the one above. DOXA is not:
|
||||
* of its 2,107 groups 2,082 are unreached and 1,745 are frontier, so 362 — one group in six — are
|
||||
* not frontier at all. Drawn as a fourth nested band it would misstate 17.2% of the list with a
|
||||
* clean shape and no error of any kind.
|
||||
*
|
||||
* So steps stack, and overlays are drawn as BRACKETS BESIDE the steps they reach into, never inside
|
||||
* them. The component asks packetCharts.overlayPlacement() whether an overlay is a slice rather
|
||||
* than deciding for itself, and refuses to render one as a band when it is not.
|
||||
*
|
||||
* PRINT FIRST (upg100): no hover, no tooltip. Every number is inked, including each overlay's parts.
|
||||
*
|
||||
* RTL: the triangle is vertically directional — it narrows downward — so it is NOT mirrored; a
|
||||
* scaleX flip would reverse nothing meaningful and invert any text inside it. What does need to
|
||||
* mirror is the label and bracket side, which is why those use logical properties (inset-inline,
|
||||
* text-align: start) rather than left/right.
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { assertNested, assertOnePublisher, bracketSpan, overlayCaption, shareOfLists } from './packetCharts.js'
|
||||
|
||||
const props = defineProps({
|
||||
/** Strictly nested, outermost first: [{ id, label, groups, population, source }] */
|
||||
steps: { type: Array, required: true },
|
||||
/** NOT slices: [{ id, label, groups, population, within: { stepId: count } }] */
|
||||
overlays: { type: Array, default: () => [] },
|
||||
highlight: { type: String, default: '' },
|
||||
/** { index, total } — K is unsettled (27 or 29), so it is passed in and nothing is asserted about it. */
|
||||
ofK: { type: Object, default: null },
|
||||
labels: { type: Object, default: () => ({}) },
|
||||
})
|
||||
|
||||
/** Refuses rather than drawing a plausible picture: a printed funnel cannot be corrected. */
|
||||
const problem = computed(() => {
|
||||
try {
|
||||
assertOnePublisher(props.steps)
|
||||
assertNested(props.steps)
|
||||
return null
|
||||
} catch (e) { return e.message }
|
||||
})
|
||||
|
||||
const W = 520, STEP_H = 74, PAD = 10
|
||||
const maxGroups = computed(() => Math.max(1, ...props.steps.map((s) => Number(s.groups) || 0)))
|
||||
|
||||
/** Each step is a trapezoid whose width is proportional to its count. */
|
||||
const bands = computed(() => props.steps.map((s, i) => {
|
||||
const next = props.steps[i + 1]
|
||||
const wTop = (Number(s.groups) || 0) / maxGroups.value * W
|
||||
const wBot = next ? (Number(next.groups) || 0) / maxGroups.value * W : wTop * 0.55
|
||||
const y = PAD + i * STEP_H
|
||||
const cx = PAD + W / 2
|
||||
return {
|
||||
id: s.id, label: s.label, groups: s.groups, population: s.population, y, h: STEP_H,
|
||||
points: [
|
||||
[cx - wTop / 2, y], [cx + wTop / 2, y],
|
||||
[cx + wBot / 2, y + STEP_H], [cx - wBot / 2, y + STEP_H],
|
||||
].map((p) => p.join(',')).join(' '),
|
||||
}
|
||||
}))
|
||||
|
||||
const height = computed(() => PAD * 2 + props.steps.length * STEP_H)
|
||||
|
||||
/**
|
||||
* An overlay's bracket spans the steps it reaches into. It is positioned BESIDE the funnel, so it
|
||||
* can cross two steps without asserting that either contains it.
|
||||
*/
|
||||
const placed = computed(() => props.overlays.map((o) => ({ o, span: bracketSpan(o, props.steps) })))
|
||||
|
||||
const brackets = computed(() => placed.value.filter((x) => x.span).map(({ o, span }) => ({
|
||||
...overlayCaption(o, props.steps),
|
||||
label: o.label,
|
||||
population: o.population,
|
||||
y: PAD + span.from * STEP_H,
|
||||
h: (span.to - span.from + 1) * STEP_H,
|
||||
isHighlight: o.id === props.highlight,
|
||||
})))
|
||||
|
||||
/**
|
||||
* Overlays we cannot place. Named in ink rather than dropped: an overlay whose `within` is missing
|
||||
* or names an unknown step has no known relationship to the steps, and silently omitting it loses
|
||||
* its groups from the page with nothing to say so.
|
||||
*/
|
||||
const unplaced = computed(() => placed.value.filter((x) => !x.span).map(({ o }) => o))
|
||||
|
||||
const ofKText = computed(() => {
|
||||
const k = props.ofK ? shareOfLists(props.ofK.index, props.ofK.total) : null
|
||||
return k ? { ...k, percent: k.percent.toFixed(1) } : null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure class="vf">
|
||||
<p v-if="problem" class="vf__error" role="alert">{{ problem }}</p>
|
||||
<template v-else>
|
||||
<figcaption v-if="labels.title" class="vf__title">{{ labels.title }}</figcaption>
|
||||
<div class="vf__body">
|
||||
<svg class="vf__svg" :viewBox="`0 0 ${W + PAD * 2} ${height}`" role="img" :aria-label="labels.title || ''">
|
||||
<polygon v-for="(b, i) in bands" :key="b.id" :points="b.points" class="vf__band" :style="{ opacity: 0.45 + i * 0.18 }" />
|
||||
<!-- Brackets sit OUTSIDE the polygons. An overlay is never painted inside a step, because
|
||||
being inside a step is exactly the claim it cannot make. -->
|
||||
<g v-for="b in brackets" :key="b.id">
|
||||
<rect :x="2" :y="b.y + 2" :width="4" :height="b.h - 4" class="vf__bracket" :class="{ 'vf__bracket--hi': b.isHighlight }" />
|
||||
</g>
|
||||
</svg>
|
||||
<ul class="vf__steps">
|
||||
<li v-for="b in bands" :key="b.id" class="vf__step">
|
||||
<b>{{ b.label }}</b> — {{ b.groups }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- The overlay's parts, as text. upg100: an unexplained band on paper tells a reader nothing. -->
|
||||
<ul class="vf__overlays">
|
||||
<li v-for="b in brackets" :key="b.id" class="vf__overlay" :class="{ 'vf__overlay--hi': b.isHighlight }">
|
||||
<b>{{ b.label }}</b> — {{ b.total }}<template v-for="p in b.parts" :key="p.stepId">, {{ p.groups }} {{ (labels.steps && labels.steps[p.stepId]) || p.stepId }}</template>
|
||||
<span v-if="!b.isSlice && labels.notASlice" class="vf__caveat"> {{ labels.notASlice }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-for="o in unplaced" :key="o.id" class="vf__unplaced" role="alert">
|
||||
{{ o.label || o.id }} — {{ o.groups }}{{ labels.unplaced ? ' · ' + labels.unplaced : ' · cannot be placed: no `within` naming a step in this funnel' }}
|
||||
</p>
|
||||
<p v-if="ofKText" class="vf__ofk">{{ labels.ofK || '' }} {{ ofKText.index }} / {{ ofKText.total }} · {{ ofKText.percent }}%</p>
|
||||
</template>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vf { margin: 0; color: var(--pk-ink, #1b1a17); }
|
||||
.vf__title { font: 700 40px Georgia, serif; margin: 0 0 6px; }
|
||||
.vf__body { display: flex; gap: 14px; align-items: flex-start; }
|
||||
.vf__svg { flex: 1 1 auto; max-width: 100%; }
|
||||
.vf__band { fill: var(--pk-on, #2f6d4f); stroke: #fff; stroke-width: 1; }
|
||||
.vf__bracket { fill: var(--pk-rule, #d8d3c4); }
|
||||
.vf__bracket--hi { fill: var(--pk-ink, #1b1a17); }
|
||||
.vf__steps, .vf__overlays { list-style: none; margin: 0; padding: 0; }
|
||||
.vf__step, .vf__overlay, .vf__ofk { font: 400 12px Georgia, serif; margin-top: 4px; text-align: start; }
|
||||
.vf__overlay--hi { font-weight: 700; }
|
||||
.vf__caveat { opacity: 0.8; }
|
||||
.vf__unplaced { font: 700 12px Georgia, serif; color: #8a1c1c; margin-top: 4px; text-align: start; }
|
||||
.vf__error { font: 700 13px Georgia, serif; color: #8a1c1c; border: 2px solid #8a1c1c; padding: 8px; }
|
||||
@media print { .vf__band, .vf__bracket { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }
|
||||
</style>
|
||||
|
|
@ -196,27 +196,6 @@ export function fractionFromPercent(percent) {
|
|||
return (Number(percent) || 0) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ WHICH cells are shaded, deterministically — LIFTED VERBATIM from packet-render.js's lifeGrid so
|
||||
* there is ONE implementation rather than a component and a renderer that drift.
|
||||
*
|
||||
* The algorithm and the reasoning are not mine; both are carried across intact. Two properties the
|
||||
* packet depends on, and the second is the one that cost a shipped page:
|
||||
*
|
||||
* DETERMINISTIC, never Math.random(): the same packet must print identically every time, or two
|
||||
* copies of one zone disagree and neither is reproducible. FNV-1a over the seed feeds a mulberry32
|
||||
* PRNG, so the pattern is stable per zone and different BETWEEN zones, which also stops every packet
|
||||
* in a print run carrying the same constellation.
|
||||
*
|
||||
* SCATTERED, NOT STACKED. The original comment reasoned that a contiguous run would read better;
|
||||
* the render showed the opposite — 32 contiguous cells fill most of row 1 of 52, so the page printed
|
||||
* as one solid bar across the header, the ordinary chart the life grid exists to avoid. The message
|
||||
* is rarity: 32 boxes lost among 5,200, and the eye has to hunt for them. Scattering IS the point.
|
||||
*
|
||||
* ⚠ AND NO GATE COULD SEE IT — the existing check asserts the shaded COUNT is between 0 and 5,200,
|
||||
* which is equally true of a scatter and of a bar. A count cannot distinguish a distribution. That
|
||||
* is why scatterSpread() below exists and why check-packet-charts asserts the shape, not the total.
|
||||
*/
|
||||
/**
|
||||
* The FIRST n cells, in reading order — the counterpart to scatterCells.
|
||||
*
|
||||
|
|
@ -238,6 +217,27 @@ export function topCells(total, n) {
|
|||
return picked
|
||||
}
|
||||
|
||||
/**
|
||||
* ★ WHICH cells are shaded, deterministically — LIFTED VERBATIM from packet-render.js's lifeGrid so
|
||||
* there is ONE implementation rather than a component and a renderer that drift.
|
||||
*
|
||||
* The algorithm and the reasoning are not mine; both are carried across intact. Two properties the
|
||||
* packet depends on, and the second is the one that cost a shipped page:
|
||||
*
|
||||
* DETERMINISTIC, never Math.random(): the same packet must print identically every time, or two
|
||||
* copies of one zone disagree and neither is reproducible. FNV-1a over the seed feeds a mulberry32
|
||||
* PRNG, so the pattern is stable per zone and different BETWEEN zones, which also stops every packet
|
||||
* in a print run carrying the same constellation.
|
||||
*
|
||||
* SCATTERED, NOT STACKED. The original comment reasoned that a contiguous run would read better;
|
||||
* the render showed the opposite — 32 contiguous cells fill most of row 1 of 52, so the page printed
|
||||
* as one solid bar across the header, the ordinary chart the life grid exists to avoid. The message
|
||||
* is rarity: 32 boxes lost among 5,200, and the eye has to hunt for them. Scattering IS the point.
|
||||
*
|
||||
* ⚠ AND NO GATE COULD SEE IT — the existing check asserts the shaded COUNT is between 0 and 5,200,
|
||||
* which is equally true of a scatter and of a bar. A count cannot distinguish a distribution. That
|
||||
* is why scatterSpread() below exists and why check-packet-charts asserts the shape, not the total.
|
||||
*/
|
||||
export function scatterCells(total, n, seed = '') {
|
||||
const TOTAL = Math.max(0, Number(total) || 0)
|
||||
const count = Math.max(0, Math.min(TOTAL, Math.round(Number(n) || 0)))
|
||||
|
|
|
|||
Loading…
Reference in a new issue