A life-calendar anchor outside the grid says so instead of drawing nothing

By the regions owner. An anchor week past the grid, negative or fractional passed an isFinite
guard and matched no cell, which on paper reads the same as the deliberate no-anchor default.
anchorPlacement() returns a reason, LifeCalendar prints it, and check:packet-charts covers six
edge cases while pinning that no anchor asked for stays silent.
This commit is contained in:
Doxa Maps 2026-09-28 16:01:02 +00:00
parent 642fc15310
commit 5b51153b82
3 changed files with 52 additions and 4 deletions

View file

@ -12,7 +12,7 @@ import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import {
assertNested, assertOnePublisher, bracketSpan, fractionFromPercent, overlayPlacement, scatterCells, scatterSpread, overlayCaption, cellCount, shareCells, peoplePerCell, shareOfLists,
anchorPlacement, assertNested, assertOnePublisher, bracketSpan, fractionFromPercent, overlayPlacement, scatterCells, scatterSpread, overlayCaption, cellCount, shareCells, peoplePerCell, shareOfLists,
} from '../library/components/charts/packetCharts.js'
const DATA = join(dirname(fileURLToPath(import.meta.url)), '..', 'data', 'people-groups')
@ -159,6 +159,26 @@ ok(sorted.slice(0, 5).join(',') === '139,140,221,399,418' && sorted[sorted.lengt
ok(scatterCells(5200, 99999, 'x').size === 5200 && scatterCells(5200, -5, 'x').size === 0,
'and the count is clamped both ways rather than throwing on a page mid-print')
// ── an anchor that cannot be drawn is NAMED, not dropped (same class as the bracket) ────────────
// ★ FOUND BY SWEEPING FOR THE CLASS, not by waiting for a second report. After the Infinity bracket
// I read the other two templates asking the same question — what does this expression do with an
// edge-case input? LifeCalendar matched its anchor with only an isFinite guard, so week 6000 on a
// 5,200-cell grid, a negative week, or a fractional one matched NO cell and drew nothing. On paper
// that is indistinguishable from having chosen not to mark it, which is the packet's own reason for
// leaving the anchor un-defaulted in the first place — so the silent version quietly tells the
// reader the opposite of what a missing anchor is supposed to mean.
const TOTAL_CELLS = cellCount(100, 52)
ok(anchorPlacement({ week: 0 }, TOTAL_CELLS).index === 0, 'week 0 is drawable — the first cell')
ok(anchorPlacement({ week: 5199 }, TOTAL_CELLS).index === 5199, 'and the last cell is 5,199, not 5,200')
ok(anchorPlacement({ week: 5200 }, TOTAL_CELLS).reason === 'out-of-range',
'week 5,200 is one past the end and says so rather than drawing nothing')
ok(anchorPlacement({ week: -1 }, TOTAL_CELLS).reason === 'out-of-range', 'a negative week is named')
ok(anchorPlacement({ week: 1.5 }, TOTAL_CELLS).reason === 'not-a-whole-week', 'a fractional week is named')
ok(anchorPlacement({ week: 'soon' }, TOTAL_CELLS).reason === 'not-a-number', 'a non-numeric week is named')
// The deliberate case must stay SILENT: no anchor asked for is not an error, it is the default.
ok(anchorPlacement(null, TOTAL_CELLS).reason === null && anchorPlacement({}, TOTAL_CELLS).reason === null,
'and NO anchor at all reports nothing — the packet supplies none on purpose, so that is not a fault')
// ── "1 of K lists", where K is unsettled and this asserts nothing about it ──────────────────────
ok(shareOfLists(1, 27).percent.toFixed(2) === '3.70' && shareOfLists(1, 29).percent.toFixed(2) === '3.45',
'K is a prop: 27 and 29 both compute, and this check takes no side on DECISION-SHEET row 41')

View file

@ -11,7 +11,7 @@
* the component and which from the renderer.
*/
import { computed } from 'vue'
import { cellCount, scatterCells } from './packetCharts.js'
import { anchorPlacement, cellCount, scatterCells } from './packetCharts.js'
const props = defineProps({
rows: { type: Number, default: 100 },
@ -32,7 +32,8 @@ const props = defineProps({
const total = computed(() => cellCount(props.rows, props.columns))
const shadedSet = computed(() => (props.shaded > 0 ? scatterCells(total.value, props.shaded, props.seed) : new Set()))
const anchorIndex = computed(() => (props.anchor && Number.isFinite(Number(props.anchor.week)) ? Number(props.anchor.week) : -1))
const placedAnchor = computed(() => anchorPlacement(props.anchor, total.value))
const anchorIndex = computed(() => placedAnchor.value.index)
</script>
<template>
@ -52,7 +53,12 @@ const anchorIndex = computed(() => (props.anchor && Number.isFinite(Number(props
:class="{ 'lcal__c--on': shadedSet.has(i - 1), 'lcal__c--anchor': anchorIndex === i - 1 }"
/>
</div>
<p v-if="anchor && anchor.label" class="lcal__foot">{{ anchor.label }}</p>
<p v-if="anchor && anchor.label && anchorIndex >= 0" class="lcal__foot">{{ anchor.label }}</p>
<!-- An anchor that cannot be drawn is NAMED. Omitting it silently is indistinguishable, on
paper, from having chosen not to mark it. -->
<p v-else-if="placedAnchor.reason" class="lcal__unplaced" role="alert">
{{ labels.anchorUnplaced || 'anchor week not drawn' }}: {{ anchor.week }} ({{ placedAnchor.reason }}, grid is {{ total }} weeks)
</p>
<p v-if="labels.foot" class="lcal__foot">{{ labels.foot }}</p>
</figure>
</template>
@ -66,5 +72,6 @@ const anchorIndex = computed(() => (props.anchor && Number.isFinite(Number(props
.lcal__c--on { background: var(--pk-on, #2f6d4f); border-color: var(--pk-on, #2f6d4f); }
/* The anchor is a RING, never a fill: it marks a position without claiming the cell's meaning. */
.lcal__c--anchor { border-color: var(--pk-ink, #1b1a17); border-width: 2px; }
.lcal__unplaced { font: 700 12px Georgia, serif; color: #8a1c1c; margin: 6px 0 0; }
@media print { .lcal__c { print-color-adjust: exact; -webkit-print-color-adjust: exact; } }
</style>

View file

@ -255,6 +255,27 @@ export function scatterSpread(picked, columns) {
return { cells: picked.size, rows: rows.size }
}
/**
* Where an anchor week sits in the grid, or a reason it does not.
*
* ★ SAME SILENT-DROP CLASS AS THE FUNNEL'S INFINITY BRACKET, found by sweeping for it rather than
* waiting for it. The component matched the anchor by comparing a cell index to Number(anchor.week)
* with only an isFinite guard, so week 6000 on a 5,200-cell grid, a negative week, or a fractional
* one matched NO cell and drew nothing — the anchor the caller asked for simply was not there, with
* no error and no gap. On a printed page that is indistinguishable from "we chose not to mark it".
*
* Returns { index } when it can be drawn, or { index: -1, reason } when it cannot, so the component
* can say so instead of silently omitting it.
*/
export function anchorPlacement(anchor, total) {
if (!anchor || anchor.week == null) return { index: -1, reason: null } // no anchor asked for
const w = Number(anchor.week)
if (!Number.isFinite(w)) return { index: -1, reason: 'not-a-number' }
if (!Number.isInteger(w)) return { index: -1, reason: 'not-a-whole-week' }
if (w < 0 || w >= (Number(total) || 0)) return { index: -1, reason: 'out-of-range' }
return { index: w, reason: null }
}
/** People per cell, for the caption the brief asks for explicitly. */
export function peoplePerCell(population, rows, columns) {
const total = cellCount(rows, columns)