@tangle-network/charts 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,32 +164,51 @@ A run page draws recorded events, never inferred ones. Every addition below has
164
164
 
165
165
  | Need | Static | React |
166
166
  |---|---|---|
167
- | Vertical events (first pass, deadline, died) and shaded spans (after pass, idle, no data) | `timeSeries(rows, { ..., marks, bands })` | `<Lines marks bands>` |
167
+ | Vertical events (first pass, deadline, died, release, steer, ruling, wake) and shaded spans (after pass, idle, no data, blocked) | `timeSeries(rows, { ..., marks, bands })` | `<Lines marks bands>` |
168
168
  | Lanes of spans with marks; an open span runs to `now` | `timeline(lanes, { measure, now, marks })` | `<Gantt lanes now marks>` |
169
+ | An agent tree over time: lanes under their parents, attempt ticks with ok and failed counts, walls across every lane, folding by subtree | `timeline(lanes, { measure, start, now, marks, bands, collapsed })` | `<TimelineView lanes options defaultCollapsed>` |
169
170
  | Part of a whole with untraced time hatched | `phaseWaterfall` (unaccounted leaves and the remainder are hatched) | `<Meter total segments>` |
170
171
  | One panel per arm on shared axes, with no-data panels and early ends | `smallMultiples(panels, { x, y, shared })` | `<FigureView figure>` |
171
172
  | Series that stay distinct without colour | later series add a dash and a marker shape | `series[].dash`, `series[].marker` |
172
173
 
173
174
  ```ts
174
- type ChartMark = { at: number; label: string; kind: "pass" | "deadline" | "died" | "event" };
175
- type ChartBand = { from: number; to: number; label: string; kind: "after-pass" | "idle" | "missing" };
176
- type TimelineLane = { id: string; label: string; spans: Array<{ start: number; end: number | null; state: string; note?: string }> };
175
+ type ChartMark = { at: number; label: string; kind: "pass" | "deadline" | "died" | "event" | "release" | "steer" | "ruling" | "wake";
176
+ verdict?: "pass" | "fail" | "unknown"; source?: string; lane?: string };
177
+ type ChartBand = { from: number; to: number; label: string; kind: "after-pass" | "idle" | "missing" | "blocked" };
178
+ type TimelineLane = { id: string; label: string; spans: Array<{ start: number; end: number | null; state: string; note?: string }>;
179
+ parent?: string; depth?: number; ticks?: TimelineTick[]; detail?: string[] };
180
+ type TimelineTick = { at: number; outcome: "ok" | "quota" | "rate-limit" | "oom" | "refused" | "aborted" | "other"; note?: string };
177
181
  type MultiplePanel = { id: string; label: string; unit?: string; domain?: [number, number]; points?: TimePoint[];
178
182
  missing?: { why: string; detail?: string }; end?: { at: number; kind: ChartMark["kind"]; label?: string }; marks?: ChartMark[] };
179
183
  type MeterSegment = { label: string; value: number | null; color?: string; kind?: "measured" | "untraced" };
180
184
  ```
181
185
 
182
186
  Mark kinds differ by stroke as well as colour: pass is solid, deadline dashed, died ends in a cross, and other events are dotted.
187
+ A release carries a diamond head, a steer a triangle, a ruling a square and a wake a circle; releases, steers and wakes stop at the plot's edge so frequent events never wall off the data, and a ruling crosses it.
188
+ A recorded `verdict` colours a mark (pass and fail take the status tones, unknown the muted tone), and a `source`, such as the operator or the keeper, follows its label.
189
+ Mark labels take up to three rows above the data, placed by measured width so none overlap; marks of one kind and verdict at one place share a rule and a label (`rc17 (+18)`), and a label with no room is left to the hover and the table.
190
+ A `blocked` band is a system-wide wall, such as a quota window: a breach wash across every lane under a solid top edge.
183
191
  Marks and bands appear in the figure's table (`Recorded events by …`) and, for React, in a text list under the plot.
184
- Static figures refuse marks or bands outside the x domain; React components throw `RangeError` for the same input, as `StackedBarChart` does.
192
+ `timeSeries` and `smallMultiples` refuse marks or bands outside the x domain, and `timeline` names them at its edge instead (below); React components throw `RangeError` for marks or bands they cannot place, as `StackedBarChart` does.
193
+ Axis labels never overlap at 720 or 358 px: `timeSeries` labels recorded positions spread evenly across the plot, and `timeline` uses the smallest clock step (1 min to 7 days) whose labels fit, naming the date at midnight.
185
194
 
186
- `timeline` refuses an open span without `now`, a span outside a supplied `start`/`end` window and reversed times. States named in `hatch` (default `missing`, `untraced`) are hatched; states in `muted` (default `idle`) use the muted tone.
187
- `Gantt` keeps its single-span `rows` input; `lanes` takes the same shape as `timeline`.
195
+ `timeline` refuses an open span without `now`, a span, tick or `now` outside a supplied `start`/`end` window and reversed times. States named in `hatch` (default `missing`, `untraced`) are hatched; states in `muted` (default `idle`) use the muted tone.
196
+ The window defaults to the extent of the data (spans, ticks and `now`), so a run that ended early is not drawn out to its deadline: a mark past the window, such as the deadline, is named at the edge it falls past with its distance (`Deadline +21.9 h →`), listed in the note, and not drawn; bands are clipped to the window.
197
+ Pass `start` (the run's start) to show time before the first span.
198
+
199
+ Lanes form a tree through `parent`: each lane is drawn under its parent with a connector, siblings and roots keep their input order, and `depth` defaults to the parent's depth plus one. A stated depth that disagrees with the parent, a parent that is not a lane and a parent chain that loops are refused.
200
+ `ticks` are recorded attempts, such as driver turns, at the time each ended: ok ticks rise above the bar and failed ticks drop below it in their outcome's colour (quota and rate-limit a light breach tone, oom, refused and other the breach tone, aborted ink), and the lane ends with its counts, `21✓ 250✗`. A lane without `ticks` shows no counts; an empty `ticks` shows `0✓ 0✗`. Ticks of one outcome within 2 px are drawn as one, with the count and the commonest `note` in its hover. An unknown outcome is refused.
201
+ A mark with a `lane` is drawn on that lane only (a steer to one agent, a wake of the root) instead of across every lane; a lane that is not in the figure is refused.
202
+ `detail` is the caller's facts about a lane (profile, model, status, attempts, cost, top error), one hover line each on the lane's label and every span, and a column of the lanes table.
203
+ `collapsed` folds the named lanes' subtrees: descendants are hidden, the label gains `+N`, and the folded lane's ticks and counts total its subtree. Passing `collapsed`, even empty, makes each parent lane's label a fold toggle (`data-toggle`, a button with `aria-expanded`); `TimelineView` keeps the fold state and wires the toggles to a click, Enter or Space, returning focus to the toggle.
204
+ `Gantt` keeps its single-span `rows` input; `lanes` takes the same shape as `timeline` and draws spans only.
188
205
 
189
206
  `Meter` with `segments` draws each recorded part of `total`; the remainder no segment accounts for is hatched and labelled `untraced` (rename it with `untraced`). A null segment is listed as `not recorded` and takes no width; a missing total hatches the whole track. Parts that sum past the total throw.
190
207
 
191
208
  `smallMultiples` draws one panel per arm. A `missing` panel is hatched and reads `no data: <why>`; an `end` stops the line at a mark of its kind. It refuses mixed units, values outside the shared domain, points after an end and a missing panel that also carries data. `shared: "x"` keeps one x domain and requires each panel's own y `domain`. The lede counts `n of m <noun>s have data`.
192
209
 
210
+ `TimelineView` is `FigureView` around `timeline(lanes, { ...options, collapsed })`; `defaultCollapsed` names the lanes folded on first render.
211
+
193
212
  `FigureView` embeds any static `Figure | Refusal` in React: the wide render in containers of 720 px or more, the phone render (or the table, when a figure has none) below that, and the data, reading and method in a closed disclosure. A refusal renders as its sentence. Import `figure.css` for its styles.
194
213
 
195
214
  `node scripts/capture-run-page.mjs [dir]` (after `npm run build`) captures each addition at 1440 px light and 390 px dark; the reviewed images are `docs/screenshots/run-page-*.png`.
package/dist/figure.css CHANGED
@@ -352,9 +352,27 @@
352
352
  .tgc-mark-deadline { stroke: var(--chart-ink); stroke-dasharray: 5 3; }
353
353
  .tgc-mark-died { stroke: var(--chart-breach, var(--status-error)); stroke-width: 2; }
354
354
  .tgc-mark-event { stroke: var(--chart-ink-muted); stroke-dasharray: 1 3; stroke-linecap: round; }
355
+ .tgc-mark-release { stroke: var(--chart-ink-muted); }
356
+ .tgc-mark-steer { stroke: var(--chart-accent); stroke-dasharray: 1 3; stroke-linecap: round; }
357
+ .tgc-mark-ruling { stroke: var(--chart-ink); stroke-dasharray: 6 2 1 2; }
358
+ .tgc-mark-wake { stroke: var(--chart-ink-muted); stroke-dasharray: 1 3; stroke-linecap: round; }
359
+ .tgc-mark-head { fill: var(--chart-ink-muted); stroke: var(--chart-canvas); stroke-width: 1; }
360
+ [data-mark="steer"] .tgc-mark-head { fill: var(--chart-accent); }
361
+ [data-mark="ruling"] .tgc-mark-head { fill: var(--chart-ink); }
362
+ /* A recorded verdict colours the rule and head: pass and fail take the status tones, unknown the muted tone. */
363
+ .tgc-verdict-pass .tgc-mark { stroke: var(--chart-pass, var(--status-running)); }
364
+ .tgc-verdict-pass .tgc-mark-head { fill: var(--chart-pass, var(--status-running)); }
365
+ .tgc-verdict-fail .tgc-mark { stroke: var(--chart-breach, var(--status-error)); }
366
+ .tgc-verdict-fail .tgc-mark-head { fill: var(--chart-breach, var(--status-error)); }
367
+ .tgc-verdict-unknown .tgc-mark { stroke: var(--chart-muted); }
368
+ .tgc-verdict-unknown .tgc-mark-head { fill: var(--chart-muted); }
355
369
  .tgc-mark-label { fill: var(--chart-ink); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }
370
+ .tgc-mark-edge { fill: var(--chart-ink-muted); }
356
371
  .tgc-band-after-pass { fill: color-mix(in srgb, var(--chart-pass, var(--status-running)) 12%, transparent); }
357
372
  .tgc-band-idle { fill: var(--chart-shade); }
373
+ /* A system-wide wall, such as a quota window: a breach wash across every lane, topped by a solid edge. */
374
+ .tgc-band-blocked { fill: color-mix(in srgb, var(--chart-breach, var(--status-error)) 9%, transparent); }
375
+ .tgc-band-wall { stroke: var(--chart-breach, var(--status-error)); stroke-width: 2; }
358
376
  /* Missing data: a neutral wash under hatch lines, never a zero-height mark. */
359
377
  .tgc-hatch-bg { fill: color-mix(in srgb, var(--chart-muted) 10%, transparent); }
360
378
  .tgc-hatch { fill: none; stroke: var(--chart-muted); stroke-width: 1; }
@@ -362,6 +380,24 @@
362
380
  .tgc-span-muted { fill: var(--chart-muted); }
363
381
  .tgc-span-open { stroke: var(--chart-ink); stroke-width: 1.5; stroke-dasharray: 3 2; }
364
382
  .tgc-now { stroke: var(--chart-ink); stroke-width: 1.5; }
383
+ /* Timeline trees and attempts. Ok ticks rise above the bar and failed ticks drop below it, so they read without colour. */
384
+ .tgc-tree { fill: none; stroke: var(--chart-muted); stroke-width: 1; }
385
+ .tgc-caret { fill: var(--chart-ink-muted); }
386
+ .tgc-lane-toggle { cursor: pointer; }
387
+ .tgc-lane-toggle:focus-visible { outline: none; }
388
+ .tgc-lane-hit { fill: transparent; stroke: transparent; stroke-width: 2; }
389
+ .tgc-lane-toggle:hover .tgc-lane-hit { fill: var(--chart-shade); }
390
+ .tgc-lane-toggle:focus-visible .tgc-lane-hit { stroke: var(--chart-accent); }
391
+ .tgc-tick { stroke-width: 1.5; }
392
+ .tgc-tick-ok { stroke: var(--chart-pass, var(--status-running)); }
393
+ .tgc-tick-quota, .tgc-tick-rate-limit { stroke: color-mix(in srgb, var(--chart-breach, var(--status-error)) 55%, var(--chart-canvas)); }
394
+ .tgc-tick-oom, .tgc-tick-refused, .tgc-tick-other { stroke: var(--chart-breach, var(--status-error)); }
395
+ .tgc-tick-aborted { stroke: var(--chart-ink); }
396
+ /* Lane labels and counts keep a canvas halo, so rules and ticks under them never strike through the text. */
397
+ .tgc-lane-label, .tgc-count { paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }
398
+ .tgc-count { fill: var(--chart-ink); }
399
+ .tgc-count-ok { fill: var(--chart-pass, var(--status-running)); }
400
+ .tgc-count-failed { fill: var(--chart-breach, var(--status-error)); }
365
401
 
366
402
  /* FigureView (React) picks the render by measured width; the wide plot fills its container. */
367
403
  .tgc-figure-view { max-width: none; margin: 0; }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as isRefusal, S as TierCounts, _ as MatrixCell, a as BandKind, b as Setup, c as MarkKind, d as CostBasis, f as Estimate, g as IntervalMethod, h as Interval, i as timeline, l as BreakdownInput, m as Figure, n as TimelineOptions, o as ChartBand, p as Exclusion, r as TimelineSpan, s as ChartMark, t as TimelineLane, u as ComparisonRow, v as RateRow, x as Tier, y as Refusal } from "./timeline-BAjzF-wL.js";
