@microcharts/react@0.1.0
Minor Changes
-
69eb408: Batch 1 wave 1 — four scalar glyph charts, each with a static RSC-safe entry and an
interactive/interactiveentry:TrendArrow(./trend-arrow) — direction glyph (arrow/triangle/chevron),flatBand
noise floor,showValuegutter,positivepolarity.StatusDot(./status-dot) — five paired shape+color states (ok/warn/error/off/busy),
pulsehalo, extensiblestatesvocabulary.HeatCell(./heat-cell) — one calibrated color step (shared 5-step ramp), shared
shapecell vocabulary, optional centered value label.Progress(./progress) — zero-anchored bar + direct percent label,segments
stepped mode, honest >100% clamp (label carries the truth), burn-down wording.
Also:
ENlocale dictionary is now composed from per-shape modules (EN_SERIES+
EN_SCALAR) so each chart bundles only its own summary templates; newScalarStrings
keysscalarDir,flatChange,status,level,progress,remaining,stepsDone. -
16a3914: Batch 1 wave 2 — five tick/cell strip charts, each with static +
/interactiveentries:RugStrip(./rug-strip) — raw observations as ticks, density via opacity tiers,
highlightfor "you are here", never downsamples.MiniBar(./mini-bar) — zero-anchored categorical bars, honest data order,sort,
highlight, signed data withpositivepolarity.PictogramRow(./pictogram-row) — countable units (●●●○○), true fractional units as
circular segments (no clipPath ids),renderPointescape hatch.Seismogram(./seismogram) — event density/intensity ticks,barcodemode,
spike-preserving max-per-bucket downsampling, summaries always from raw values.HeatStrip(./heat-strip) — 1×N calibrated intensity cells sharing ActivityGrid's
vocabulary; empty ≠ zero; density-adaptive gaps.
Kernel fix:
scaleLinearnow treats denormal-span domains as degenerate (the slope
overflowed to ±Infinity and0 × Infinitypoisoned coordinates with NaN). New summary
template modules:EN_CATEGORY,EN_DIST,EN_SLOTS(slot-empty announcements — also
fixes the latent "No data.." double period in ActivityGrid's null-cell announcement). -
7bb9ee1: Batch 1 wave 3 — five categorical/relationship charts, each with static +
/interactive
entries:DotPlot(./dot-plot) — named values on one scale;stemflips to a zero-anchored
magnitude read; deterministic label drop-out and coincident-dot de-overlap.Dumbbell(./dumbbell) — hollow→filled before/after per row;positivevalence for
changes (documented: drop it for ranges).PairedBars(./paired-bars) — actual vs reference on ONE zero-anchored domain;
grouped or overlay-ghost modes; ref muted by opacity AND width.Slope(./slope) — two aligned columns, one y-domain; neutral untilpositive;
greedy endpoint-label dedup; dashed "incomplete" stubs for missing ends.MicroScatter(./micro-scatter) — 2-D position + Pearson r in the summary (claim and
evidence travel together); linear-only least-squarestrend;focalpoint.
New summary template modules:
EN_PAIRED(fromTo/pairs/slopes…),EN_SCATTER
(count + relationship tiers). Pluralization fixes incategories/pairstemplates. -
0c92adc: Batch 1 wave 4 — the annotations layer + seven part-to-whole/staged charts:
@microcharts/react/annotations—<Threshold>,<TargetZone>,<Marker celebrate?>,
<Callout>as declarative children; hosts pay only a tiny walker (mark renderers ship
with this entry). Sparkline/SparkBar retrofitted as hosts.SegmentedBar(./segmented-bar) — composition with a labeled Other rollup +
largest-remainder percents.HistogramStrip(./histogram-strip) — ≤ 12 uniform bins from raw observations;
valuehighlight.MicroBox(./micro-box) — five-number box; min-max or tukey whiskers; refuses fake
boxes (< 5 raw values → dots; non-monotonic stats → refused).ProgressRing(./progress-ring) — fixed 12-o'clock arc, butt caps,sweep
countdown mode; threshold-crossing announcements.MicroDonut(./micro-donut) — ≤ 4 wedges + Other;decorativeornament mode.Funnel(./funnel) — stepped columns + retained-share slats; rate-of-first mode;
inversion notes.LikertStrip(./likert-strip) — diverging valence bar; split/omit neutral (never
silently dropped); ends/net labels.
New
EN_COMPOSITIONstring module + distribution templates (binAt, distribution,
fiveNum, boxStat). Sparkline budget 3.65/4.65 kB pending sign-off (annotations walker). -
15b0a12: Batch 1 wave 5 — six composed-series charts:
Waterfall(./waterfall) — signed deltas bridging into a total; zero-anchored total
bar on by default;positive="down"for cost bridges.BumpStrip(./bump-strip) — rank trajectory on an inverted ordinal scale (#1 on
top); change dots; "#5" → "#1" end labels; null = unranked gap.DualSparkline(./dual-sparkline) — series vs benchmark on ONE shared domain;
dashed/thinner/neutral reference; coincident-endpoint dedup.curve="step"renders
as linear (documented — keeps the entry inside the 3 kB hard cap).StackedArea(./stacked-area) — share-normalized stack, ≤ 3 series;
variant="ridge"skin;order="asc".Ohlc(./ohlc) — candle/bars price action, ≤ 20 periods rendered (never averaged);
valence + geometry encode direction.Horizon(./horizon) — 2–3 fold horizon band; mirror/offset modes; authored
baseline; summaries read the unfolded values.
EN_COMPOSEDsplit into per-chart string modules (EN_FLOW/EN_VS/EN_STACK/
EN_OHLC) so no chart pays for another family's templates. Horizondomainprop is
now actually wired to the geometry (was silently ignored). -
cf601c5: Batch 1 wave 6 — the two date-structured charts, completing the core-29:
CalendarStrip(./calendar-strip) — the last few weeks day by day on a real
weeks × 7 UTC calendar grid; value days step the shared intensity ramp, zero days
show the track, no-record days render as outlines (empty ≠ zero), future days are
blank. 2-D keyboard nav announces real calendar days.EventTimeline(./event-timeline) — spans + point-event diamonds on one linear
time axis; window clipping is flat-cut honest; coverage merges intervals (overlaps
never double-count); authorednowtick.
New core: cached
makeDateFormatter(UTC-forced for calendar charts) +EN_CALENDAR/
EN_TIMELINEstring modules.core/calendarsplit intocalendar(day parsing) and
calendar-grid(week-grid/year math) so ActivityGrid stops carrying grid math it never
calls. Fixed:normalizeSharescould emit a negative share when the float remainder
landed on a denormal-tiny entry (property-test counterexample) — remainder now folds
into the largest share. -
425f933: Batch 2 wave 1 — five decision micrographs, each with a static RSC-safe entry and an
interactive/interactiveentry:CoverageStrip(./coverage-strip) — presence/absence on a time strip;null(no
measurement) is hollow,0(a measured zero) is filled, so absence never masquerades
as zero.expectedmakes trailing missingness count;mode="intensity"shades measured
cells;label="percent".BenchmarkStrip(./benchmark-strip) — a focal dot against the peers' empirical
quantile bands; the stated percentile uses a mid-rank rule; small samples fall back to
min–max.range,median,label,positive.PercentileLadder(./percentile-ladder) — p50/p90/p99 as graduated ticks on a
zero-anchored track;scale="log"renders an in-chartlogtag (never silent);
ps,label,dots.GradedBand(./graded-band) — nested central intervals graded by opacity, never a bar
from zero;levels(1–3),valuedot,softEdge,label="median".IconArray(./icon-array) — one rate made countable in a fixed N-unit grid with the
denominator visible; no partial-unit fills (sub-unit rates are flagged);of(10/20/100),
label,shape,positive.
Also: new per-family summary-string modules
EN_COVERAGE,EN_QUANTILE,EN_FREQ(each
chart bundles only its own templates) and the correspondingSummaryStringskeys. -
425f933: Batch 2 wave 2 —
BurnChart(./burn-chart), static +/interactiveentries:BurnChart— a dashed plan line (full length to the deadline), the solid
actual line to today, a today tick, and a dotted projection whose slope
is a linear fit over the lastmax(2, ⌈today/3⌉)actual points — provisional by
construction, never a smoothed or optimistic curve.label="gap"states the
signed schedule landing vs the deadline (e.g.+2 d), colored by valence with
the sign in text.mode="down" | "up",projection={false}for retrospectives.
A flattened recent burn never reaches zero: no landing, and the summary says
"not finishing at the current pace" outright.
New
EN_BURNsummary module (burn,burnNoPlan,burnLanding,burnFlatlined,
burnAt,burnAtProjected,burnRemain/burnDone). -
425f933: Batch 2 wave 2 —
ControlStrip(./control-strip), static +/interactiveentries:ControlStrip— a Shewhart individuals control chart: the band is center ± 3σ̂
where σ̂ = mean moving range ÷ 1.128 (the individuals estimator, stated —
sample SD is not used, it inflates limits under drift). In-control points are bare
vertices; only out-of-control points are marked (ringed, negative).limits="sigma" | "percentile"(empirical p0.135/p99.865 for skew),baselinefor a golden-period
center,rules="we"for the enumerated Western Electric subset (WE-1/2/4, no rule
fires silently),dots="all"for sparse series. Fewer than 10 points → dashed band- "limits provisional"; zero moving range → band collapses to the center hairline.
New
EN_CONTROLsummary module (control,controlInControl,controlProvisional,
controlAt). -
425f933: Batch 2 wave 2 —
ErrorBudget(./error-budget), static +/interactiveentries:ErrorBudget— budget remaining (0–1) against the steady-burn diagonal (the
pace that exactly spends the SLO window); below the diagonal = burning too fast.
Faster burn-rate reference lines (the Google-SRE 1×/6×/14.4× convention, not
physics) render as faint region context, never data ink;ratesis configurable.
windowsets the full window length (so "now" can sit mid-window);currentRate
is the observed slope over the lastmax(2, ⌈n/6⌉)steps ÷ steady. A budget that
hits 0 before the window ends stops at an ✕ and the summary reads "Budget exhausted
at day N of M." Values outside [0,1] are clamped.
New
EN_ERROR_BUDGETsummary module (errorBudget,errorBudgetExhausted,
errorBudgetAt). -
425f933: Batch 2 wave 2 —
ForecastCone(./forecast-cone), static +/interactiveentries:ForecastCone— history as a solid line, then a fan of prediction bands (p80
outer, p50 inner) widening over the horizon with a dashed median. The fan's
entire honesty is visible confidence decay, so three rules are enforced, not
offered as options: at most 2 bands (a 95% band reads as false tail confidence at
micro scale), the median is always dashed (an estimate never renders as fact), and
a cone that fails to widen is flagged (widening: false), never auto-inflated. An
optionaltargetline adds a clearance clause (clears / straddles / misses).
Reversed[hi, lo]pairs are swapped; empty history renders a cone-only cell.
New
EN_FORECASTsummary module (forecast,forecastClearance,forecastAtHistory,
forecastAtForecast). The interactive entry is region-aware (history value vs forecast
median + interval). ThesoftEdge/curvecosmetic variants are deferred (plan/12). -
425f933: Batch 2 wave 2 —
NetFlow(./net-flow), static +/interactiveentries:NetFlow— inflow area above a zero baseline, outflow mirrored below on one
shared magnitude scale (never independently balanced), with the net line
(in − out) on top restoring the precise decision value.mode="area" | "bars"
(bars, and single periods, avoid an area through one point);net={false}for
gross flows only;positive="down"swaps the valence coloring for debt-paydown
contexts (color = which direction is good, position = in/out identity — two
independent channels). Negative inputs are invalid (flows are magnitudes) and
coerced to 0; the net sign is stated in the label's text, never color-alone.
New
EN_NET_FLOWsummary module (netFlow,netFlowAt,netFlowNoFlow) — the
summary and live region always pair the net with its gross. -
425f933: Batch 2 wave 2 —
QuantileDots(./quantile-dots), static +/interactiveentries:QuantileDots— a quantile dotplot:countdots (default 20) at equal-probability
quantiles (Kay/Fernandes binning fromcore/quantile), stacked into columns. Each
dot ≈ a 1-in-count chance — NOT a raw observation. With athreshold, the dots past
the line are re-inked accent AND ringed (never color-alone), and the summary uses
frequency framing ("10 in 20"), never a bare percentage.count(capped at 25),
side="above" | "below". The interactive entry is the probe: hovering / arrowing
moves a live threshold and the count past it recomputes purely.
New
EN_FREQkeysquantileDots/quantileDotsRange(shares the frequency-framing
module with IconArray). Audit flag (plan/12): the studied dotplot (Kay 2016 /
Fernandes 2018) used 50 dots; the 15–20 default is a micro-scale countability judgment,
not a validated equivalence. -
425f933: Batch 2 wave 2 —
RateVolume(./rate-volume), static +/interactiveentries:RateVolume— a precise rate line over deliberately low-precision, zero-anchored ghost
volume bars (the denominator). There is no prop to remove the bars, and a rate on zero
volume is never plotted (line gap + zero bar) — the lie this type prevents.minVolume
flags a thin denominator by rendering the rate mark hollow (shape cue, survives
forced-colors);curve="linear" | "step"(no smooth — a rate line must not imply
between-period values); separatevolumeFormat. The summary and the interactive live
region never state a rate without its volume.
New
data-mc-ink="ghost"ink-role (neutral, low-opacity context) instyles.csswith a
forced-colors mapping. NewEN_RATE_VOLUMEsummary template module (rateVolume,
rateVolumeShort,rateVolumeAt,rateVolumeNoEvents). -
425f933: Batch 2 wave 2 —
RetentionCurve(./retention-curve), static +/interactiveentries:RetentionCurve— a step line (cohort periods are discrete) on a y-domain
locked to [0,1] (the full range is the honest frame for a share; truncating
the floor manufactures drama). Detects and marks a plateau (mean |Δ| over the
lastmax(3, ⌈n/3⌉)periods < 0.005) with a dotted horizontal; an optional
benchmarkpeer curve rides behind as a subordinate dashed muted ghost.
curve="smooth"for editorial contexts (docs note step is the honest default);
plateau={false}for the raw curve. Percent input (max > 1.001) is divided by
100; non-monotone bumps (resurrection) render as-is, never sorted or smoothed away.
New
EN_RETENTIONsummary module (retention,retentionNoPlateau,retentionAt). -
425f933: Batch 2 wave 3 —
ABStrips(./ab-strips), static +/interactiveentries:ABStrips— two graded quantile strips on ONE shared scale (p5–95 outer, p25–75
inner middle half, median dot; row A muted, row B accent). The visible overlap of
the middle halves is the answer, and the overlap number is always in the summary —
an average delta without its spread is how A/B results lie. Never a bare mean bar;
the delta label never appears without the strips behind it.labels,positive
(colors the delta's valence, sign always in text),label="delta" | "none".
Identical arms → "no clear difference"; disjoint → "clearly separated"; an arm with
n < 8 falls back to a min–max band. The interactive entry roves rows (↑/↓) and
quantile edges (←/→).
New
EN_ABsummary module (ab,abSeparated,abNoDiff,abRow,abEdge). -
425f933: Batch 2 wave 3 —
ChangePoint(./change-point), static +/interactiveentries:ChangePoint— when did the behavior change level? Regime shading (neutral
identity, not valence) + per-regime mean hairlines + the series line + a break
marker, so a spike is read against the regime it broke. The detector is a
documented heuristic, not statistics: a two-segment mean-shift via binary
segmentation, accepted only when the split cuts the pooled sum-of-squares by
more thanBREAK_SS_RATIO(0.2) and the mean gap clearsBREAK_EFFECT_SIZE
(0.8) × the pooled SD — both are named exports, property-tested (no break on
constant / low-noise series, exact index on a clean step, never more than
max).breaks("auto"or explicit indices — the recommended production
path, detection off),max(1–3),means,label="delta". Gradual ramps are
honestly found to have no level shift (named limitation → Sparkline). The
interactive entry steps points with ←/→ (value + regime) and cycles breaks with
Tab (each announcing the mean shift).
New
EN_CHANGE_POINTsummary module (changePoint,changePointNone,
changePointAt,changePointBreak). -
425f933: Batch 2 wave 3 —
CyclePlot(./cycle-plot), static +/interactiveentries:CyclePlot— a cycle plot (seasonal-subseries chart): what repeats beneath the
trend, and is any slot drifting? The series is reshaped row-major intoperiod
slots; each slot shows its own raw values across cycles as a muted polyline in
time order (never smoothed, never joined across a slot boundary) plus a
mean/median tick, and the accent spine connects the slot centers — seasonality
and drift kept as separate reads.period(4–12, required),slots(names for
summaries),center("mean"/"median"),trend("line"/"none"),spine
(off for drift-only),cycleUnit. Ragged final cycles and per-slot counts are
carried honestly;period ≥ lengthdrops to a spine-only read; nulls are
excluded from a slot, never interpolated. The interactive entry steps slots with
←/→ (mean, cycle count, drift) and individual observations with ↑/↓.
New
EN_CYCLEsummary module (cycle,cycleNoDrift,cycleAt,cyclePoint). -
425f933: Batch 2 wave 3 —
DataDiff(./data-diff), static +/interactiveentries:DataDiff— what changed between two versions of the data? One diverging bar
per key: removed leftward, added rightward, both always drawn on one
symmetric shared scale, so a +500/−480 churn never looks like a +20/−0 trickle.
labels(in-chart key tags),net(a tick at added−removed — a summary mark,
never a stand-in for the two bars),sort("none"keeps the input order,
which is often meaningful),label="totals"(a+added / −removedfooter),
domain(a shared scale for cross-chart comparison). Negative counts are
magnitudes → clamped to 0; a 0/0 key keeps a hairline placeholder tick (absence
of change ≠ absence of the key); more than 12 rows warns and steers to a table
of DataDiffs rather than truncating silently. The interactive entry steps the
rows and announces each key's added, removed, and net change.
New
EN_DATA_DIFFsummary module (dataDiff,dataDiffEmpty,dataDiffAt). -
425f933: Batch 2 wave 3 —
EnsembleGhosts(./ensemble-ghosts), static +/interactive
entries (the final Batch 2 chart):EnsembleGhosts— what could happen across the simulated futures? A faint
bundle of member paths with one emphasized representative, because a mean line
hides that futures disagree in shape, not just endpoint. Ghost selection is
deterministic — members ranked by endpoint value and picked at evenly
spaced quantiles of that ranking (noMath.random, no jitter), so the same
input renders identically every time.ghosts(default 8, cap 12),emphasis
("nearest-median"= a real member closest to the pointwise median /
"median"= the synthetic median, flagged in the summary / a pinned member
index),endpoints(ghost endpoint dots). Members of unequal length each draw
to their own length; NaN members are excluded; a single member steers to
Sparkline.- The interactive entry is the HOP loop: on hover/focus it flips through the
members one at a time (~400 ms/frame ≈ 2.5 Hz), looping until the pointer
leaves. Reduced motion turns the loop off — ←/→ step members discretely (the
same information without motion) — and the live region announces only on a
keyboard step or when the loop stops, never per frame. A static frame is not a
HOP; the loop lives only in the interactive entry.
New
EN_ENSEMBLEsummary module (ensemble,ensembleSingle,ensembleAt). -
425f933: Batch 2 wave 3 —
ParetoStrip(./pareto-strip), static +/interactiveentries:ParetoStrip— descending bars + a cumulative-share line on a fixed 0–100%
scale that spans the full height and is never rescaled to steepen the curve.
Bars up to the threshold crossing are accent (the vital few); the rest are muted
— the chart's one job is to say where to stop reading.threshold(default 80,
a working reference not a law;falseturns it off),max(categories beyond it
roll up intoOther, always rendered last and never re-ranked). Negatives are
excluded (a composition can't be negative); zero total → "No recorded ".
The interactive entry steps the bars (share + cumulative) and T jumps to the
crossing.
New
EN_PARETOsummary module (pareto,paretoTop,paretoEmpty,paretoAt). -
425f933: Batch 2 wave 3 —
QuadrantDot(./quadrant-dot), static +/interactiveentries:QuadrantDot— where does this item sit in the 2×2, against the field? A focal
dot placed by 2-D position, a hairline cross at the split (default = domain
midpoints, always overridable but never hidden), a faint tint on the
focal's quadrant, and tiny muted ghost dots for the peers. The read is quadrant
membership first, so it lives at glyph scale (24×24) with no in-chart text —
axis meaning rides ontitle+ summary (skipping them is the documented
anti-pattern).xDomain/domain,split,field,quadrants(names in
reading order, summaries only),xLabel/yLabel,region(tint off for dense
grids). Boundary rule: ≥ split ⇒ right/top; a degenerate axis centers the focal
and suppresses that split line. The interactive entry cycles the peers
nearest-first with coords + quadrant, and a pointer picks the nearest dot.
New
EN_QUADRANTsummary module (quadrantName,quadrant,quadrantLone,
quadrantAt) and adata-mc-ink="region"tint role (accent 5%, drops out under
forced-colors). -
425f933: Batch 2 wave 3 —
ShiftHistogram(./shift-histogram), static +/interactiveentries:ShiftHistogram— mirrored before/after histograms on SHARED bin edges (before up
muted, after down accent) with the median shift as the takeaway. Bar heights are
per-side proportions (each side's counts ÷ its own n) on one shared height scale —
the only allowed normalization — so unequal sample sizes cannot fake a shift, and
the summary carries both n's when they differ. The mirror carries identity, not
valence (up ≠ good).mode="mirror" | "overlay",bins(shared, Sturges ≤12).
One side empty → single histogram + "no sample"; no change → "unchanged".
New
EN_SHIFTsummary module (shift,shiftHeld,shiftSamples,shiftOneSide,
shiftBin). Median computed inline (not viacore/quantile) to keep the histogram
bundle under the 3 kB static hard cap. Side tags dropped — they collided with the
bars at every micro size; position + color + summary carry identity (plan/12). -
01b6e7f: Batch 3 (expressive) —
BalanceBeam(./balance-beam), static +/interactiveentries:BalanceBeam— which of two sides outweighs, and roughly by how much. A beam
tilts toward the heavier side; direction is instant and the angle saturates at
maxTilt(read direction + rough magnitude, not an exact ratio — docs steer precise
ratios toPairedBars/Delta). The two weights are area-true (half = k·√value). The
beam endpoints are pre-rotated in geometry (no SVG transform in the static entry, so
containment is provable from coords).mode="ratio"(share-of-whole, default) or
mode="difference"(absolute, scaled bydomain);shape="square"|"round",
label="values". The interactive entry eases the beam to its new tilt (CSS geometry
transition), reveals a side's value on hover or ←/→, and announces when the heavier
side flips.
New
EN_BEAMsummary module (balanceBeam,balanceBeamBalanced) + a beam-tilt CSS
transition rule. Node budget ≤ 6. -
01b6e7f: Batch 3 (expressive, motion) —
BreathingDot(./breathing-dot), static +/interactiveentries:BreathingDot— how loaded is the system right now, ambiently? The static frame is a real
chart with zero JS: a core dot colored by threshold band (calm / elevated / strained) plus a
level ring whose distance from the core encodes the level. The interactive entry adds a WAAPI
pulse whose rate and amplitude are snapped to the three bands — motion IS the encoding
(the allowed idle-loop exception, because the loop parameter is the datum).thresholds
(default[0.5, 0.8]),label="value"(percent numeral escape hatch).- Reduced-motion + viewport gating — the pulse never runs under
prefers-reduced-motion
(reduced-motion readers get exactly the static frame; the ring offset already carries the
level) and pauses when the dot scrolls off-screen (a sharedIntersectionObserver). Band
color is always doubled (ring offset statically, pulse rate in motion), never color-alone. - Honesty — an unknown value (
null/NaN) makes the dot gray, drops the ring, and never
pulses: an unknown system must not look calm. The live region announces band changes only,
never per tick.
New
EN_BREATHING_DOTsummary module (breathingDot/breathingDotUnknown+loadBands) and
a sharedsrc/shared/motion.ts(usePrefersReducedMotion,useInViewportwith one shared
observer) that the batch's motion charts reuse. Node budget 3. -
01b6e7f: Batch 3 (expressive) —
BubbleRow(./bubble-row), static +/interactiveentries:BubbleRow— roughly how a few magnitudes compare, with physical presence: a row of
circles whose area (r ∝ √value) is proportional to value. This is the catalog's
honest LOW-precision exemplar — area is the weakest common channel, so the LOW rating
and the standing "for precise comparison, use MiniBar" steer are printed in the
catalog,/catalog.json, and the docs header. Value numerals are ON by default
(label="value"), because a low-precision channel owes the reader the number;both
adds the category label,noneopts out.align="center"(specimen row, default) or
"baseline"(weights on a shelf). No sorting prop — order = data order (reordering is
the caller's statement). Zero renders a small presence ring. The interactive entry
roves bubbles with ←/→ (or hover), announcing each one's exact value.
New
EN_BUBBLEsummary module (bubbleRow,bubbleAt). Node budget 2 per bubble. -
01b6e7f: Batch 3 (expressive) —
CitySkyline(./city-skyline), static +/interactiveentries:CitySkyline— how groups compare on size and how activated each is, two variables
in one row. Building height is the primary, zero-anchored, high-precision channel
(like a MiniBar); the lit-window fraction is a secondary low-precision channel
("mostly lit / half lit / dark", not a number). Windows are a fixed 2 columns, filled
bottom-up, quantized to the window count. Omitliteverywhere → a plain bar row. NO
roofline/antenna/width variation — width, roof, and ground are constants (earn every
mark). The secondary channel drops out before the primary: a tower too short for a
window row is solid, and itslitstill shows in the summary and on hover.labels,
ground,label="value". The interactive entry roves buildings with ←/→, announcing
each as "Platform: 46; 70% lit."
New
EN_SKYLINEsummary module (citySkyline,citySkylineAt,citySkylineAtLit).
Node budget 2 per building + 1 (merged windows path per building). -
01b6e7f: Batch 3 (expressive, motion) —
CometTrail(./comet-trail), static +/interactiveentries:CometTrail— where is the value now, and where has it just been? The static frame is a
decaying dot-sparkline with zero JS: a fading trail of recent points + a bright head dot at the
current value + the now-value numeral. The interactive entry eases the head to each new value
(WAAPI transform, ~220 ms) and decays the old head into the trail — motion only on data change,
no idle loop.trail(points kept visible, default 12, cap 20),label="last"(default).- Honesty — opacity encodes AGE only, never value (the y position does value);
traillength
is recency context, so changing it never changes the head read. The dot jumps to truth, eased —
never interpolated between updates (a stalled stream goes still, which is the signal). Reduced
motion → instant reposition (the static encoding is already complete); off-viewport paused. - Arrow-Left steps back through the trail ("3 updates ago: 74."), Arrow-Right returns toward now,
each announced through a polite live region.
New
EN_COMET_TRAILsummary module (cometTrail/cometTrailNow/cometTrailAt+cometTrends
trend words). Node budget ≤ trail + 2. -
01b6e7f: Batch 3 (expressive) —
Constellation(./constellation), static +/interactiveentries:Constellation— when the rare events happened, and how big. Each event is a dot
placed by time (x) and value (y); an optional magnitude drives an area-true dot
size (r ∝ √m). A hairline chronology line connects the events in time order. Built
for rare events (a dozen or fewer); dense streams steer toSeismogram/
EventTimeline.connect(defaulttrue; off for a pure scatter),label="max"
(numeral beside the largest event),xFormat(formats time for the summary — e.g. a
month name),domain/xDomain.- Honesty — dot size is area-true, a deliberately low-precision channel, so the
number lives in the summary and the hover readout, never in a size you must measure.
When no event carries a value, the vertical position is deterministic seeded jitter
that encodes nothing (SSR/hydration-stable viacore/jitter); the connector's slope
is then meaningless and the summary never reads vertical position. Time is sacred — the
x position is never jittered, so simultaneous events share an x. - The interactive entry steps through the events chronologically (←/→), announcing each
event's time, value, and magnitude through a polite live region, with a matching hover
readout and a focus ring on the active event.
New
EN_CONSTELLATIONsummary module (constellation/constellationOne/
constellationAt). Node budget n+1. -
01b6e7f: Batch 3 (expressive) —
DicePips(./dice-pips), static +/interactiveentries:DicePips— a small count or severity read instantly as a canonical die face,
subitized rather than counted. Only the real 1–6 pip patterns exist;0is an
empty face (zero, not missing), and any value> 6renders the exact numeral in
the face instead of inventing a seven-pip layout — the documented honesty fallback.
face={false}drops the outline for dense table columns. The interactive entry
cross-fades the pip set on change and announces the new face through a polite
region; the pips are one value, so there is no cursor to move.
New
EN_DICEsummary module (dicePips,dicePipsOver). Node budget ≤ 7
(face + up to six pips). -
01b6e7f: Batch 3 (expressive) —
FatDigits(./fat-digits), static +/interactiveentries:FatDigits— the numeral is the exact value; font weight is a redundant,
preattentive tier (5 or 3 ordinal steps mapped fromdomain) so big numbers pop
as you scan a dense column. Weight is never the primary read — it is ordinal and
coarse, and the number is right there.encode="value"weights the whole numeral;
encode="digit"weights each digit by its own magnitude.tiers={3|5}. Always
pass adomain(a lone number renders at the middle weight, documented). The
interactive entry eases the weight to its new tier on variable fonts (snaps
otherwise) with no layout shift, and announces value + tier through a polite region.
New
EN_FATsummary module (fatDigits,fatDigitsPlain) + one motion-layer CSS
rule for the weight transition (tspans are inside.mc-root, so the reduced-motion
block gates it). Deviation from FatFonts (plan/12): the source encodes magnitude
as glyph ink area via a custom font; shipping a font would break zero-dep
(non-negotiable #1), so we map to discretefont-weighttiers on the inherited font —
ordinal, never continuous; the numeral is always exact. Static budget 1.6 kB (the
cachedmakeFormatterfor grouped numerals is needed) — above the 1.5 kB Delta-class
target but well under the 3 kB hard cap; logged for the batch gate. -
01b6e7f: Batch 3 (expressive) —
FillWord(./fill-word), static +/interactiveentries:FillWord— the label IS the bar: a muted word overlaid with an accent copy of
itself, clipped (CSSclip-path: inset()— no<clipPath>element, so no
generated id) to the value fraction of the word's OWN inked extent, so 50%
visually bisects the word and the fill is never of a hidden wider track. Glyph
extent is estimated deterministically (0.62 em/char) and pinned withtextLengthlengthAdjust, so containment is provable server-side without measuring text.
mode="fill"|"drain"(drain empties from the left for a remaining-time / TTL
story),label="value"appends the percent. The interactive entry glides the ink
edge with a reduced-motion-gated clip-path transition and announces changes
through a polite region, throttled to ≥1 s.
New
EN_FILL_WORDsummary module (fillWord,fillWordRemaining) + one motion-layer
CSS rule for the clip-path transition (the accent copy is inside.mc-root, so the
existing reduced-motion block gates it). Node budget 2 (+1 numeral). -
01b6e7f: Batch 3 (expressive) —
GardenGrid(./garden-grid), static +/interactiveentries:GardenGrid— ActivityGrid's grayscale sibling: it encodes a calendar-shaped
activity rhythm as quantized dot area (single ink) instead of color, so it
survives print and monochrome. The radius is √-quantized (r = rMax·√(k/S)) so
perceived area steps evenly — a linear radius map would exaggerate the highs
quadratically. A zero cell is a hairline ring (present, quiet); anullcell is
nothing (missing ≠ zero).rows(default 7;1= strip),steps={3|5},
empty="ring"|"blank". The interactive entry walks the grid in 2-D with the arrow
keys (or hover), announcing each cell's ordinal step — "3 of 12: 8, step 2 of
5." — never a false-precise value.
New
EN_GARDENsummary module (gardenGrid,gardenCell). Node budget 1 per cell
(cap 400, dev-warned). -
01b6e7f: Batch 3 (expressive, motion) —
HeartbeatBlip(./heartbeat-blip), static +/interactiveentries:HeartbeatBlip— is it alive, and how busy? A baseline with an ECG-style spike at each event
across the recent window (x = how long ago). The static frame shows the spike positions with
zero JS; the interactive entry sweeps the trace left in real time so the blip frequency IS the
event rate.window(default 60000 ms),now(explicit clock — pass from the data layer;
the static entry never callsDate.now(), so SSR stays deterministic),label="count".- Honesty (the load-bearing rule) — every spike is ONE real event; nothing is synthesized on
a timer. An empty window → a flat baseline that IS the down signal (shape, never color), with a
"no events" state distinct from no-data. Events afternoware clamped (clock skew); events
older than the window drop. - Reduced-motion + viewport gating — the sweep pauses off-screen (shared observer) and never
runs underprefers-reduced-motion(the static strip re-renders on each data change instead —
same information). A new event blips the endpoint once (WAAPI scale) and announces through a
polite live region; there is no per-spike navigation (spikes are transient — the summary is the
record).
New
EN_HEARTBEATsummary module (heartbeat/heartbeatFlat+ translatableheartbeatWindow
/heartbeatAgoduration helpers). Node budget 3. -
01b6e7f: Batch 3 (expressive) —
Honeycomb(./honeycomb), static +/interactiveentries:Honeycomb— how many of the available slots are taken, as filled cells in an
area-filling hex grid. The unit is the cell, so the count is genuinely countable
(total ≤ 60 — above that unit counting stops being countable and the chart steers
toProgress; refusing is the feature). Pointy-top hexes, odd-row offset, near-
square by default, filled row-major from the top-left so occupancy reads as a sweep.
The whole grid is exactly two<path>nodes (filled + empty) regardless of total.
total(capacity),rows(number orauto;1= strip),empty="outline"|"dim".
A value past the total fills every cell but the accessible name still states the true
number — occupancy is never silently clipped. The interactive entry announces the
count on change and reveals value/total on hover; cells are anonymous units, so there
is no per-cell cursor.
New
EN_HONEYCOMBsummary module (honeycomb). Own hex math (hexPath+
axial→pixel), chart-local. Node budget 2. -
01b6e7f: Batch 3 (expressive) —
Hourglass(./hourglass), static +/interactiveentries:Hourglass— how much time is gone AND how much remains, the two-sided story a
progress bar can't tell. Sand fills the top chamber (remaining) and the bottom
(elapsed), both area-true: a naive linear-height fill in a triangular bulb would
overstate early progress by up to 2×, so the geometry uses closed forms (top:
remainingrfills from the apex toH·√r; bottom: elapsedefills from the base
toH·(1−√(1−e))) — sand areas are exactly proportional (lie factor 1).valueis
the elapsed fraction (same polarity asProgress). A thin neck stream is a binary
"running" state mark, rendered only while0 < value < 1(finished and not-started
are shape-distinct).streamtoggle,label="remaining"|"elapsed". The interactive
entry cross-fades the sand levels and announces only at documented thresholds
(50/90/100%).
New
EN_HOURGLASSsummary module (hourglass). Node budget 4. NomakeFormatter
(percents computed inline). -
01b6e7f: Batch 3 (expressive) —
MoonPhase(./moon-phase), static +/interactiveentries:MoonPhase— the illuminated area of a disc encodes a 0–1 fraction, area-true and
readable across cultures. The lit area equals the value exactly via a closed-form
terminator (a semi-ellipse withrx = r·|2f−1|; lit area = right semicircle ±
semi-ellipse =f·πr²), never the phase-angle approximation that under-lights
mid-cycle.mode="progress"is monotonic (0 new → 0.5 half → 1 full — the sprint/
quota story);mode="cycle"maps the real lunar sequence (0 new → 0.5 full → 1 new,
waxing then waning) — a data-semantic switch, never a preset. Waxing lights from the
right. The interactive entry cross-fades the lit region on change (an opacity swap,
never ad:path interpolation — no Safari), reveals the percent on hover/focus, and
announces through a polite region.
New
EN_MOONsummary module (moonPhase,moonPhaseCycle). Node budget 3 (disc +
lit region + outline). NomakeFormatter— the percent is computed inline. -
01b6e7f: Batch 3 (expressive) —
MusicStaff(./music-staff), static +/interactiveentries:MusicStaff— the shape of a short series read as a melody: each value is a note,
its pitch (vertical position on a five-line staff, quantized to line/space
positions) is the value, and left-to-right is time. Pitch is the ONLY channel —
no clefs, stems, beams, or bar lines (every other notation convention would be
decoration).range="ledger"(default, ±2 ledger positions with hairline ticks) or
range="staff"(clamp on-staff for dense cells);label="last"prints the final
value. Two adjacent equal values are spaced along the time axis, never dodged
vertically (that would change pitch = the data). The interactive entry steps notes
with ←/→.
Reuses
describeSeriesverbatim for the accessible name (same S1 pipeline as
Sparkline — no new summary template). Static budget 2.32 kB (the describeSeries /
seriesStats machinery) — above the spec's 2 kB target, under the 3 kB hard cap; logged.
Node budget n + 2. -
01b6e7f: Batch 3 (expressive, motion) —
OrbitStatus(./orbit-status), static +/interactiveentries.
Completes the 22-chart Batch 3 (expressive) catalog.OrbitStatus— how slow and how busy is this dependency right now? A service dot with an orbit
whose RADIUS = latency and whose DASH DENSITY = call rate (quantized to 5 steps, "denser dashes =
more calls") + a satellite. The static frame carries BOTH variables with zero JS; the interactive
entry mirrors the rate as the satellite's angular SPEED (the same 5 steps, so static and motion
decode identically) — motion IS the encoding (idle-loop exception, loop rate = the datum).
latencyDomain/rateDomain(insisted on — a lone radius is meaningless),alert(latency
threshold: satellite doubles + summary flags),label="latency"(ms numeral escape hatch).- Honesty — both channels are quantized 5-step ordinals; radius and speed come from the same
domains in both frames (no drift). The satellite's angular POSITION encodes nothing — only its
speed does (documented).rate0 → a solid, dash-free orbit; unknown latency/rate → gray, no
satellite, and no spin (an unknown dependency must not look healthy). The alert state is doubled
by the satellite size + the summary, never color-alone. - Reduced-motion → the static frame (dash density already carries rate); off-viewport paused
(shared observer); the live region announces threshold crossings only.
New
EN_ORBIT_STATUSsummary module (orbitStatus/orbitAlert/orbitUnknown, units in-template).
Built oncore/arc(evenDashes/polarPoint). Node budget 3. -
01b6e7f: Batch 3 (expressive) —
PolarClock(./polar-clock), static +/interactiveentries:PolarClock— the shape of a day or week cycle: when is it busy? Each value in a
cyclic series is a radial bar at its fixed cycle angle, growing outward from an inner
baseline (length ∝ value). 0 sits at 12 o'clock and the cycle runs clockwise. Works
for any n (24 hourly, 7 daily, or arbitrary).nowaccents the current segment
(position + color, never color alone),startrotates which index sits at 12 o'clock,
labelsadds hairline cardinal ticks,label="max"prints the peak value in a gutter,
formatSegmentlabels segments (hour formatting for n=24, weekday names for n=7).mode="opacity"switches the channel to fixed-length sectors whose 5-step fill
carries the value — a radialActivityGridfor very small sizes — a deliberate,
documented change of encoding.- Honesty — the channel is radial LENGTH from the inner baseline, never sector area;
a nonzero inner radius curbs the outer-area distortion and the docs ask you to compare
lengths, not wedges. Bars are always zero-anchored; anullsegment leaves a gap
(missing ≠ zero). - The interactive entry maps the cursor angle to a segment (lifting it to the accent),
arrows through segments circularly, and announces each segment's label and value through
a polite live region with a matching hover readout.
New
EN_POLAR_CLOCKsummary module (polarClock/polarClockFlat/polarClockAt+
weekdaysi18n array). Built oncore/arc(annulusSector). Node budget 3. -
01b6e7f: Batch 3 (expressive) —
SpiralYear(./spiral-year), static +/interactiveentries:SpiralYear— how did the year breathe? A calendar series wound onto an Archimedean
spiral: angle = position in the year (Jan at 12 o'clock, clockwise), each turn outward =
the next year. The value is a 5-step (or 3-step) opacity — an ordinal channel — so this
is a PATTERN instrument, and the docs steer exact reads toActivityGrid/HeatStrip.
cadence(day/week, inferred from length),startDate(anchors index 0 to a calendar
angle),steps={3|5},monthTicks(12 faint radial ticks, default on),mark="dot"|"arc".- Honesty — the spiral RADIUS encodes time only, never value; an outer mark is a later
date, not a bigger number. Opacity is 5-step-quantized and ordinal. Anullleaves a gap
in the spiral (missing ≠ a step-one mark). Marks are grouped by opacity step into ≤steps
merged<path>nodes, so the node count is O(steps), not O(days). - The interactive entry finds the nearest mark by 2-D distance, arrows along the spiral
chronologically, and announces each period and value through a polite live region with a
matching hover readout.
New
EN_SPIRAL_YEARsummary module (spiralYear/spiralYearAt). Built oncore/arc
(arc marks) +core/calendar-grid(dayOfYear,monthStartDays). Node budget ≤ 7. -
01b6e7f: Batch 3 (expressive) —
SproutRow(./sprout-row), static +/interactiveentries:SproutRow— how mature or healthy each item in a small set is, as an ordinal
growth-stage glyph (seed → sprout → leaf → bloom). Glyph height is strictly
monotonic with stage, so taller always reads as further along and the ordering
survives without the key; the four stages are discrete with no interpolated
half-stages (a growth metaphor must not fake continuity). Each item is one filled
path (thin stem + leaves/head), scaled to the usable height. Anullvalue is a
real gap — it draws only the soil tick, distinct from a seed.labels(category
labels under the slots) andlabel="value"(stage number above each glyph). The
interactive entry roves items with ←/→ (or hover), announcing each as "Acme: bloom,
stage 4 of 4." with a focus ring.
New
EN_SPROUTsummary module (sproutRow,sproutStage,sproutStageNames,
sproutEmpty— the four stage names carry the i18n contract). Node budget n + 1. -
01b6e7f: Batch 3 (expressive) —
TallyMarks(./tally-marks), static +/interactiveentries:TallyMarks— counts the way a human counts: four-and-strike clusters of five,
then the remainder, all in ONE merged stroke path (node budget 2 with the overflow
numeral). The count reads back exactly up tomax; past it the marks stop growing
and a+Nnumeral tells the truth, so a cell never blows out its width — marks are
never resized to fit.pen="ruled" | "drawn"(seeded, SSR-stable jitter for the
editorial voice — perturbs stroke rendering only, never the count) and
overflow="numeral" | "clamp"(clamp drops the numeral; the accessible name still
carries the true count). The interactive entry announces the new total through a
polite region and draws newly added marks in with a brief, reduced-motion-gated
stroke-dashoffset sweep; a count has no sub-parts, so focus reads the summary and
there is no cursor to move.
New
EN_TALLYsummary module (tally). Naming note: the spec named the pen variant
style, but every chart already exposesstyle?: CSSProperties; the knob ships as
pento keep that passthrough intact (recorded in plan/12). -
01b6e7f: Batch 3 (expressive) —
Thermometer(./thermometer), static +/interactiveentries:Thermometer— where a value sits on a calibrated ticked tube, and how close to
a goal. The fill anchors atdomain[0](never re-zeroed, never log — the ticks
calibrate the read, which is what buys the high precision); the fill rect width
equals the tube inner width so no clipPath/id is needed. An optionaltargetdraws
a line across the tube (distinct shape from the side ticks, never color-alone). The
bulb is instrument chrome (always full), never data.orientation="vertical" |"horizontal",bulb,ticks(count or explicit values),label="value",
domain(default[0,100]— a stated range, never auto-fit). A value beyond the
domain clamps the fill but the accessible name reports the true number. The
interactive entry reveals the value on hover/focus and glides the fill with a
reduced-motion-gated transition.
New
EN_THERMOMETERmodule (thermometer,thermometerTarget) + a fill-transition
CSS rule. Static 2.22 kB (needsmakeFormatter+scaleLinear) — above the spec's
2 kB target, well under the 3 kB hard cap; logged for the batch gate. -
01b6e7f: Batch 3 (expressive) —
TreeRings(./tree-rings), static +/interactiveentries:TreeRings— how growth accumulated period over period, like the rings of a tree:
oldest at the centre, each new period adds a ring outward whose thickness is that
period's value. The channel is radial thickness, NOT area (equal thickness at a larger
radius spans more area — the ring illusion; the docs say "compare thicknesses"). No
minimum visual thickness — a near-zero period looks near-zero, and a zero-value period
collapses its boundaries.accent="last"|"none"|indexemphasizes a boundary (1.5×
weight + accent color, never color alone);totalscales the disc to Σdata/total of
the radius (the cohort-age story);rings="stroke"|"fill";label="last". The
interactive entry steps rings from the centre out with ←/→ (radial pointer lookup).
New
EN_TREEsummary module (treeRings,treeRingAt). The full-annulus path is
inlined (evenodd fill) rather than importingcore/arc, keeping the static entry at
1.78 kB. Naming note (plan/12): the spec'sstylerender variant collides with the
universalstyle?: CSSProperties, so it ships asrings. Node budget n + 1.