@tangle-network/charts 0.6.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 +26 -7
- package/dist/figure.css +36 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -291
- package/dist/react/index.d.ts +22 -6
- package/dist/react/index.js +60 -9
- package/dist/react/live.css +18 -1
- package/dist/{timeline-BAjzF-wL.d.ts → timeline-CCEJug7n.d.ts} +41 -6
- package/dist/timeline-CCWFvLxg.js +1130 -0
- package/package.json +1 -1
- package/dist/events-Cw6abvgR.js +0 -491
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
|
-
|
|
176
|
-
type
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { a as
|
|
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)[];
|
|
@@ -66,6 +70,12 @@ interface StackedBarBucket {
|
|
|
66
70
|
readonly total: number | null;
|
|
67
71
|
/** Stack order is input order. Charts neither groups nor sorts. */
|
|
68
72
|
readonly segments: readonly StackedBarSegment[];
|
|
73
|
+
/**
|
|
74
|
+
* Caller-owned text about this bucket that the chart cannot derive, such as
|
|
75
|
+
* request counts. Appended to the bar's name, the live readout and the
|
|
76
|
+
* pointer tooltip; never parsed.
|
|
77
|
+
*/
|
|
78
|
+
readonly description?: string;
|
|
69
79
|
}
|
|
70
80
|
interface StackedBarChartProps {
|
|
71
81
|
label: string;
|
|
@@ -132,10 +142,6 @@ interface WaterfallHeaderProps extends WaterfallTimeAxisProps {
|
|
|
132
142
|
}
|
|
133
143
|
export declare function WaterfallHeader({ label, attribution, durationLabel, ...axis }: WaterfallHeaderProps): import("react").JSX.Element;
|
|
134
144
|
//#endregion
|
|
135
|
-
//#region src/svg.d.ts
|
|
136
|
-
/** Point shapes: the second channel that keeps series distinct without colour. */
|
|
137
|
-
type MarkerShape = "circle" | "square" | "triangle" | "diamond";
|
|
138
|
-
//#endregion
|
|
139
145
|
//#region src/react/live.d.ts
|
|
140
146
|
export declare const LIVE_SERIES: readonly ['#8b7cf6', '#0ea5a0', '#c98500', '#e2508f', '#4a90e2'];
|
|
141
147
|
export declare const formatCompact: (value: number | null | undefined) => string;
|
|
@@ -253,4 +259,14 @@ interface FigureViewProps {
|
|
|
253
259
|
}
|
|
254
260
|
export declare function FigureView({ figure, presentation }: FigureViewProps): import("react").JSX.Element;
|
|
255
261
|
//#endregion
|
|
256
|
-
|
|
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 };
|