1
+ import { C as Setup, E as isRefusal, S as Refusal, T as TierCounts, _ as Figure, a as TimelineTick, b as MatrixCell, c as ChartBand, d as MarkVerdict, f as BreakdownInput, g as Exclusion, h as Estimate, i as TimelineSpan, l as ChartMark, m as CostBasis, n as TimelineLane, o as timeline, p as ComparisonRow, r as TimelineOptions, s as BandKind, t as TickOutcome, u as MarkKind, v as Interval, w as Tier, x as RateRow, y as IntervalMethod } from "./timeline-CCEJug7n.js";
2
2
  //#region src/block.d.ts
3
3
  interface RenderFigureOptions {
4
4
  /** Chart uses a closed data disclosure; plot keeps only the SVG and accessible data. */
@@ -260,4 +260,4 @@ interface SmallMultiplesOptions {
260
260
  }
261
261
  export declare function smallMultiples(panels: MultiplePanel[], o: SmallMultiplesOptions): Figure | Refusal;
262
262
  //#endregion
263
- export { type BandKind, type BreakdownInput, type BreakdownOptions, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type Estimate, type Exclusion, type Figure, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, type Observations, type PhaseWaterfallOptions, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type WaterfallPhase, isRefusal, timeline };
263
+ export { type BandKind, type BreakdownInput, type BreakdownOptions, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type Estimate, type Exclusion, type Figure, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, type Observations, type PhaseWaterfallOptions, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type WaterfallPhase, isRefusal, timeline };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as listJoin, D as wrap, E as textWidth, S as fitLines, T as textBlock, _ as svgRoot, a as eventsTable, b as capitalize, c as MARKER_SHAPES, f as hbar, g as rect, h as polyline, i as eventsGap, l as circle, m as markerPath, n as drawBands, o as isRefusal, p as line, r as drawMarks, s as table, t as EVENTS_READ, u as hatch, v as text, w as plural, x as esc, y as titled } from "./events-Cw6abvgR.js";
1
+ import { A as usdTick, B as rect, C as rateDataGap, D as tierSummary, E as tierAtLeast, G as esc, H as text, I as hbar, J as plural, K as fitLines, L as line, M as MARKER_SHAPES, N as circle, O as tierTotal, P as hatch, R as markerPath, S as pts, T as rateOrder, U as titled, V as svgRoot, W as capitalize, X as textWidth, Y as textBlock, Z as wrap, _ as intervalGap, a as drawMarks, b as pct, c as markLabelRows, d as METHOD_NAME, f as TIERS_BEST_FIRST, g as estimateNote, h as duration, i as drawBands, j as table, k as usd, m as drawable, n as EVENTS_READ, o as eventsGap, p as costText, q as listJoin, s as eventsTable, t as timeline, u as isRefusal, v as intervalName, w as rateIntervalGap, x as perPass, y as kn, z as polyline } from "./timeline-CCWFvLxg.js";
2
2
  //#region src/block.ts
3
3
  /**
4
4
  * The figure block: one markup for every figure on every surface.
@@ -526,138 +526,6 @@ function draw$2(rows, total, width, short) {
526
526
  return svgRoot(width, top + rows.length * rowH + 2, body);
527
527
  }
528
528
  //#endregion
529
- //#region src/format.ts
530
- const TIERS_BEST_FIRST = [
531
- "S",
532
- "A",
533
- "B",
534
- "C",
535
- "F"
536
- ];
537
- const LADDER = [
538
- "F",
539
- "C",
540
- "B",
541
- "A",
542
- "S"
543
- ];
544
- function tierAtLeast(tier, floor) {
545
- return LADDER.indexOf(tier) >= LADDER.indexOf(floor);
546
- }
547
- function pct(value) {
548
- return `${Math.round(value * 100)}%`;
549
- }
550
- /** A difference in points: "+25 pts". */
551
- function pts(value) {
552
- const n = Math.round(value * 100);
553
- return `${n > 0 ? "+" : n < 0 ? "−" : ""}${Math.abs(n)} pts`;
554
- }
555
- function usd(value) {
556
- if (value >= 100) return `$${Math.round(value).toLocaleString("en-US")}`;
557
- if (value >= 1) return `$${value.toFixed(2)}`;
558
- if (value >= .01) return `$${value.toFixed(3)}`;
559
- return `$${value.toFixed(4)}`;
560
- }
561
- /** Axis ticks: "$30", "$2.5", "$0.05". */
562
- function usdTick(value) {
563
- return `$${Number(value.toPrecision(2)).toLocaleString("en-US", { maximumFractionDigits: 4 })}`;
564
- }
565
- function duration(ms) {
566
- const s = ms / 1e3;
567
- if (s < 60) return `${Math.round(s)} s`;
568
- if (s < 3600) return `${(s / 60).toFixed(1)} min`;
569
- return `${(s / 3600).toFixed(1)} h`;
570
- }
571
- function kn(k, n) {
572
- return `${k}/${n}`;
573
- }
574
- const METHOD_NAME = {
575
- "task-cluster-bootstrap": "task-clustered bootstrap",
576
- "mcnemar-exact": "exact McNemar",
577
- wilson: "Wilson score"
578
- };
579
- /** "95% Wilson score" */
580
- function intervalName(interval) {
581
- return `${Math.round((interval.level ?? 0) * 100)}% ${interval.method ? METHOD_NAME[interval.method] : ""}`.trim();
582
- }
583
- /** Why an interval cannot be drawn, or null when it can. */
584
- function intervalGap(interval) {
585
- if (!interval) return "no interval";
586
- const missing = [interval.level === null ? "level" : "", interval.method === null ? "method" : ""].filter(Boolean);
587
- if (missing.length) return `the interval names no ${missing.join(" or ")}`;
588
- if (!(interval.method in METHOD_NAME)) return "the interval method is unknown";
589
- if (!Number.isFinite(interval.level) || interval.level <= 0 || interval.level >= 1) return "the interval level is outside 0–1";
590
- if (!Number.isFinite(interval.lower) || !Number.isFinite(interval.upper)) return "the interval bounds are not finite";
591
- if (!(interval.lower <= interval.upper)) return "the interval bounds are out of order";
592
- return null;
593
- }
594
- /** Whether a rate bar may be drawn for this estimate. */
595
- function drawable(estimate) {
596
- return estimate === "descriptive" || estimate === "bootstrap";
597
- }
598
- function estimateNote(estimate, units) {
599
- if (estimate === "none") return units === 1 ? "1 unit" : `${units} units`;
600
- if (estimate === "insufficient") return "too few units";
601
- return "";
602
- }
603
- /**
604
- * The positive cost per pass this figure can print or place, or null.
605
- * A zero value needs a separate verified-free representation; this field alone
606
- * cannot distinguish a free run from missing spend, so it prints "unknown".
607
- */
608
- function perPass(cost) {
609
- return cost.basis !== "unknown" && cost.perSolvedUsd !== null && Number.isFinite(cost.perSolvedUsd) && cost.perSolvedUsd > 0 ? cost.perSolvedUsd : null;
610
- }
611
- /** "$23.76", "$23.76 est.", "unknown", "no pass" */
612
- function costText(cost, solved) {
613
- if (solved === 0) return "no pass";
614
- const value = perPass(cost);
615
- if (value === null) return "unknown";
616
- return cost.basis === "receipts" ? usd(value) : `${usd(value)} est.`;
617
- }
618
- /** Wilson can describe an eligible rate; clustered bootstrap needs the producer's bootstrap class. */
619
- function rateIntervalGap(row) {
620
- if (!drawable(row.estimate)) return `${row.estimate} estimate`;
621
- if (!Number.isFinite(row.rate) || row.rate < 0 || row.rate > 1) return "rate is outside 0–1";
622
- const gap = intervalGap(row.interval);
623
- if (gap) return gap;
624
- if (row.interval.method === "mcnemar-exact") return "exact McNemar applies to paired comparisons, not rates";
625
- if (row.estimate === "descriptive" && row.interval.method !== "wilson") return `descriptive estimate does not support ${intervalName(row.interval)}`;
626
- if (row.interval.lower < 0 || row.interval.upper > 1) return "the rate interval is outside 0–1";
627
- if (row.rate < row.interval.lower - 1e-6 || row.rate > row.interval.upper + 1e-6) return "the rate falls outside its interval";
628
- return null;
629
- }
630
- /** Check the source arithmetic before using a rate in a finding or a mark. */
631
- function rateDataGap(row) {
632
- if (!Number.isInteger(row.attempts) || row.attempts < 0 || !Number.isInteger(row.solved) || row.solved < 0 || row.solved > row.attempts) return "passed and attempt counts are invalid";
633
- if (!Number.isFinite(row.rate) || row.rate < 0 || row.rate > 1) return "rate is outside 0–1";
634
- if (row.attempts > 0 && Math.abs(row.rate - row.solved / row.attempts) > 1e-6) return "rate disagrees with passed/attempts";
635
- if (row.attempts === 0 && row.rate !== 0) return "rate has no attempts";
636
- if (drawable(row.estimate) && row.attempts === 0) return "drawable estimate has no attempts";
637
- return null;
638
- }
639
- /**
640
- * The order rows are drawn in. Ranked rows keep their rank. Otherwise rows
641
- * with enough units to draw a rate come first, highest rate first; rows with
642
- * too few units follow, most units first, so a 1/1 row never heads the list.
643
- */
644
- function rateOrder(rows) {
645
- if (rows.length > 0 && rows.every((row) => row.rank !== null && row.estimate === "bootstrap")) return [...rows].sort((a, b) => a.rank - b.rank);
646
- const drawn = rows.filter((row) => drawable(row.estimate)).sort((a, b) => b.rate - a.rate);
647
- const counted = rows.filter((row) => !drawable(row.estimate)).sort((a, b) => b.attempts - a.attempts);
648
- return [...drawn, ...counted];
649
- }
650
- /** "2A 1F" */
651
- function tierSummary(tiers, unmeasured = 0, flagged = 0) {
652
- const parts = tiers ? TIERS_BEST_FIRST.filter((t) => tiers[t] > 0).map((t) => `${tiers[t]}${t}`) : [];
653
- if (flagged) parts.push(`${flagged} flagged`);
654
- if (unmeasured) parts.push(`${unmeasured}U`);
655
- return parts.join(" ");
656
- }
657
- function tierTotal(tiers) {
658
- return tiers ? LADDER.reduce((sum, t) => sum + tiers[t], 0) : 0;
659
- }
660
- //#endregion
661
529
  //#region src/scale.ts
662
530
  function linear(domain, range) {
663
531
  const [d0, d1] = domain;
@@ -1268,7 +1136,7 @@ function plot$1(costed, unplaced, drawLine, o) {
1268
1136
  //#endregion
1269
1137
  //#region src/css.generated.ts
1270
1138
  /** The figure block and mark styles (src/figure.css), for documents that inline their CSS. */
1271
- const figureCss = "/* @tangle-network/charts — figure block and mark styles.\n *\n * Every colour is a --chart-* token from @tangle-network/brand tokens.css, so a\n * figure follows the page theme. A standalone document without brand tokens\n * embeds `chartTokensCss` first. Fonts fall back to Inter and JetBrains Mono.\n *\n * Each figure carries a 720 px and a 358 px render. The swap follows the\n * figure's own width (a container query), so a narrow column shows the phone\n * render even on a wide screen. */\n\n.tgc-figure {\n margin: 32px 0 0;\n padding: 0;\n max-width: 720px;\n color: var(--chart-ink);\n font-family: var(--font-sans, Inter, ui-sans-serif, system-ui, sans-serif);\n}\n\n.tgc-finding {\n margin: 0 0 8px;\n font-size: 18px;\n line-height: 25px;\n font-weight: 650;\n color: var(--chart-ink);\n}\n\n.tgc-noteref {\n margin-left: 2px;\n font-size: 12px;\n font-weight: 400;\n color: var(--chart-ink-muted);\n text-decoration: none;\n font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace);\n}\n.tgc-noteref:hover,\n.tgc-noteref:focus-visible {\n color: var(--chart-accent);\n text-decoration: underline;\n}\n\n.tgc-lede {\n margin: 0 0 16px;\n font-size: 15px;\n line-height: 23px;\n color: var(--text-secondary, var(--chart-ink));\n}\n\n.tgc-plot {\n container-type: inline-size;\n}\n.tgc-wide svg,\n.tgc-narrow svg {\n display: block;\n overflow: visible;\n}\n.tgc-narrow {\n display: none;\n}\n.tgc-narrow svg {\n max-width: 100%;\n height: auto;\n}\n@container (max-width: 719.98px) {\n .tgc-wide {\n display: none;\n }\n .tgc-narrow {\n display: block;\n }\n}\n\n.tgc-read {\n margin: 8px 0 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--chart-ink-muted);\n}\n\n.tgc-data {\n margin: 8px 0 0;\n font-size: 12px;\n line-height: 16px;\n}\n.tgc-data > summary {\n cursor: pointer;\n color: var(--chart-ink-muted);\n width: fit-content;\n}\n.tgc-data > summary:hover {\n color: var(--chart-ink);\n}\n.tgc-data[open] > summary {\n margin-bottom: 8px;\n}\n\n.tgc-scroll {\n overflow-x: auto;\n}\n.tgc-scroll:focus-visible {\n outline: 2px solid var(--chart-accent);\n outline-offset: 2px;\n}\n\n.tgc-table {\n border-collapse: collapse;\n min-width: 100%;\n font-size: 12px;\n line-height: 16px;\n color: var(--chart-ink);\n /* A host page's table rules must not restyle the figure's table. */\n border: 0;\n border-radius: 0;\n background: none;\n}\n.tgc-table caption {\n text-align: left;\n padding: 0 0 8px;\n color: var(--chart-ink-muted);\n}\n.tgc-table th,\n.tgc-table td {\n padding: 6px 12px 6px 0;\n border-bottom: 1px solid var(--chart-rule);\n text-align: left;\n vertical-align: top;\n font-weight: 400;\n font-size: inherit;\n color: inherit;\n background: none;\n text-transform: none;\n letter-spacing: normal;\n}\n.tgc-table thead th {\n color: var(--chart-ink-muted);\n white-space: nowrap;\n}\n.tgc-table tbody th {\n position: sticky;\n left: 0;\n background: var(--chart-canvas, var(--depth-1, transparent));\n}\n.tgc-table .tgc-n {\n font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n.tgc-table thead .tgc-n {\n font-family: inherit;\n}\n.tgc-table tbody tr:hover {\n background: var(--chart-shade);\n}\n.tgc-narrow-table .tgc-table {\n display: block;\n overflow-x: auto;\n}\n\n.tgc-notes {\n margin: 16px 0 0;\n padding-left: 20px;\n font-size: 12px;\n line-height: 16px;\n color: var(--chart-ink-muted);\n}\n.tgc-notes li {\n margin: 0 0 8px;\n padding-left: 4px;\n}\n.tgc-notes li:target {\n color: var(--chart-ink);\n}\n.tgc-backlink {\n color: var(--chart-ink-muted);\n text-decoration: none;\n}\n\n.tgc-refusals {\n margin: 8px 0 0;\n padding-left: 20px;\n font-size: 14px;\n line-height: 22px;\n color: var(--chart-ink-muted);\n}\n\n/* Marks */\n.tgc-svg {\n font-family: var(--font-sans, Inter, ui-sans-serif, system-ui, sans-serif);\n}\n.tgc-svg .tgc-num {\n font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace);\n font-variant-numeric: tabular-nums;\n}\n.tgc-ink {\n fill: var(--chart-ink);\n}\n.tgc-ink-muted {\n fill: var(--chart-ink-muted);\n}\n.tgc-bar,\n.tgc-ci,\n.tgc-pt-front {\n fill: var(--chart-accent);\n}\n.tgc-gridline {\n stroke: var(--chart-grid);\n stroke-width: 1;\n}\n.tgc-rule {\n stroke: var(--chart-rule);\n stroke-width: 1;\n}\n.tgc-axis,\n.tgc-zero {\n stroke: var(--chart-muted);\n stroke-width: 1;\n}\n.tgc-whisker,\n.tgc-tick {\n stroke: var(--chart-ink);\n stroke-width: 2;\n stroke-linecap: round;\n}\n.tgc-threshold {\n stroke: var(--chart-ink);\n stroke-width: 1.5;\n stroke-dasharray: 3 3;\n}\n.tgc-front-line {\n fill: none;\n stroke: var(--chart-ink);\n stroke-width: 1.5;\n stroke-linejoin: round;\n}\n.tgc-pt,\n.tgc-open {\n fill: none;\n stroke: var(--chart-muted);\n stroke-width: 1.5;\n}\n.tgc-pt-est {\n fill: none;\n stroke: var(--chart-accent);\n stroke-width: 2;\n}\n.tgc-pt-ci,\n.tgc-strike {\n stroke: var(--chart-muted);\n stroke-width: 1.5;\n}\n.tgc-leader {\n stroke: var(--chart-muted);\n stroke-width: 1;\n}\n.tgc-ci-exact,\n.tgc-dot {\n fill: var(--chart-muted);\n}\n.tgc-dashed {\n stroke-dasharray: 2 2;\n}\n.tgc-tier-c {\n fill: var(--chart-tier-c);\n}\n.tgc-tier-b {\n fill: var(--chart-tier-b);\n}\n.tgc-tier-a {\n fill: var(--chart-tier-a);\n}\n.tgc-tier-s {\n fill: var(--chart-tier-s);\n}\n/* Neutral tint for counts: never the accent, capped at half strength so ink keeps 6:1. */\n.tgc-tint {\n fill: color-mix(in srgb, var(--chart-muted) calc(var(--s, 0) * 50%), transparent);\n}\n\n@media print {\n .tgc-plot {\n container-type: normal;\n }\n .tgc-wide {\n display: block;\n }\n .tgc-narrow {\n display: none;\n }\n .tgc-wide svg {\n width: 100%;\n height: auto;\n }\n .tgc-figure {\n break-inside: avoid;\n }\n}\n\n/* Chart-led surfaces keep supporting text in the native disclosure. */\n.tgc-figure-chart { max-width: none; margin: 0; }\n.tgc-figure-chart .tgc-wide svg { width: 100%; height: auto; }\n.tgc-wide:has(.tgc-vertical), .tgc-narrow:has(.tgc-vertical) { overflow-x: auto; }\n.tgc-vertical { width: 100%; }\n.tgc-narrow .tgc-vertical { max-width: none; }\n.tgc-column-hit { fill: transparent; stroke: transparent; stroke-width: 2; }\n.tgc-column:focus-visible { outline: none; }\n.tgc-column:focus-visible .tgc-column-hit { stroke: var(--chart-accent); }\n.tgc-vertical-bar { fill: var(--chart-accent); }\n/* Identity assets keep their approved colors on a white plate in either theme. */\n.tgc-logo-plate { fill: #fff; stroke: var(--chart-rule); }\n.tgc-column-tip { opacity: 0; pointer-events: none; }\n.tgc-tip-bg { fill: var(--chart-ink); }\n.tgc-tip-ink { fill: var(--chart-canvas); }\n\n.tgc-series-line { fill: none; stroke: var(--chart-accent); stroke-width: 2; stroke-linejoin: round; }\n.tgc-series-point, .tgc-series-key { fill: var(--chart-accent); stroke: var(--chart-canvas); stroke-width: 2; }\n\n/* Semantic states stay tied to Brand status tokens. No-data has its own neutral mark. */\n.tgc-heat-pass { fill: color-mix(in srgb, var(--chart-pass, var(--status-running)) 19%, var(--chart-canvas)); stroke: var(--chart-pass, var(--status-running)); stroke-width: .8; }\n.tgc-heat-breach { fill: color-mix(in srgb, var(--chart-breach, var(--status-error)) 17%, var(--chart-canvas)); stroke: var(--chart-breach, var(--status-error)); stroke-width: .8; }\n.tgc-heat-no-data { fill: var(--chart-grid); stroke: var(--chart-rule); stroke-width: .8; }\n.tgc-heat-label { fill: var(--chart-ink); }\n.tgc-heat-missing { fill: var(--chart-ink-muted); }\n.tgc-wide:has(.tgc-heat-pass), .tgc-wide:has(.tgc-heat-breach) { overflow-x: auto; }\n.tgc-range-pass { fill: var(--chart-pass, var(--status-running)); }\n.tgc-range-breach { fill: var(--chart-breach, var(--status-error)); }\n.tgc-range-dot-pass { fill: var(--chart-canvas); stroke: var(--chart-pass, var(--status-running)); stroke-width: 3; }\n.tgc-range-dot-breach { fill: var(--chart-canvas); stroke: var(--chart-breach, var(--status-error)); stroke-width: 3; }\n.tgc-waterfall-bar { fill: var(--chart-accent); }\n.tgc-series-hit { fill: transparent; stroke: transparent; }\n.tgc-series-observation:focus-visible { outline: none; }\n.tgc-series-observation:focus-visible .tgc-series-hit { stroke: var(--chart-ink); stroke-width: 2; }\n.tgc-series-tip { opacity: 0; pointer-events: none; }\n\n.tgc-scalar-table { min-width: 620px; }\n.tgc-scalar-table tbody th { min-width: 180px; max-width: 260px; }\n\n/* Plot presentation keeps the same complete data available to assistive technology. */\n.tgc-accessible-data { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }\n.tgc-figure-plot .tgc-wide svg { box-sizing: content-box; width: calc(100% - 48px); padding: 0 24px; }\n.tgc-figure-plot svg text { font-size: 14px; }\n.tgc-figure-plot svg text.tgc-num { font-size: 15px; }\n/* Larger tooltip text needs padding outside the original report backdrop. */\n.tgc-figure-plot .tgc-tip-bg { stroke: var(--chart-ink); stroke-width: 24px; }\n.tgc-figure-plot .tgc-phone-plot svg { min-width: var(--tgc-phone-width); }\n.tgc-figure-plot .tgc-wide, .tgc-figure-plot .tgc-narrow { overflow-x: auto; }\n\n.tgc-figure-plot .tgc-wide-fallback svg { min-width: var(--tgc-fallback-width); }\n.tgc-phone-plot:focus-visible, .tgc-wide-fallback:focus-visible { outline: 2px solid var(--chart-accent); outline-offset: 2px; }\n\n/* Recorded events and spans. Kinds differ by stroke pattern as well as token, so they read without colour. */\n.tgc-mark { stroke-width: 1.5; }\n.tgc-mark-pass { stroke: var(--chart-pass, var(--status-running)); }\n.tgc-mark-deadline { stroke: var(--chart-ink); stroke-dasharray: 5 3; }\n.tgc-mark-died { stroke: var(--chart-breach, var(--status-error)); stroke-width: 2; }\n.tgc-mark-event { stroke: var(--chart-ink-muted); stroke-dasharray: 1 3; stroke-linecap: round; }\n.tgc-mark-label { fill: var(--chart-ink); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n.tgc-band-after-pass { fill: color-mix(in srgb, var(--chart-pass, var(--status-running)) 12%, transparent); }\n.tgc-band-idle { fill: var(--chart-shade); }\n/* Missing data: a neutral wash under hatch lines, never a zero-height mark. */\n.tgc-hatch-bg { fill: color-mix(in srgb, var(--chart-muted) 10%, transparent); }\n.tgc-hatch { fill: none; stroke: var(--chart-muted); stroke-width: 1; }\n.tgc-span { fill: var(--chart-accent); }\n.tgc-span-muted { fill: var(--chart-muted); }\n.tgc-span-open { stroke: var(--chart-ink); stroke-width: 1.5; stroke-dasharray: 3 2; }\n.tgc-now { stroke: var(--chart-ink); stroke-width: 1.5; }\n\n/* FigureView (React) picks the render by measured width; the wide plot fills its container. */\n.tgc-figure-view { max-width: none; margin: 0; }\n.tgc-view-plot svg { display: block; overflow: visible; }\n.tgc-view-wide svg { width: 100%; height: auto; }\n.tgc-view-narrow svg { max-width: 100%; height: auto; }\n";
1139
+ const figureCss = "/* @tangle-network/charts — figure block and mark styles.\n *\n * Every colour is a --chart-* token from @tangle-network/brand tokens.css, so a\n * figure follows the page theme. A standalone document without brand tokens\n * embeds `chartTokensCss` first. Fonts fall back to Inter and JetBrains Mono.\n *\n * Each figure carries a 720 px and a 358 px render. The swap follows the\n * figure's own width (a container query), so a narrow column shows the phone\n * render even on a wide screen. */\n\n.tgc-figure {\n margin: 32px 0 0;\n padding: 0;\n max-width: 720px;\n color: var(--chart-ink);\n font-family: var(--font-sans, Inter, ui-sans-serif, system-ui, sans-serif);\n}\n\n.tgc-finding {\n margin: 0 0 8px;\n font-size: 18px;\n line-height: 25px;\n font-weight: 650;\n color: var(--chart-ink);\n}\n\n.tgc-noteref {\n margin-left: 2px;\n font-size: 12px;\n font-weight: 400;\n color: var(--chart-ink-muted);\n text-decoration: none;\n font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace);\n}\n.tgc-noteref:hover,\n.tgc-noteref:focus-visible {\n color: var(--chart-accent);\n text-decoration: underline;\n}\n\n.tgc-lede {\n margin: 0 0 16px;\n font-size: 15px;\n line-height: 23px;\n color: var(--text-secondary, var(--chart-ink));\n}\n\n.tgc-plot {\n container-type: inline-size;\n}\n.tgc-wide svg,\n.tgc-narrow svg {\n display: block;\n overflow: visible;\n}\n.tgc-narrow {\n display: none;\n}\n.tgc-narrow svg {\n max-width: 100%;\n height: auto;\n}\n@container (max-width: 719.98px) {\n .tgc-wide {\n display: none;\n }\n .tgc-narrow {\n display: block;\n }\n}\n\n.tgc-read {\n margin: 8px 0 0;\n font-size: 13px;\n line-height: 18px;\n color: var(--chart-ink-muted);\n}\n\n.tgc-data {\n margin: 8px 0 0;\n font-size: 12px;\n line-height: 16px;\n}\n.tgc-data > summary {\n cursor: pointer;\n color: var(--chart-ink-muted);\n width: fit-content;\n}\n.tgc-data > summary:hover {\n color: var(--chart-ink);\n}\n.tgc-data[open] > summary {\n margin-bottom: 8px;\n}\n\n.tgc-scroll {\n overflow-x: auto;\n}\n.tgc-scroll:focus-visible {\n outline: 2px solid var(--chart-accent);\n outline-offset: 2px;\n}\n\n.tgc-table {\n border-collapse: collapse;\n min-width: 100%;\n font-size: 12px;\n line-height: 16px;\n color: var(--chart-ink);\n /* A host page's table rules must not restyle the figure's table. */\n border: 0;\n border-radius: 0;\n background: none;\n}\n.tgc-table caption {\n text-align: left;\n padding: 0 0 8px;\n color: var(--chart-ink-muted);\n}\n.tgc-table th,\n.tgc-table td {\n padding: 6px 12px 6px 0;\n border-bottom: 1px solid var(--chart-rule);\n text-align: left;\n vertical-align: top;\n font-weight: 400;\n font-size: inherit;\n color: inherit;\n background: none;\n text-transform: none;\n letter-spacing: normal;\n}\n.tgc-table thead th {\n color: var(--chart-ink-muted);\n white-space: nowrap;\n}\n.tgc-table tbody th {\n position: sticky;\n left: 0;\n background: var(--chart-canvas, var(--depth-1, transparent));\n}\n.tgc-table .tgc-n {\n font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n.tgc-table thead .tgc-n {\n font-family: inherit;\n}\n.tgc-table tbody tr:hover {\n background: var(--chart-shade);\n}\n.tgc-narrow-table .tgc-table {\n display: block;\n overflow-x: auto;\n}\n\n.tgc-notes {\n margin: 16px 0 0;\n padding-left: 20px;\n font-size: 12px;\n line-height: 16px;\n color: var(--chart-ink-muted);\n}\n.tgc-notes li {\n margin: 0 0 8px;\n padding-left: 4px;\n}\n.tgc-notes li:target {\n color: var(--chart-ink);\n}\n.tgc-backlink {\n color: var(--chart-ink-muted);\n text-decoration: none;\n}\n\n.tgc-refusals {\n margin: 8px 0 0;\n padding-left: 20px;\n font-size: 14px;\n line-height: 22px;\n color: var(--chart-ink-muted);\n}\n\n/* Marks */\n.tgc-svg {\n font-family: var(--font-sans, Inter, ui-sans-serif, system-ui, sans-serif);\n}\n.tgc-svg .tgc-num {\n font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace);\n font-variant-numeric: tabular-nums;\n}\n.tgc-ink {\n fill: var(--chart-ink);\n}\n.tgc-ink-muted {\n fill: var(--chart-ink-muted);\n}\n.tgc-bar,\n.tgc-ci,\n.tgc-pt-front {\n fill: var(--chart-accent);\n}\n.tgc-gridline {\n stroke: var(--chart-grid);\n stroke-width: 1;\n}\n.tgc-rule {\n stroke: var(--chart-rule);\n stroke-width: 1;\n}\n.tgc-axis,\n.tgc-zero {\n stroke: var(--chart-muted);\n stroke-width: 1;\n}\n.tgc-whisker,\n.tgc-tick {\n stroke: var(--chart-ink);\n stroke-width: 2;\n stroke-linecap: round;\n}\n.tgc-threshold {\n stroke: var(--chart-ink);\n stroke-width: 1.5;\n stroke-dasharray: 3 3;\n}\n.tgc-front-line {\n fill: none;\n stroke: var(--chart-ink);\n stroke-width: 1.5;\n stroke-linejoin: round;\n}\n.tgc-pt,\n.tgc-open {\n fill: none;\n stroke: var(--chart-muted);\n stroke-width: 1.5;\n}\n.tgc-pt-est {\n fill: none;\n stroke: var(--chart-accent);\n stroke-width: 2;\n}\n.tgc-pt-ci,\n.tgc-strike {\n stroke: var(--chart-muted);\n stroke-width: 1.5;\n}\n.tgc-leader {\n stroke: var(--chart-muted);\n stroke-width: 1;\n}\n.tgc-ci-exact,\n.tgc-dot {\n fill: var(--chart-muted);\n}\n.tgc-dashed {\n stroke-dasharray: 2 2;\n}\n.tgc-tier-c {\n fill: var(--chart-tier-c);\n}\n.tgc-tier-b {\n fill: var(--chart-tier-b);\n}\n.tgc-tier-a {\n fill: var(--chart-tier-a);\n}\n.tgc-tier-s {\n fill: var(--chart-tier-s);\n}\n/* Neutral tint for counts: never the accent, capped at half strength so ink keeps 6:1. */\n.tgc-tint {\n fill: color-mix(in srgb, var(--chart-muted) calc(var(--s, 0) * 50%), transparent);\n}\n\n@media print {\n .tgc-plot {\n container-type: normal;\n }\n .tgc-wide {\n display: block;\n }\n .tgc-narrow {\n display: none;\n }\n .tgc-wide svg {\n width: 100%;\n height: auto;\n }\n .tgc-figure {\n break-inside: avoid;\n }\n}\n\n/* Chart-led surfaces keep supporting text in the native disclosure. */\n.tgc-figure-chart { max-width: none; margin: 0; }\n.tgc-figure-chart .tgc-wide svg { width: 100%; height: auto; }\n.tgc-wide:has(.tgc-vertical), .tgc-narrow:has(.tgc-vertical) { overflow-x: auto; }\n.tgc-vertical { width: 100%; }\n.tgc-narrow .tgc-vertical { max-width: none; }\n.tgc-column-hit { fill: transparent; stroke: transparent; stroke-width: 2; }\n.tgc-column:focus-visible { outline: none; }\n.tgc-column:focus-visible .tgc-column-hit { stroke: var(--chart-accent); }\n.tgc-vertical-bar { fill: var(--chart-accent); }\n/* Identity assets keep their approved colors on a white plate in either theme. */\n.tgc-logo-plate { fill: #fff; stroke: var(--chart-rule); }\n.tgc-column-tip { opacity: 0; pointer-events: none; }\n.tgc-tip-bg { fill: var(--chart-ink); }\n.tgc-tip-ink { fill: var(--chart-canvas); }\n\n.tgc-series-line { fill: none; stroke: var(--chart-accent); stroke-width: 2; stroke-linejoin: round; }\n.tgc-series-point, .tgc-series-key { fill: var(--chart-accent); stroke: var(--chart-canvas); stroke-width: 2; }\n\n/* Semantic states stay tied to Brand status tokens. No-data has its own neutral mark. */\n.tgc-heat-pass { fill: color-mix(in srgb, var(--chart-pass, var(--status-running)) 19%, var(--chart-canvas)); stroke: var(--chart-pass, var(--status-running)); stroke-width: .8; }\n.tgc-heat-breach { fill: color-mix(in srgb, var(--chart-breach, var(--status-error)) 17%, var(--chart-canvas)); stroke: var(--chart-breach, var(--status-error)); stroke-width: .8; }\n.tgc-heat-no-data { fill: var(--chart-grid); stroke: var(--chart-rule); stroke-width: .8; }\n.tgc-heat-label { fill: var(--chart-ink); }\n.tgc-heat-missing { fill: var(--chart-ink-muted); }\n.tgc-wide:has(.tgc-heat-pass), .tgc-wide:has(.tgc-heat-breach) { overflow-x: auto; }\n.tgc-range-pass { fill: var(--chart-pass, var(--status-running)); }\n.tgc-range-breach { fill: var(--chart-breach, var(--status-error)); }\n.tgc-range-dot-pass { fill: var(--chart-canvas); stroke: var(--chart-pass, var(--status-running)); stroke-width: 3; }\n.tgc-range-dot-breach { fill: var(--chart-canvas); stroke: var(--chart-breach, var(--status-error)); stroke-width: 3; }\n.tgc-waterfall-bar { fill: var(--chart-accent); }\n.tgc-series-hit { fill: transparent; stroke: transparent; }\n.tgc-series-observation:focus-visible { outline: none; }\n.tgc-series-observation:focus-visible .tgc-series-hit { stroke: var(--chart-ink); stroke-width: 2; }\n.tgc-series-tip { opacity: 0; pointer-events: none; }\n\n.tgc-scalar-table { min-width: 620px; }\n.tgc-scalar-table tbody th { min-width: 180px; max-width: 260px; }\n\n/* Plot presentation keeps the same complete data available to assistive technology. */\n.tgc-accessible-data { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }\n.tgc-figure-plot .tgc-wide svg { box-sizing: content-box; width: calc(100% - 48px); padding: 0 24px; }\n.tgc-figure-plot svg text { font-size: 14px; }\n.tgc-figure-plot svg text.tgc-num { font-size: 15px; }\n/* Larger tooltip text needs padding outside the original report backdrop. */\n.tgc-figure-plot .tgc-tip-bg { stroke: var(--chart-ink); stroke-width: 24px; }\n.tgc-figure-plot .tgc-phone-plot svg { min-width: var(--tgc-phone-width); }\n.tgc-figure-plot .tgc-wide, .tgc-figure-plot .tgc-narrow { overflow-x: auto; }\n\n.tgc-figure-plot .tgc-wide-fallback svg { min-width: var(--tgc-fallback-width); }\n.tgc-phone-plot:focus-visible, .tgc-wide-fallback:focus-visible { outline: 2px solid var(--chart-accent); outline-offset: 2px; }\n\n/* Recorded events and spans. Kinds differ by stroke pattern as well as token, so they read without colour. */\n.tgc-mark { stroke-width: 1.5; }\n.tgc-mark-pass { stroke: var(--chart-pass, var(--status-running)); }\n.tgc-mark-deadline { stroke: var(--chart-ink); stroke-dasharray: 5 3; }\n.tgc-mark-died { stroke: var(--chart-breach, var(--status-error)); stroke-width: 2; }\n.tgc-mark-event { stroke: var(--chart-ink-muted); stroke-dasharray: 1 3; stroke-linecap: round; }\n.tgc-mark-release { stroke: var(--chart-ink-muted); }\n.tgc-mark-steer { stroke: var(--chart-accent); stroke-dasharray: 1 3; stroke-linecap: round; }\n.tgc-mark-ruling { stroke: var(--chart-ink); stroke-dasharray: 6 2 1 2; }\n.tgc-mark-wake { stroke: var(--chart-ink-muted); stroke-dasharray: 1 3; stroke-linecap: round; }\n.tgc-mark-head { fill: var(--chart-ink-muted); stroke: var(--chart-canvas); stroke-width: 1; }\n[data-mark=\"steer\"] .tgc-mark-head { fill: var(--chart-accent); }\n[data-mark=\"ruling\"] .tgc-mark-head { fill: var(--chart-ink); }\n/* A recorded verdict colours the rule and head: pass and fail take the status tones, unknown the muted tone. */\n.tgc-verdict-pass .tgc-mark { stroke: var(--chart-pass, var(--status-running)); }\n.tgc-verdict-pass .tgc-mark-head { fill: var(--chart-pass, var(--status-running)); }\n.tgc-verdict-fail .tgc-mark { stroke: var(--chart-breach, var(--status-error)); }\n.tgc-verdict-fail .tgc-mark-head { fill: var(--chart-breach, var(--status-error)); }\n.tgc-verdict-unknown .tgc-mark { stroke: var(--chart-muted); }\n.tgc-verdict-unknown .tgc-mark-head { fill: var(--chart-muted); }\n.tgc-mark-label { fill: var(--chart-ink); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n.tgc-mark-edge { fill: var(--chart-ink-muted); }\n.tgc-band-after-pass { fill: color-mix(in srgb, var(--chart-pass, var(--status-running)) 12%, transparent); }\n.tgc-band-idle { fill: var(--chart-shade); }\n/* A system-wide wall, such as a quota window: a breach wash across every lane, topped by a solid edge. */\n.tgc-band-blocked { fill: color-mix(in srgb, var(--chart-breach, var(--status-error)) 9%, transparent); }\n.tgc-band-wall { stroke: var(--chart-breach, var(--status-error)); stroke-width: 2; }\n/* Missing data: a neutral wash under hatch lines, never a zero-height mark. */\n.tgc-hatch-bg { fill: color-mix(in srgb, var(--chart-muted) 10%, transparent); }\n.tgc-hatch { fill: none; stroke: var(--chart-muted); stroke-width: 1; }\n.tgc-span { fill: var(--chart-accent); }\n.tgc-span-muted { fill: var(--chart-muted); }\n.tgc-span-open { stroke: var(--chart-ink); stroke-width: 1.5; stroke-dasharray: 3 2; }\n.tgc-now { stroke: var(--chart-ink); stroke-width: 1.5; }\n/* Timeline trees and attempts. Ok ticks rise above the bar and failed ticks drop below it, so they read without colour. */\n.tgc-tree { fill: none; stroke: var(--chart-muted); stroke-width: 1; }\n.tgc-caret { fill: var(--chart-ink-muted); }\n.tgc-lane-toggle { cursor: pointer; }\n.tgc-lane-toggle:focus-visible { outline: none; }\n.tgc-lane-hit { fill: transparent; stroke: transparent; stroke-width: 2; }\n.tgc-lane-toggle:hover .tgc-lane-hit { fill: var(--chart-shade); }\n.tgc-lane-toggle:focus-visible .tgc-lane-hit { stroke: var(--chart-accent); }\n.tgc-tick { stroke-width: 1.5; }\n.tgc-tick-ok { stroke: var(--chart-pass, var(--status-running)); }\n.tgc-tick-quota, .tgc-tick-rate-limit { stroke: color-mix(in srgb, var(--chart-breach, var(--status-error)) 55%, var(--chart-canvas)); }\n.tgc-tick-oom, .tgc-tick-refused, .tgc-tick-other { stroke: var(--chart-breach, var(--status-error)); }\n.tgc-tick-aborted { stroke: var(--chart-ink); }\n/* Lane labels and counts keep a canvas halo, so rules and ticks under them never strike through the text. */\n.tgc-lane-label, .tgc-count { paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n.tgc-count { fill: var(--chart-ink); }\n.tgc-count-ok { fill: var(--chart-pass, var(--status-running)); }\n.tgc-count-failed { fill: var(--chart-breach, var(--status-error)); }\n\n/* FigureView (React) picks the render by measured width; the wide plot fills its container. */\n.tgc-figure-view { max-width: none; margin: 0; }\n.tgc-view-plot svg { display: block; overflow: visible; }\n.tgc-view-wide svg { width: 100%; height: auto; }\n.tgc-view-narrow svg { max-width: 100%; height: auto; }\n";
1272
1140
  /** --chart-* resolved per theme, for documents without brand tokens: light, dark by preference or [data-theme="dark"], light in print. */
1273
1141
  const chartTokensCss = ":root{--chart-ink:#2d2d2d;--chart-ink-muted:#686868;--chart-muted:#787878;--chart-grid:#f1f2f7;--chart-rule:#c7c6d6;--chart-shade:rgba(76, 63, 198, 0.06);--chart-accent:hsl(246 54% 51%);--chart-tier-c:#9b9ef0;--chart-tier-b:#7c7fe3;--chart-tier-a:#5b5bd6;--chart-tier-s:#272962;--chart-pass:#059669;--chart-breach:#ba1a1a;--chart-canvas:#ffffff;color-scheme:light}\n@media (prefers-color-scheme: dark){:root:not([data-theme=\"light\"]){--chart-ink:#e6e6e6;--chart-ink-muted:#a5a5a5;--chart-muted:#8e8e8e;--chart-grid:#221f33;--chart-rule:#2a293d;--chart-shade:rgba(165, 180, 252, 0.07);--chart-accent:#818CF8;--chart-tier-c:#4a4a95;--chart-tier-b:#5b5bd6;--chart-tier-a:#8b87f0;--chart-tier-s:#b1a9ff;--chart-pass:#10B981;--chart-breach:#FF4D6D;--chart-canvas:#0a0a14;color-scheme:dark}}\n:root[data-theme=\"dark\"]{--chart-ink:#e6e6e6;--chart-ink-muted:#a5a5a5;--chart-muted:#8e8e8e;--chart-grid:#221f33;--chart-rule:#2a293d;--chart-shade:rgba(165, 180, 252, 0.07);--chart-accent:#818CF8;--chart-tier-c:#4a4a95;--chart-tier-b:#5b5bd6;--chart-tier-a:#8b87f0;--chart-tier-s:#b1a9ff;--chart-pass:#10B981;--chart-breach:#FF4D6D;--chart-canvas:#0a0a14;color-scheme:dark}\n@media print{:root,:root:not([data-theme=\"light\"]),:root[data-theme=\"dark\"]{--chart-ink:#2d2d2d;--chart-ink-muted:#686868;--chart-muted:#787878;--chart-grid:#f1f2f7;--chart-rule:#c7c6d6;--chart-shade:rgba(76, 63, 198, 0.06);--chart-accent:hsl(246 54% 51%);--chart-tier-c:#9b9ef0;--chart-tier-b:#7c7fe3;--chart-tier-a:#5b5bd6;--chart-tier-s:#272962;--chart-pass:#059669;--chart-breach:#ba1a1a;--chart-canvas:#ffffff;color-scheme:light}}";
1274
1142
  //#endregion
@@ -2403,9 +2271,11 @@ function timeSeries(rows, o) {
2403
2271
  function plot(rows, o, width, xLabel, yLabel, facts) {
2404
2272
  const ticks = Array.from({ length: 5 }, (_, i) => o.y.domain[0] + i / 4 * (o.y.domain[1] - o.y.domain[0]));
2405
2273
  const left = axisGutter(ticks, yLabel);
2406
- const right = width - 24, top = 36, bottom = 260;
2274
+ const right = width - 24;
2407
2275
  const px = (x) => left + (x - o.x.domain[0]) / (o.x.domain[1] - o.x.domain[0]) * (right - left);
2408
- const py = (y) => bottom - (y - o.y.domain[0]) / (o.y.domain[1] - o.y.domain[0]) * 224;
2276
+ const labelRows = markLabelRows(o.marks ?? [], px, left, right);
2277
+ const top = 36 + (labelRows ? labelRows * 14 + 2 : 0), bottom = top + 224;
2278
+ const py = (y) => bottom - (y - o.y.domain[0]) / (o.y.domain[1] - o.y.domain[0]) * (bottom - top);
2409
2279
  let marks = text(left, 18, o.y.label, { cls: "tgc-ink-muted" });
2410
2280
  for (const [i, y] of ticks.entries()) marks += line(left, py(y), right, py(y), i === 0 ? "tgc-axis" : "tgc-gridline") + text(left - 8, py(y) + 4, yLabel(y), {
2411
2281
  anchor: "end",
@@ -2413,17 +2283,16 @@ function plot(rows, o, width, xLabel, yLabel, facts) {
2413
2283
  size: 12
2414
2284
  });
2415
2285
  const xs = [...new Set(rows.flatMap((row) => row.points.map((point) => point.x)))].sort((a, b) => a - b);
2416
- const tickStep = Math.max(1, Math.ceil(xs.length / (width < 720 ? 5 : 10)));
2417
- for (const [i, x] of xs.entries()) if (i % tickStep === 0 || i === xs.length - 1) marks += text(px(x), 280, xLabel(x), {
2286
+ for (const x of xTicks(xs, px, left, right, width, xLabel)) marks += text(px(x), bottom + 20, xLabel(x), {
2418
2287
  anchor: "middle",
2419
2288
  cls: "tgc-ink-muted tgc-num"
2420
2289
  });
2421
- marks += text((left + right) / 2, 303, o.x.label, {
2290
+ marks += text((left + right) / 2, bottom + 43, o.x.label, {
2422
2291
  anchor: "middle",
2423
2292
  cls: "tgc-ink-muted"
2424
2293
  });
2425
2294
  marks += drawBands(o.bands ?? [], px, top, bottom, xLabel);
2426
- let legendY = 332;
2295
+ let legendY = bottom + 72;
2427
2296
  let points = "", tips = "", rules = "";
2428
2297
  let pointId = 0;
2429
2298
  for (const [seriesIndex, row] of rows.entries()) {
@@ -2447,7 +2316,7 @@ function plot(rows, o, width, xLabel, yLabel, facts) {
2447
2316
  points += `<g class="tgc-series-observation" data-point="${key}" tabindex="0" role="img" aria-label="${esc(pointFacts.join(" · "))}"><title>${esc(pointFacts.join(" · "))}</title>${dot}</g>`;
2448
2317
  const tipWidth = Math.min(280, width - 24), tipX = Math.max(12, Math.min(width - tipWidth - 12, x - tipWidth / 2));
2449
2318
  const lines = pointFacts.flatMap((fact) => wrap(fact, tipWidth - 24, 12, "sans", 8).lines);
2450
- tips += `<g class="tgc-series-tip" data-series-tip="${key}" aria-hidden="true">${rect(tipX, top, tipWidth, lines.length * 17 + 24, "tgc-tip-bg", "rx=\"7\"")}${textBlock(lines, tipX + 12, 56, 17, "class=\"tgc-tip-ink\" font-size=\"12\"")}</g>`;
2319
+ tips += `<g class="tgc-series-tip" data-series-tip="${key}" aria-hidden="true">${rect(tipX, top, tipWidth, lines.length * 17 + 24, "tgc-tip-bg", "rx=\"7\"")}${textBlock(lines, tipX + 12, top + 20, 17, "class=\"tgc-tip-ink\" font-size=\"12\"")}</g>`;
2451
2320
  rules += `.tgc-trajectories:has([data-point="${key}"]:hover) [data-series-tip="${key}"],.tgc-trajectories:not(:has(.tgc-series-observation:hover)):has([data-point="${key}"]:focus-visible) [data-series-tip="${key}"]{opacity:1}`;
2452
2321
  }
2453
2322
  flush();
@@ -2455,162 +2324,39 @@ function plot(rows, o, width, xLabel, yLabel, facts) {
2455
2324
  marks += `<g style="stroke-dasharray:${dash}">${line(left, legendY - 4, left + 24, legendY - 4, "tgc-series-line")}</g>` + (rows.length > 1 ? `<path class="tgc-series-key" d="${markerPath(MARKER_SHAPES[seriesIndex % MARKER_SHAPES.length], left + 12, legendY - 4, 3.5)}"/>` : "") + textBlock(label.lines, left + 32, legendY, 16, "class=\"tgc-ink\" font-size=\"12\"");
2456
2325
  legendY += label.lines.length * 16 + 8;
2457
2326
  }
2458
- const events = drawMarks(o.marks ?? [], px, top, bottom, left, right, xLabel);
2327
+ const events = drawMarks(o.marks ?? [], px, top, bottom, left, right, xLabel, 22);
2459
2328
  return `<svg class="tgc-svg tgc-trajectories" xmlns="http://www.w3.org/2000/svg" width="${width}" height="${legendY + 12}" viewBox="0 0 ${width} ${legendY + 12}" role="group" aria-label="${esc(o.y.label)} by ${esc(o.x.label)}"><style>${rules}</style>${marks}${events}${points}${tips}</svg>`;
2460
2329
  }
2330
+ /**
2331
+ * X labels at recorded positions, spread evenly across the plot and measured so none overlap: at most as many as fit,
2332
+ * each snapped to the nearest recorded x, always keeping the first and the last that fit.
2333
+ */
2334
+ function xTicks(xs, px, left, right, width, label) {
2335
+ if (!xs.length) return [];
2336
+ const span = (x) => {
2337
+ const w = textWidth(label(x), 12, "mono");
2338
+ return [px(x) - w / 2, px(x) + w / 2];
2339
+ };
2340
+ const widest = Math.max(...xs.map((x) => textWidth(label(x), 12, "mono")));
2341
+ const slots = Math.max(1, Math.min(xs.length, Math.floor((right - left) / (widest + 12)) + 1));
2342
+ const anchors = slots === 1 ? [xs[0]] : Array.from({ length: slots }, (_, i) => left + (right - left) * i / (slots - 1)).map((target) => xs.reduce((best, x) => Math.abs(px(x) - target) < Math.abs(px(best) - target) ? x : best, xs[0]));
2343
+ const kept = [];
2344
+ for (const x of [...new Set(anchors)]) {
2345
+ const [a, b] = span(x);
2346
+ if (a < 2 || b > width - 2) continue;
2347
+ const previous = kept.at(-1);
2348
+ if (previous !== void 0 && a < span(previous)[1] + 10) {
2349
+ if (x !== xs.at(-1) || kept.length === 1) continue;
2350
+ kept.pop();
2351
+ }
2352
+ kept.push(x);
2353
+ }
2354
+ return kept;
2355
+ }
2461
2356
  function axisGutter(ticks, label) {
2462
2357
  return Math.max(64, ...ticks.map((value) => Math.ceil(textWidth(label(value), 12, "mono")) + 20));
2463
2358
  }
2464
2359
  //#endregion
2465
- //#region src/timeline.ts
2466
- /** Lanes of recorded spans on one time axis, with recorded marks; an open span runs to the supplied `now`. */
2467
- const utcClock = (ms) => new Date(ms).toISOString().slice(11, 16);
2468
- function timeline(lanes, o) {
2469
- const id = o.id ?? "timeline";
2470
- if (!lanes.length) return {
2471
- id,
2472
- refused: "No lanes recorded."
2473
- };
2474
- if (!o.measure?.trim()) return {
2475
- id,
2476
- refused: "A timeline needs a measure."
2477
- };
2478
- if (new Set(lanes.map((lane) => lane.id)).size !== lanes.length) return {
2479
- id,
2480
- refused: "Duplicate lane ids."
2481
- };
2482
- if (o.now !== void 0 && !Number.isFinite(o.now)) return {
2483
- id,
2484
- refused: "now must be a finite time."
2485
- };
2486
- const spans = lanes.flatMap((lane) => lane.spans.map((span) => ({
2487
- lane,
2488
- span
2489
- })));
2490
- for (const { lane, span } of spans) {
2491
- if (typeof span.state !== "string" || !span.state.trim()) return {
2492
- id,
2493
- refused: `${lane.label}: every span needs a state.`
2494
- };
2495
- if (!Number.isFinite(span.start) || span.end !== null && (!Number.isFinite(span.end) || span.end < span.start)) return {
2496
- id,
2497
- refused: `${lane.label}: span times must be finite and end at or after their start.`
2498
- };
2499
- if (span.end === null && o.now === void 0) return {
2500
- id,
2501
- refused: `${lane.label}: an open span needs now.`
2502
- };
2503
- if (span.end === null && o.now < span.start) return {
2504
- id,
2505
- refused: `${lane.label}: an open span starts after now.`
2506
- };
2507
- }
2508
- const shape = eventsGap(o.marks, o.bands);
2509
- if (shape) return {
2510
- id,
2511
- refused: shape
2512
- };
2513
- const marks = o.marks ?? [], bands = o.bands ?? [];
2514
- const ends = (span) => span.end ?? o.now;
2515
- const times = [
2516
- ...spans.flatMap(({ span }) => [span.start, ends(span)]),
2517
- ...marks.map((m) => m.at),
2518
- ...bands.flatMap((b) => [b.from, b.to]),
2519
- ...o.now === void 0 ? [] : [o.now]
2520
- ];
2521
- if (!times.length) return {
2522
- id,
2523
- refused: "No spans, marks or bands recorded."
2524
- };
2525
- const t0 = o.start ?? Math.min(...times), t1 = o.end ?? Math.max(...times);
2526
- if (!Number.isFinite(t0) || !Number.isFinite(t1) || t1 <= t0) return {
2527
- id,
2528
- refused: "The window needs two finite, increasing times."
2529
- };
2530
- if (times.some((t) => t < t0 || t > t1)) return {
2531
- id,
2532
- refused: "A span, mark, band or now falls outside the recorded window."
2533
- };
2534
- const fmt = o.formatTime ?? utcClock;
2535
- const hatched = new Set(o.hatch ?? ["missing", "untraced"]), muted = new Set(o.muted ?? ["idle"]);
2536
- const open = spans.filter(({ span }) => span.end === null).length;
2537
- const noData = spans.filter(({ span }) => hatched.has(span.state));
2538
- const noDataMs = noData.reduce((sum, { span }) => sum + ends(span) - span.start, 0);
2539
- const spanLabel = (lane, span) => `${lane.label}: ${span.state} ${fmt(span.start)} to ${span.end === null ? `now (${fmt(o.now)}, open)` : fmt(span.end)}, ${duration(ends(span) - span.start)}${span.note ? ` · ${span.note}` : ""}`;
2540
- const draw = (width) => {
2541
- const phone = width === 358, left = phone ? 12 : 150, right = width - 16;
2542
- const top = marks.length ? 8 + Math.min(3, marks.length) * 14 + 4 : 8;
2543
- const rowH = phone ? 38 : 24, bottom = top + lanes.length * rowH;
2544
- const px = (t) => left + (t - t0) / (t1 - t0) * (right - left);
2545
- let body = drawBands(bands, px, top, bottom, fmt);
2546
- for (let i = 0; i <= 4; i++) {
2547
- const x = left + (right - left) * i / 4;
2548
- body += line(x, top, x, bottom, "tgc-gridline") + text(x, bottom + 16, fmt(t0 + (t1 - t0) * i / 4), {
2549
- size: 11,
2550
- cls: "tgc-ink-muted tgc-num",
2551
- anchor: i === 0 ? "start" : i === 4 ? "end" : "middle"
2552
- });
2553
- }
2554
- lanes.forEach((lane, i) => {
2555
- const y = top + i * rowH;
2556
- const label = wrap(lane.label, phone ? width - 24 : left - 10, phone ? 11 : 12, "sans", 1);
2557
- body += titled(lane.label, text(phone ? 12 : 2, y + (phone ? 12 : 16), label.lines[0], { size: phone ? 11 : 12 }));
2558
- const barY = y + (phone ? 18 : 5), barH = phone ? 14 : 14;
2559
- for (const span of lane.spans) {
2560
- const x = px(span.start), w = Math.max(1, px(ends(span)) - x);
2561
- const mark = hatched.has(span.state) ? hatch(x, barY, w, barH, "tgc-span tgc-span-missing") : rect(x, barY, w, barH, `tgc-span${muted.has(span.state) ? " tgc-span-muted" : ""}${span.end === null ? " tgc-span-open" : ""}`, "rx=\"2\"");
2562
- body += titled(spanLabel(lane, span), `<g data-state="${span.state.replace(/[^a-z0-9-]/gi, "")}"${span.end === null ? " data-open=\"true\"" : ""}>${mark}</g>`);
2563
- }
2564
- });
2565
- if (o.now !== void 0) body += line(px(o.now), top, px(o.now), bottom, "tgc-now") + text(px(o.now), bottom + 30, `now ${fmt(o.now)}`, {
2566
- size: 11,
2567
- cls: "tgc-ink tgc-num",
2568
- anchor: px(o.now) > (left + right) / 2 ? "end" : "start"
2569
- });
2570
- body += drawMarks(marks, px, 4, bottom, left, right, fmt);
2571
- return svgRoot(width, bottom + (o.now === void 0 ? 22 : 36), body);
2572
- };
2573
- return {
2574
- id,
2575
- finding: `${capitalize(o.measure)} across ${lanes.length} ${plural(lanes.length, "lane")}, ${fmt(t0)} to ${fmt(t1)}.`,
2576
- lede: `${spans.length} recorded ${plural(spans.length, "span")}; ${open} open${o.now === void 0 ? "" : ` at ${fmt(o.now)}`}; ${noData.length} without data (${duration(noDataMs)}).`,
2577
- read: `Each lane shows its recorded spans on one time axis. Muted spans are ${[...muted].join(", ") || "none"}; hatched spans have no data; an open span runs to now.${marks.length + bands.length ? ` ${EVENTS_READ}` : ""}`,
2578
- svg: {
2579
- wide: draw(720),
2580
- narrow: draw(358)
2581
- },
2582
- table: table(`${capitalize(o.measure)} by lane`, [
2583
- { label: "Lane" },
2584
- { label: "State" },
2585
- {
2586
- label: "Start",
2587
- numeric: true
2588
- },
2589
- {
2590
- label: "End",
2591
- numeric: true
2592
- },
2593
- {
2594
- label: "Duration",
2595
- numeric: true
2596
- },
2597
- { label: "Note" }
2598
- ], spans.map(({ lane, span }) => [
2599
- lane.label,
2600
- span.state,
2601
- fmt(span.start),
2602
- span.end === null ? "open" : fmt(span.end),
2603
- duration(ends(span) - span.start),
2604
- span.note ?? ""
2605
- ])) + eventsTable(marks, bands, "time", fmt),
2606
- note: {
2607
- method: "Recorded span start and end times as supplied by the producer; open spans are drawn to the supplied now. No span is inferred, merged or reordered.",
2608
- n: `${spans.length} spans in ${lanes.length} lanes.`,
2609
- exclusions: noData.map(({ lane, span }) => `${lane.label}: ${span.state} ${fmt(span.start)} to ${span.end === null ? "now" : fmt(span.end)} has no data.`)
2610
- }
2611
- };
2612
- }
2613
- //#endregion
2614
2360
  //#region src/small-multiples.ts
2615
2361
  /**
2616
2362
  * One small trajectory per arm on shared axes. A panel without data is a
@@ -1,5 +1,9 @@
1
- import { a as BandKind, c as MarkKind, m as Figure, o as ChartBand, r as TimelineSpan, s as ChartMark, t as TimelineLane, y as Refusal } from "../timeline-BAjzF-wL.js";
1
+ import { S as Refusal, _ as Figure, a as TimelineTick, c as ChartBand, d as MarkVerdict, i as TimelineSpan, l as ChartMark, n as TimelineLane, r as TimelineOptions, s as BandKind, t as TickOutcome, u as MarkKind } from "../timeline-CCEJug7n.js";
2
2
  import { ReactElement, ReactNode } from "react";
3
+ //#region src/svg.d.ts
4
+ /** Point shapes: the second channel that keeps series distinct without colour. */
5
+ type MarkerShape = "circle" | "square" | "triangle" | "diamond";
6
+ //#endregion
3
7
  //#region src/react/sparkline.d.ts
4
8
  type SparklineDirection = "rising" | "falling" | "flat";
5
9
  type SparklineValues = readonly (number | null)[];
@@ -138,10 +142,6 @@ interface WaterfallHeaderProps extends WaterfallTimeAxisProps {
138
142
  }
139
143
  export declare function WaterfallHeader({ label, attribution, durationLabel, ...axis }: WaterfallHeaderProps): import("react").JSX.Element;
140
144
  //#endregion
141
- //#region src/svg.d.ts
142
- /** Point shapes: the second channel that keeps series distinct without colour. */
143
- type MarkerShape = "circle" | "square" | "triangle" | "diamond";
144
- //#endregion
145
145
  //#region src/react/live.d.ts
146
146
  export declare const LIVE_SERIES: readonly ['#8b7cf6', '#0ea5a0', '#c98500', '#e2508f', '#4a90e2'];
147
147
  export declare const formatCompact: (value: number | null | undefined) => string;
@@ -259,4 +259,14 @@ interface FigureViewProps {
259
259
  }
260
260
  export declare function FigureView({ figure, presentation }: FigureViewProps): import("react").JSX.Element;
261
261
  //#endregion
262
- export type { BandKind, ChartBand, ChartMark, Figure, FigureViewProps, LineSeries, MarkKind, MarkerShape, MeterSegment, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TimelineLane, TimelineSpan, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow };
262
+ //#region src/react/timeline-view.d.ts
263
+ interface TimelineViewProps {
264
+ lanes: TimelineLane[];
265
+ options: Omit<TimelineOptions, 'collapsed'>;
266
+ /** Lane ids folded on first render, such as every lead on a large team. */
267
+ defaultCollapsed?: string[];
268
+ presentation?: FigureViewProps['presentation'];
269
+ }
270
+ export declare function TimelineView({ lanes, options, defaultCollapsed, presentation }: TimelineViewProps): import("react").JSX.Element;
271
+ //#endregion
272
+ export type { BandKind, ChartBand, ChartMark, Figure, FigureViewProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow };