@tangle-network/charts 0.10.0 → 0.12.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 +7 -1
- package/dist/figure.css +26 -11
- package/dist/index.d.ts +45 -2
- package/dist/index.js +141 -6
- package/dist/react/index.d.ts +48 -2
- package/dist/react/index.js +434 -3
- package/dist/react/live.css +15 -0
- package/dist/{timeline-irTei5Fl.d.ts → timeline-BvVxPTGp.d.ts} +16 -4
- package/dist/{timeline-ApeJgFac.js → timeline-CVXBpZzt.js} +24 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,7 +244,7 @@ const climb = timeSeries(rows, {
|
|
|
244
244
|
|
|
245
245
|
The bar panels share the climb's columns: `smallMultiples(panels, { shared: "x", x: { label, domain, categories }, columns: 2, connect: false, markers })`, each panel with its own `unit`, `format`, `scale: "log"`, `domain` and `targets`. `docs/fixtures/magnetar-play.json` holds the Magnetar play's real 17c, 18b and 19b rows in these shapes (`docs/fixtures/export-magnetar-play.mjs` exports them on the Lab host), and `src/run-page-figures.test.ts` draws them.
|
|
246
246
|
|
|
247
|
-
Attempt outcomes and
|
|
247
|
+
Attempt outcomes, checkers and run endings have one palette in every view. Green, red and amber are status colours and mean one thing everywhere: an `ok` attempt and a solved run are green, a run that died is red, a stalled run and a rate-limited attempt are amber. `OUTCOME_ORDER` is the stack order (ok, quota, lost, rate-limit, oom, refused, aborted, other; an open attempt is hatched, never an outcome); `OUTCOME_COLORS[outcome]`, `checkerColor(index)` and `ENDING_COLORS[ending]` return the `var(--chart-outcome-*)`, `var(--chart-checker-*)` and `var(--chart-ending-*)` tokens `figure.css` defines as `light-dark()` pairs, so they follow the page's `color-scheme`. Aborted and other are two distinct greys and always carry a label. Checker hues (blue for the current checker, then magenta, cyan and violet) avoid the status colours, so a checker's colour never reads as a verdict; a failing verdict without a value is a neutral ring in the strip. `ENDING_ORDER` is solved, not solved, died, stalled, cancelled, live, never started. `node scripts/palette-check.mjs <dataviz validate_palette.js>` checks both palettes, adjacent pairs in order, on the light surface and on #161616.
|
|
248
248
|
|
|
249
249
|
| Play page figure | 1440 light | 390 dark |
|
|
250
250
|
|---|---|---|
|
|
@@ -302,6 +302,12 @@ Selection is controlled by bucket ID. Hover, focus, click, Enter, Space and touc
|
|
|
302
302
|
|
|
303
303
|
**Provenance and extraction boundary.** Sparkline comes from `tangle-network/agent-app`, `src/web-react/sparkline.tsx` and its tests at `a8947683a050394d13848bb7e4cbbe7cf08d208e`. Stacked SVG rendering and select-never-toggle regressions come from merged ADC [PR #8746](https://github.com/tangle-network/agent-dev-container/pull/8746), `products/platform/web/src/client/components/UsageChartSvg.tsx` and `UsageChart.test.tsx` at `a1c3958fd73a3894d4515f9dc476dae9ace30ce0`. Platform's aggregation, currency/product/category functions, legend composition and portal-tooltip positioning are **not** copied. The persistent readout and native pointer popover avoid a new React DOM peer. This is not a new universal usage-chart API, a zero-baseline replacement for SpendCard's sparkline, or a consumer migration; Platform and Agent App are not edited.
|
|
304
304
|
|
|
305
|
+
### Thousands of dots: `DotMap` and `unitDotLayout`
|
|
306
|
+
|
|
307
|
+
`unitDotLayout(groups, width, height)` places one dot per item in a rectangle per group, the rectangles a squarified treemap sized by count, so a fleet of 6,000 runs reads as its groups first and its items second with no item hidden in an aggregate. It returns the rectangles, every item's `x, y` (interleaved, groups in input order) and a radius per item from its group's cell.
|
|
308
|
+
|
|
309
|
+
`DotMap` draws those positions, or a semantic map's `x, y`, on one canvas: wheel or pinch zooms about the pointer, drag pans, a click selects, double-click or `0` fits, and `+ - ←→↑↓` work from the keyboard. Colours are palette indexes (`colors: Uint8Array` into `palette`, CSS colours or custom properties), so a lens change repaints without a new layout; `dimmed` greys filtered points, `radii` may vary per point (a dot sized by its run's hours inside its cell), and `labels` name clusters without overlapping. Hit tests use a uniform grid, one path is filled per colour, and the canvas records its last frame's draw time in `data-frame-ms`. The caller owns hover text (`describe`), selection, the legend and the details.
|
|
310
|
+
|
|
305
311
|
### React and package-boundary checks
|
|
306
312
|
|
|
307
313
|
```sh
|
package/dist/figure.css
CHANGED
|
@@ -395,7 +395,8 @@
|
|
|
395
395
|
.tgc-tick-rate-limit { stroke: var(--chart-outcome-rate-limit); }
|
|
396
396
|
.tgc-tick-oom { stroke: var(--chart-outcome-oom); }
|
|
397
397
|
.tgc-tick-refused { stroke: var(--chart-outcome-refused); }
|
|
398
|
-
.tgc-tick-aborted
|
|
398
|
+
.tgc-tick-aborted { stroke: var(--chart-outcome-neutral); }
|
|
399
|
+
.tgc-tick-other { stroke: var(--chart-outcome-other); }
|
|
399
400
|
/* Lane labels and counts keep a canvas halo, so rules and ticks under them never strike through the text. */
|
|
400
401
|
.tgc-lane-label, .tgc-count { paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }
|
|
401
402
|
.tgc-count { fill: var(--chart-ink); }
|
|
@@ -409,24 +410,36 @@
|
|
|
409
410
|
.tgc-view-narrow svg { max-width: 100%; height: auto; }
|
|
410
411
|
|
|
411
412
|
/*
|
|
412
|
-
* One fixed palette
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* as adjacent
|
|
416
|
-
*
|
|
413
|
+
* One fixed palette in every view (src/outcomes.ts names them), each a light-dark() pair picked by the page's
|
|
414
|
+
* color-scheme. Green, red and amber are status colours and mean one thing everywhere: green is a turn or run that
|
|
415
|
+
* worked (an `ok` attempt, a solved run), red a run that died, amber a run stalled or a turn rate-limited. Outcomes
|
|
416
|
+
* pass the dataviz colour-vision checks as adjacent pairs in stack order (ok, quota, lost, rate-limit, oom, refused) on
|
|
417
|
+
* the light surface and on #161616; `aborted` and `other` are two distinct greys. Checkers take hues no status uses
|
|
418
|
+
* (blue for the current checker, then magenta, cyan and violet for older ones) and pass as adjacent series; their
|
|
419
|
+
* marks also carry a vN label. Run endings reuse those colours. `scripts/palette-check.mjs` re-runs the validator. A
|
|
420
|
+
* page may override any of these properties; :where() keeps them at zero specificity.
|
|
417
421
|
*/
|
|
418
422
|
:where(:root) {
|
|
419
|
-
--chart-outcome-ok: light-dark(#
|
|
423
|
+
--chart-outcome-ok: light-dark(#1baf7a, #199e70);
|
|
420
424
|
--chart-outcome-quota: light-dark(#eb6834, #d95926);
|
|
421
|
-
--chart-outcome-lost: light-dark(#
|
|
425
|
+
--chart-outcome-lost: light-dark(#0f9fb8, #1a9cb3);
|
|
422
426
|
--chart-outcome-rate-limit: light-dark(#eda100, #c98500);
|
|
423
427
|
--chart-outcome-oom: light-dark(#e87ba4, #d55181);
|
|
424
428
|
--chart-outcome-refused: light-dark(#6250d6, #9085e9);
|
|
425
429
|
--chart-outcome-neutral: #898781;
|
|
430
|
+
--chart-outcome-other: #b5b3ac;
|
|
426
431
|
--chart-checker-0: light-dark(#2a78d6, #3987e5);
|
|
427
|
-
--chart-checker-1: light-dark(#
|
|
428
|
-
--chart-checker-2: light-dark(#
|
|
429
|
-
--chart-checker-3: light-dark(#
|
|
432
|
+
--chart-checker-1: light-dark(#b8478f, #c2408f);
|
|
433
|
+
--chart-checker-2: light-dark(#0f9fb8, #1a9cb3);
|
|
434
|
+
--chart-checker-3: light-dark(#6250d6, #8c7ce6);
|
|
435
|
+
/* How a run ended: solved green, died red, stalled amber, live blue; not solved and cancelled are neutral. */
|
|
436
|
+
--chart-ending-solved: var(--chart-outcome-ok);
|
|
437
|
+
--chart-ending-died: light-dark(#d03b3b, #e05252);
|
|
438
|
+
--chart-ending-stalled: var(--chart-outcome-rate-limit);
|
|
439
|
+
--chart-ending-live: var(--chart-checker-0);
|
|
440
|
+
--chart-ending-not-solved: light-dark(#6b7a90, #8494ab);
|
|
441
|
+
--chart-ending-cancelled: var(--chart-outcome-neutral);
|
|
442
|
+
--chart-ending-never-started: color-mix(in srgb, var(--chart-outcome-other) 45%, transparent);
|
|
430
443
|
}
|
|
431
444
|
/* A series drawn in a checker's hue; past the fourth hue it is muted and named by its label. */
|
|
432
445
|
.tgc-checker-0 { --tgc-series: var(--chart-checker-0); }
|
|
@@ -438,6 +451,8 @@
|
|
|
438
451
|
.tgc-series-point.tgc-point-outline { fill: var(--chart-canvas); stroke: var(--tgc-series, var(--chart-accent)); stroke-width: 1.75; }
|
|
439
452
|
.tgc-series-point.tgc-point-undecided { fill: var(--chart-canvas); stroke: var(--tgc-series, var(--chart-accent)); stroke-width: 1.5; stroke-dasharray: 2 1.5; }
|
|
440
453
|
.tgc-strip-hatch .tgc-hatch { stroke: var(--tgc-series, var(--chart-muted)); }
|
|
454
|
+
/* A failing verdict without a value is a neutral ring: in its checker's hue it read as that checker passing. */
|
|
455
|
+
.tgc-series-point.tgc-point-outline.tgc-strip-fail { stroke: var(--chart-ink-muted); }
|
|
441
456
|
.tgc-strip-edge { fill: none; stroke: var(--tgc-series, var(--chart-muted)); stroke-width: 1.25; }
|
|
442
457
|
.tgc-strip-observation:focus-visible { outline: none; }
|
|
443
458
|
.tgc-strip-observation:focus-visible .tgc-series-hit { stroke: var(--chart-ink); stroke-width: 2; }
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as Exclusion, B as isRefusal, C as YScale, D as ComparisonRow, E as BreakdownInput, F as RateRow, I as Refusal, L as Setup, M as Interval, N as IntervalMethod, O as CostBasis, P as MatrixCell, R as Tier, S as XCategory, T as YTick, _ as ChartBand, a as timeline, b as MarkVerdict, c as ENDING_ORDER, d as RunEnding, f as TickOutcome, g as BandKind, h as PointGlyph, i as TimelineTick, j as Figure, k as Estimate, l as OUTCOME_COLORS, m as PointFields, n as TimelineOptions, o as CHECKER_HUES, p as checkerColor, r as TimelineSpan, s as ENDING_COLORS, t as TimelineLane, u as OUTCOME_ORDER, v as ChartMark, w as YTarget, x as MarkerShape, y as MarkKind, z as TierCounts } from "./timeline-BvVxPTGp.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. */
|
|
@@ -301,4 +301,47 @@ interface SmallMultiplesOptions {
|
|
|
301
301
|
}
|
|
302
302
|
export declare function smallMultiples(panels: MultiplePanel[], o: SmallMultiplesOptions): Figure | Refusal;
|
|
303
303
|
//#endregion
|
|
304
|
-
|
|
304
|
+
//#region src/unit-dots.d.ts
|
|
305
|
+
/**
|
|
306
|
+
* Unit dots: one dot per item, packed in a rectangle per group, the rectangles laid out as a squarified treemap sized
|
|
307
|
+
* by count. A fleet of 6,000 runs reads as its groups first and its items second, with no item hidden in an
|
|
308
|
+
* aggregate. Positions are in layout units (the `width` and `height` given); a canvas scales them to its pixels.
|
|
309
|
+
*/
|
|
310
|
+
interface DotGroup {
|
|
311
|
+
id: string;
|
|
312
|
+
label: string;
|
|
313
|
+
/** Items in this group; their positions come out in group order, then item order within the group. */
|
|
314
|
+
count: number;
|
|
315
|
+
}
|
|
316
|
+
interface DotGroupRect {
|
|
317
|
+
id: string;
|
|
318
|
+
label: string;
|
|
319
|
+
count: number;
|
|
320
|
+
x0: number;
|
|
321
|
+
y0: number;
|
|
322
|
+
x1: number;
|
|
323
|
+
y1: number;
|
|
324
|
+
}
|
|
325
|
+
interface UnitDotLayout {
|
|
326
|
+
rects: DotGroupRect[];
|
|
327
|
+
/** x, y of every item, interleaved, groups in input order. */
|
|
328
|
+
positions: Float32Array;
|
|
329
|
+
/** Each item's dot radius in layout units: one size per group, from its cell. */
|
|
330
|
+
radii: Float32Array;
|
|
331
|
+
}
|
|
332
|
+
interface UnitDotOptions {
|
|
333
|
+
/** Space between rectangles. Default 6. */
|
|
334
|
+
gap?: number;
|
|
335
|
+
/** Space above each rectangle's dots for its label. Default 16. */
|
|
336
|
+
header?: number;
|
|
337
|
+
/** The largest dot radius, so a group of three is not three giant discs. Default 6. */
|
|
338
|
+
maxRadius?: number;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Lays out `groups` in a `width` × `height` box: a treemap rectangle per group with a header for its label, and its
|
|
342
|
+
* items on a grid of square cells inside it, filled row by row. Groups keep their input order in `positions`; the
|
|
343
|
+
* treemap places larger groups first.
|
|
344
|
+
*/
|
|
345
|
+
export declare function unitDotLayout(groups: DotGroup[], width: number, height: number, o?: UnitDotOptions): UnitDotLayout;
|
|
346
|
+
//#endregion
|
|
347
|
+
export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, ENDING_COLORS, ENDING_ORDER, 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, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type RunEnding, 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 UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as titled, A as pct, B as usdTick, C as costText, D as intervalGap, E as estimateNote, F as rateOrder, H as MARKER_SHAPES, I as tierAtLeast, J as markerPath, K as hbar, L as tierSummary, M as pts, N as rateDataGap, O as intervalName, P as rateIntervalGap, Q as text, R as tierTotal, S as TIERS_BEST_FIRST, T as duration, U as circle, V as table, W as hatch, X as rect, Y as polyline, Z as svgRoot, _ as groupRowHeight, a as OUTCOME_COLORS, at as plural, b as isRefusal, c as checkerColor, ct as wrap, d as MARK_VERDICTS, et as wideWidth, f as drawBands, g as eventsTable, h as eventsGap, i as ENDING_ORDER, it as listJoin, j as perPass, k as kn, l as EVENTS_READ, m as drawMarks, n as CHECKER_HUES, nt as esc, o as OUTCOME_ORDER, ot as textBlock, p as drawGroups, q as line, r as ENDING_COLORS, rt as fitLines, s as checkerClass, st as textWidth, t as timeline, tt as capitalize, v as markLabelRows, w as drawable, x as METHOD_NAME, z as usd } from "./timeline-CVXBpZzt.js";
|
|
2
2
|
//#region src/block.ts
|
|
3
3
|
/**
|
|
4
4
|
* The figure block: one markup for every figure on every surface.
|
|
@@ -1136,7 +1136,7 @@ function plot$1(costed, unplaced, drawLine, o) {
|
|
|
1136
1136
|
//#endregion
|
|
1137
1137
|
//#region src/css.generated.ts
|
|
1138
1138
|
/** The figure block and mark styles (src/figure.css), for documents that inline their CSS. */
|
|
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(--tgc-series, var(--chart-accent)); stroke-width: 2; stroke-linejoin: round; }\n.tgc-series-point, .tgc-series-key { fill: var(--tgc-series, 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-outcome-ok); }\n.tgc-tick-quota { stroke: var(--chart-outcome-quota); }\n.tgc-tick-lost { stroke: var(--chart-outcome-lost); }\n.tgc-tick-rate-limit { stroke: var(--chart-outcome-rate-limit); }\n.tgc-tick-oom { stroke: var(--chart-outcome-oom); }\n.tgc-tick-refused { stroke: var(--chart-outcome-refused); }\n.tgc-tick-aborted, .tgc-tick-other { stroke: var(--chart-outcome-neutral); }\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\n/*\n * One fixed palette for attempt outcomes and check versions in every view (src/outcomes.ts names them). Each is a\n * light-dark() pair picked by the page's color-scheme. Outcomes pass the dataviz colour-vision checks as adjacent\n * pairs in stack order (ok, quota, lost, rate-limit, oom, refused) on the light surface and on #161616; checkers pass\n * as adjacent series (current, then older). `scripts/palette-check.mjs` re-runs the validator. A page may override\n * any of these properties; :where() keeps them at zero specificity.\n */\n:where(:root) {\n --chart-outcome-ok: light-dark(#2a78d6, #3987e5);\n --chart-outcome-quota: light-dark(#eb6834, #d95926);\n --chart-outcome-lost: light-dark(#1baf7a, #199e70);\n --chart-outcome-rate-limit: light-dark(#eda100, #c98500);\n --chart-outcome-oom: light-dark(#e87ba4, #d55181);\n --chart-outcome-refused: light-dark(#6250d6, #9085e9);\n --chart-outcome-neutral: #898781;\n --chart-checker-0: light-dark(#2a78d6, #3987e5);\n --chart-checker-1: light-dark(#1baf7a, #199e70);\n --chart-checker-2: light-dark(#6250d6, #9085e9);\n --chart-checker-3: light-dark(#eda100, #c98500);\n}\n/* A series drawn in a checker's hue; past the fourth hue it is muted and named by its label. */\n.tgc-checker-0 { --tgc-series: var(--chart-checker-0); }\n.tgc-checker-1 { --tgc-series: var(--chart-checker-1); }\n.tgc-checker-2 { --tgc-series: var(--chart-checker-2); }\n.tgc-checker-3 { --tgc-series: var(--chart-checker-3); }\n.tgc-checker-other { --tgc-series: var(--chart-muted); }\n/* Recorded verdicts: a pass is filled, a fail or an incomparable value is an outline, an undecided one dashed. */\n.tgc-series-point.tgc-point-outline { fill: var(--chart-canvas); stroke: var(--tgc-series, var(--chart-accent)); stroke-width: 1.75; }\n.tgc-series-point.tgc-point-undecided { fill: var(--chart-canvas); stroke: var(--tgc-series, var(--chart-accent)); stroke-width: 1.5; stroke-dasharray: 2 1.5; }\n.tgc-strip-hatch .tgc-hatch { stroke: var(--tgc-series, var(--chart-muted)); }\n.tgc-strip-edge { fill: none; stroke: var(--tgc-series, var(--chart-muted)); stroke-width: 1.25; }\n.tgc-strip-observation:focus-visible { outline: none; }\n.tgc-strip-observation:focus-visible .tgc-series-hit { stroke: var(--chart-ink); stroke-width: 2; }\n.tgc-point-badge { paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace); }\n.tgc-point-label { fill: var(--chart-ink-muted); }\n.tgc-glyph-note { fill: var(--chart-ink); font-weight: 600; }\n/* Status colours appear only on these glyphs. */\n.tgc-glyph-check { fill: var(--chart-pass, var(--status-running)); font-weight: 600; }\n.tgc-glyph-cross { fill: var(--chart-breach, var(--status-error)); font-weight: 600; }\n/* Recorded targets sit under the data: a target is a soft band between solid edges, a stretch a dashed outline. */\n.tgc-target-target { fill: color-mix(in srgb, var(--chart-ink) 7%, transparent); }\n.tgc-target-stretch { fill: none; }\n.tgc-target-target-edge, .tgc-target-target.tgc-target-line { stroke: var(--chart-ink-muted); stroke-width: 1; }\n.tgc-target-stretch-edge, .tgc-target-stretch.tgc-target-line { stroke: var(--chart-ink-muted); stroke-width: 1; stroke-dasharray: 4 3; }\n.tgc-target-label { fill: var(--chart-ink-muted); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n/* Group headers: a labelled bracket above the plot and a light rule at each inner edge. */\n.tgc-group-rule { stroke: var(--chart-ink-muted); stroke-width: 1.5; }\n.tgc-group-edge { stroke: var(--chart-rule); stroke-width: 1; }\n.tgc-group-label { fill: var(--chart-ink); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n/* The verdict key: glyphs in the neutral ink, so they describe a mark's form, not a series. */\n.tgc-key-mark { stroke-width: 1.5; }\n.tgc-key-filled { fill: var(--chart-ink-muted); stroke: none; }\n.tgc-key-outline { fill: none; stroke: var(--chart-ink-muted); }\n.tgc-key-dashed { fill: none; stroke: var(--chart-ink-muted); stroke-dasharray: 2 1.5; }\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(--tgc-series, var(--chart-accent)); stroke-width: 2; stroke-linejoin: round; }\n.tgc-series-point, .tgc-series-key { fill: var(--tgc-series, 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-outcome-ok); }\n.tgc-tick-quota { stroke: var(--chart-outcome-quota); }\n.tgc-tick-lost { stroke: var(--chart-outcome-lost); }\n.tgc-tick-rate-limit { stroke: var(--chart-outcome-rate-limit); }\n.tgc-tick-oom { stroke: var(--chart-outcome-oom); }\n.tgc-tick-refused { stroke: var(--chart-outcome-refused); }\n.tgc-tick-aborted { stroke: var(--chart-outcome-neutral); }\n.tgc-tick-other { stroke: var(--chart-outcome-other); }\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\n/*\n * One fixed palette in every view (src/outcomes.ts names them), each a light-dark() pair picked by the page's\n * color-scheme. Green, red and amber are status colours and mean one thing everywhere: green is a turn or run that\n * worked (an `ok` attempt, a solved run), red a run that died, amber a run stalled or a turn rate-limited. Outcomes\n * pass the dataviz colour-vision checks as adjacent pairs in stack order (ok, quota, lost, rate-limit, oom, refused) on\n * the light surface and on #161616; `aborted` and `other` are two distinct greys. Checkers take hues no status uses\n * (blue for the current checker, then magenta, cyan and violet for older ones) and pass as adjacent series; their\n * marks also carry a vN label. Run endings reuse those colours. `scripts/palette-check.mjs` re-runs the validator. A\n * page may override any of these properties; :where() keeps them at zero specificity.\n */\n:where(:root) {\n --chart-outcome-ok: light-dark(#1baf7a, #199e70);\n --chart-outcome-quota: light-dark(#eb6834, #d95926);\n --chart-outcome-lost: light-dark(#0f9fb8, #1a9cb3);\n --chart-outcome-rate-limit: light-dark(#eda100, #c98500);\n --chart-outcome-oom: light-dark(#e87ba4, #d55181);\n --chart-outcome-refused: light-dark(#6250d6, #9085e9);\n --chart-outcome-neutral: #898781;\n --chart-outcome-other: #b5b3ac;\n --chart-checker-0: light-dark(#2a78d6, #3987e5);\n --chart-checker-1: light-dark(#b8478f, #c2408f);\n --chart-checker-2: light-dark(#0f9fb8, #1a9cb3);\n --chart-checker-3: light-dark(#6250d6, #8c7ce6);\n /* How a run ended: solved green, died red, stalled amber, live blue; not solved and cancelled are neutral. */\n --chart-ending-solved: var(--chart-outcome-ok);\n --chart-ending-died: light-dark(#d03b3b, #e05252);\n --chart-ending-stalled: var(--chart-outcome-rate-limit);\n --chart-ending-live: var(--chart-checker-0);\n --chart-ending-not-solved: light-dark(#6b7a90, #8494ab);\n --chart-ending-cancelled: var(--chart-outcome-neutral);\n --chart-ending-never-started: color-mix(in srgb, var(--chart-outcome-other) 45%, transparent);\n}\n/* A series drawn in a checker's hue; past the fourth hue it is muted and named by its label. */\n.tgc-checker-0 { --tgc-series: var(--chart-checker-0); }\n.tgc-checker-1 { --tgc-series: var(--chart-checker-1); }\n.tgc-checker-2 { --tgc-series: var(--chart-checker-2); }\n.tgc-checker-3 { --tgc-series: var(--chart-checker-3); }\n.tgc-checker-other { --tgc-series: var(--chart-muted); }\n/* Recorded verdicts: a pass is filled, a fail or an incomparable value is an outline, an undecided one dashed. */\n.tgc-series-point.tgc-point-outline { fill: var(--chart-canvas); stroke: var(--tgc-series, var(--chart-accent)); stroke-width: 1.75; }\n.tgc-series-point.tgc-point-undecided { fill: var(--chart-canvas); stroke: var(--tgc-series, var(--chart-accent)); stroke-width: 1.5; stroke-dasharray: 2 1.5; }\n.tgc-strip-hatch .tgc-hatch { stroke: var(--tgc-series, var(--chart-muted)); }\n/* A failing verdict without a value is a neutral ring: in its checker's hue it read as that checker passing. */\n.tgc-series-point.tgc-point-outline.tgc-strip-fail { stroke: var(--chart-ink-muted); }\n.tgc-strip-edge { fill: none; stroke: var(--tgc-series, var(--chart-muted)); stroke-width: 1.25; }\n.tgc-strip-observation:focus-visible { outline: none; }\n.tgc-strip-observation:focus-visible .tgc-series-hit { stroke: var(--chart-ink); stroke-width: 2; }\n.tgc-point-badge { paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; font-family: var(--font-mono, \"JetBrains Mono\", ui-monospace, monospace); }\n.tgc-point-label { fill: var(--chart-ink-muted); }\n.tgc-glyph-note { fill: var(--chart-ink); font-weight: 600; }\n/* Status colours appear only on these glyphs. */\n.tgc-glyph-check { fill: var(--chart-pass, var(--status-running)); font-weight: 600; }\n.tgc-glyph-cross { fill: var(--chart-breach, var(--status-error)); font-weight: 600; }\n/* Recorded targets sit under the data: a target is a soft band between solid edges, a stretch a dashed outline. */\n.tgc-target-target { fill: color-mix(in srgb, var(--chart-ink) 7%, transparent); }\n.tgc-target-stretch { fill: none; }\n.tgc-target-target-edge, .tgc-target-target.tgc-target-line { stroke: var(--chart-ink-muted); stroke-width: 1; }\n.tgc-target-stretch-edge, .tgc-target-stretch.tgc-target-line { stroke: var(--chart-ink-muted); stroke-width: 1; stroke-dasharray: 4 3; }\n.tgc-target-label { fill: var(--chart-ink-muted); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n/* Group headers: a labelled bracket above the plot and a light rule at each inner edge. */\n.tgc-group-rule { stroke: var(--chart-ink-muted); stroke-width: 1.5; }\n.tgc-group-edge { stroke: var(--chart-rule); stroke-width: 1; }\n.tgc-group-label { fill: var(--chart-ink); paint-order: stroke; stroke: var(--chart-canvas); stroke-width: 3px; stroke-linejoin: round; }\n/* The verdict key: glyphs in the neutral ink, so they describe a mark's form, not a series. */\n.tgc-key-mark { stroke-width: 1.5; }\n.tgc-key-filled { fill: var(--chart-ink-muted); stroke: none; }\n.tgc-key-outline { fill: none; stroke: var(--chart-ink-muted); }\n.tgc-key-dashed { fill: none; stroke: var(--chart-ink-muted); stroke-dasharray: 2 1.5; }\n";
|
|
1140
1140
|
/** --chart-* resolved per theme, for documents without brand tokens: light, dark by preference or [data-theme="dark"], light in print. */
|
|
1141
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}}";
|
|
1142
1142
|
//#endregion
|
|
@@ -2485,7 +2485,7 @@ function pointBadge(x, y, r, p, limit) {
|
|
|
2485
2485
|
function stripMark(x, y, size, p) {
|
|
2486
2486
|
const h = size / 2;
|
|
2487
2487
|
if (p.verdict === "unknown") return `<g class="tgc-strip-mark tgc-point-undecided">${hatch(x - h, y - h, size, size, "tgc-strip-hatch")}${rect(x - h, y - h, size, size, "tgc-strip-edge")}</g>`;
|
|
2488
|
-
return rect(x - h, y - h, size, size, p.verdict === "fail" ? "tgc-series-point tgc-point-outline" : "tgc-series-point");
|
|
2488
|
+
return rect(x - h, y - h, size, size, p.verdict === "fail" ? "tgc-series-point tgc-point-outline tgc-strip-fail" : "tgc-series-point");
|
|
2489
2489
|
}
|
|
2490
2490
|
/**
|
|
2491
2491
|
* Places each mark's badge to its right, or to its left at the plot's edge, skipping any that would cover a mark or
|
|
@@ -2747,7 +2747,7 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
|
|
|
2747
2747
|
cls: "tgc-ink-muted"
|
|
2748
2748
|
});
|
|
2749
2749
|
marks += drawBands(o.bands ?? [], px, top, bottom, xLabel, void 0, axis.extent);
|
|
2750
|
-
|
|
2750
|
+
const groups = drawGroups(o.bands ?? [], axis.extent, top, bottom, left, right, xLabel);
|
|
2751
2751
|
if (strip) marks += text(left - 8, bottom + 16, stripName, {
|
|
2752
2752
|
anchor: "end",
|
|
2753
2753
|
cls: "tgc-ink-muted",
|
|
@@ -2846,7 +2846,7 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
|
|
|
2846
2846
|
legendY += key.height;
|
|
2847
2847
|
marks += key.svg;
|
|
2848
2848
|
const events = drawMarks(o.marks ?? [], px, top - groupRowHeight(o.bands), bottom, left, right, xLabel, 22);
|
|
2849
|
-
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}${badges}${tips}</svg>`;
|
|
2849
|
+
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}${groups}${points}${badges}${tips}</svg>`;
|
|
2850
2850
|
}
|
|
2851
2851
|
function axisGutter(ticks, stripName) {
|
|
2852
2852
|
return Math.max(64, ...ticks.map((tick) => Math.ceil(textWidth(tick.label, 12, "mono")) + 20), stripName ? Math.ceil(textWidth(stripName, 11)) + 14 : 0);
|
|
@@ -3183,4 +3183,139 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3183
3183
|
return svgRoot(width, height + key.height, body + key.svg);
|
|
3184
3184
|
}
|
|
3185
3185
|
//#endregion
|
|
3186
|
-
|
|
3186
|
+
//#region src/unit-dots.ts
|
|
3187
|
+
/** Squarified treemap (Bruls, Huizing and van Wijk) of positive values, largest first, into the rectangle. */
|
|
3188
|
+
function squarify(values, x, y, w, h) {
|
|
3189
|
+
const out = new Array(values.length);
|
|
3190
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
3191
|
+
if (!total) return values.map(() => [
|
|
3192
|
+
x,
|
|
3193
|
+
y,
|
|
3194
|
+
0,
|
|
3195
|
+
0
|
|
3196
|
+
]);
|
|
3197
|
+
const scale = w * h / total;
|
|
3198
|
+
let i = 0;
|
|
3199
|
+
while (i < values.length) {
|
|
3200
|
+
const side = Math.min(w, h);
|
|
3201
|
+
let row = [i];
|
|
3202
|
+
let rowSum = values[i] * scale;
|
|
3203
|
+
const worst = (sum, items) => {
|
|
3204
|
+
const areas = items.map((k) => values[k] * scale);
|
|
3205
|
+
const max = Math.max(...areas), min = Math.min(...areas);
|
|
3206
|
+
return Math.max(side * side * max / (sum * sum), sum * sum / (side * side * min));
|
|
3207
|
+
};
|
|
3208
|
+
let j = i + 1;
|
|
3209
|
+
while (j < values.length) {
|
|
3210
|
+
const next = rowSum + values[j] * scale;
|
|
3211
|
+
if (worst(next, [...row, j]) > worst(rowSum, row)) break;
|
|
3212
|
+
row = [...row, j];
|
|
3213
|
+
rowSum = next;
|
|
3214
|
+
j++;
|
|
3215
|
+
}
|
|
3216
|
+
const thickness = rowSum / side;
|
|
3217
|
+
let offset = 0;
|
|
3218
|
+
for (const k of row) {
|
|
3219
|
+
const length = values[k] * scale / thickness;
|
|
3220
|
+
out[k] = w >= h ? [
|
|
3221
|
+
x,
|
|
3222
|
+
y + offset,
|
|
3223
|
+
thickness,
|
|
3224
|
+
length
|
|
3225
|
+
] : [
|
|
3226
|
+
x + offset,
|
|
3227
|
+
y,
|
|
3228
|
+
length,
|
|
3229
|
+
thickness
|
|
3230
|
+
];
|
|
3231
|
+
offset += length;
|
|
3232
|
+
}
|
|
3233
|
+
if (w >= h) {
|
|
3234
|
+
x += thickness;
|
|
3235
|
+
w -= thickness;
|
|
3236
|
+
} else {
|
|
3237
|
+
y += thickness;
|
|
3238
|
+
h -= thickness;
|
|
3239
|
+
}
|
|
3240
|
+
i = j;
|
|
3241
|
+
}
|
|
3242
|
+
return out;
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* Lays out `groups` in a `width` × `height` box: a treemap rectangle per group with a header for its label, and its
|
|
3246
|
+
* items on a grid of square cells inside it, filled row by row. Groups keep their input order in `positions`; the
|
|
3247
|
+
* treemap places larger groups first.
|
|
3248
|
+
*/
|
|
3249
|
+
function unitDotLayout(groups, width, height, o = {}) {
|
|
3250
|
+
const gap = o.gap ?? 6, header = o.header ?? 16, maxRadius = o.maxRadius ?? 6;
|
|
3251
|
+
const order = groups.map((group, index) => ({
|
|
3252
|
+
group,
|
|
3253
|
+
index
|
|
3254
|
+
})).filter(({ group }) => group.count > 0).sort((a, b) => b.group.count - a.group.count);
|
|
3255
|
+
const totalItems = order.reduce((sum, item) => sum + item.group.count, 0) || 1;
|
|
3256
|
+
const headerShare = header * width * .25 / (width * height) * totalItems;
|
|
3257
|
+
const boxes = squarify(order.map(({ group }) => group.count + headerShare), 0, 0, width, height);
|
|
3258
|
+
const rects = new Array(groups.length);
|
|
3259
|
+
const total = groups.reduce((sum, group) => sum + Math.max(0, group.count), 0);
|
|
3260
|
+
const positions = new Float32Array(total * 2);
|
|
3261
|
+
const radii = new Float32Array(total);
|
|
3262
|
+
const startOf = [];
|
|
3263
|
+
let start = 0;
|
|
3264
|
+
for (const group of groups) {
|
|
3265
|
+
startOf.push(start);
|
|
3266
|
+
start += Math.max(0, group.count);
|
|
3267
|
+
}
|
|
3268
|
+
order.forEach(({ group, index }, k) => {
|
|
3269
|
+
const [bx, by, bw, bh] = boxes[k];
|
|
3270
|
+
const x0 = bx + gap / 2, y0 = by + gap / 2, x1 = bx + bw - gap / 2, y1 = by + bh - gap / 2;
|
|
3271
|
+
rects[index] = {
|
|
3272
|
+
id: group.id,
|
|
3273
|
+
label: group.label,
|
|
3274
|
+
count: group.count,
|
|
3275
|
+
x0,
|
|
3276
|
+
y0,
|
|
3277
|
+
x1,
|
|
3278
|
+
y1
|
|
3279
|
+
};
|
|
3280
|
+
const innerW = Math.max(1, x1 - x0 - 4), innerH = Math.max(1, y1 - y0 - header - 2);
|
|
3281
|
+
let best = {
|
|
3282
|
+
cols: 1,
|
|
3283
|
+
cell: 0
|
|
3284
|
+
};
|
|
3285
|
+
const ideal = Math.max(1, Math.round(Math.sqrt(group.count * innerW / innerH)));
|
|
3286
|
+
for (let cols = Math.max(1, ideal - 2); cols <= ideal + 2; cols++) {
|
|
3287
|
+
const rows = Math.ceil(group.count / cols);
|
|
3288
|
+
const cell = Math.min(innerW / cols, innerH / rows);
|
|
3289
|
+
if (cell > best.cell) best = {
|
|
3290
|
+
cols,
|
|
3291
|
+
cell
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
const r = Math.min(maxRadius, best.cell * .4);
|
|
3295
|
+
for (let item = 0; item < group.count; item++) {
|
|
3296
|
+
const col = item % best.cols, row = Math.floor(item / best.cols);
|
|
3297
|
+
const p = startOf[index] + item;
|
|
3298
|
+
positions[p * 2] = x0 + 2 + (col + .5) * best.cell;
|
|
3299
|
+
positions[p * 2 + 1] = y0 + header + (row + .5) * best.cell;
|
|
3300
|
+
radii[p] = r;
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
groups.forEach((group, index) => {
|
|
3304
|
+
if (!rects[index]) rects[index] = {
|
|
3305
|
+
id: group.id,
|
|
3306
|
+
label: group.label,
|
|
3307
|
+
count: 0,
|
|
3308
|
+
x0: 0,
|
|
3309
|
+
y0: 0,
|
|
3310
|
+
x1: 0,
|
|
3311
|
+
y1: 0
|
|
3312
|
+
};
|
|
3313
|
+
});
|
|
3314
|
+
return {
|
|
3315
|
+
rects,
|
|
3316
|
+
positions,
|
|
3317
|
+
radii
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
//#endregion
|
|
3321
|
+
export { CHECKER_HUES, ENDING_COLORS, ENDING_ORDER, OUTCOME_COLORS, OUTCOME_ORDER, breakdown, chartTokensCss, checkerColor, comparisonIntervals, costFrontier, figureCss, heatmap, isRefusal, latencyRange, metricBars, phaseWaterfall, rankedRates, rateOrder, renderFigure, renderNotes, renderRefusals, smallMultiples, taskMatrix, timeSeries, timeline, unitDotLayout };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as YScale, I as Refusal, S as XCategory, T as YTick, _ as ChartBand, b as MarkVerdict, f as TickOutcome, g as BandKind, h as PointGlyph, i as TimelineTick, j as Figure, m as PointFields, n as TimelineOptions, r as TimelineSpan, t as TimelineLane, v as ChartMark, w as YTarget, x as MarkerShape, y as MarkKind } from "../timeline-BvVxPTGp.js";
|
|
2
2
|
import { ReactElement, ReactNode } from "react";
|
|
3
3
|
//#region src/react/sparkline.d.ts
|
|
4
4
|
type SparklineDirection = "rising" | "falling" | "flat";
|
|
@@ -272,4 +272,50 @@ interface TimelineViewProps {
|
|
|
272
272
|
}
|
|
273
273
|
export declare function TimelineView({ lanes, options, defaultCollapsed, presentation }: TimelineViewProps): import("react").JSX.Element;
|
|
274
274
|
//#endregion
|
|
275
|
-
|
|
275
|
+
//#region src/react/dot-map.d.ts
|
|
276
|
+
interface DotMapRect {
|
|
277
|
+
x0: number;
|
|
278
|
+
y0: number;
|
|
279
|
+
x1: number;
|
|
280
|
+
y1: number;
|
|
281
|
+
label: string;
|
|
282
|
+
/** Shown after the label, such as the group's count. */
|
|
283
|
+
note?: string;
|
|
284
|
+
}
|
|
285
|
+
interface DotMapLabel {
|
|
286
|
+
x: number;
|
|
287
|
+
y: number;
|
|
288
|
+
text: string;
|
|
289
|
+
}
|
|
290
|
+
interface DotMapProps {
|
|
291
|
+
/** x and y of every point in layout units, interleaved. */
|
|
292
|
+
positions: Float32Array;
|
|
293
|
+
/** Each point's radius in layout units, or one radius for all. */
|
|
294
|
+
radii: Float32Array | number;
|
|
295
|
+
/** Each point's palette index. */
|
|
296
|
+
colors: Uint8Array;
|
|
297
|
+
/** CSS colours (custom properties allowed) by palette index. */
|
|
298
|
+
palette: readonly string[];
|
|
299
|
+
/** The box the positions are laid out in: [x0, y0, x1, y1]. The map fits it on first draw and on reset. */
|
|
300
|
+
extent: readonly [number, number, number, number];
|
|
301
|
+
/** Group rectangles drawn under the points, labelled at their top left. */
|
|
302
|
+
rects?: readonly DotMapRect[];
|
|
303
|
+
/** Labels drawn over the points, such as cluster names at their centroids. */
|
|
304
|
+
labels?: readonly DotMapLabel[];
|
|
305
|
+
/** 1 dims a point (filtered out); dimmed points draw first and faint. */
|
|
306
|
+
dimmed?: Uint8Array | null;
|
|
307
|
+
selected?: number | null;
|
|
308
|
+
/** Radius floor in screen pixels, so a dense semantic map stays visible zoomed out. Default 1.2. */
|
|
309
|
+
minPixelRadius?: number;
|
|
310
|
+
onHover?: (index: number | null) => void;
|
|
311
|
+
onSelect?: (index: number | null) => void;
|
|
312
|
+
/** Text for a point's hover card. */
|
|
313
|
+
describe?: (index: number) => string;
|
|
314
|
+
height?: number;
|
|
315
|
+
ariaLabel: string;
|
|
316
|
+
}
|
|
317
|
+
/** The categorical lens colours, in slot order, then "other": the palette a DotMap lens indexes. */
|
|
318
|
+
export declare const DOT_MAP_CATEGORIES: readonly string[];
|
|
319
|
+
export declare function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius, onHover, onSelect, describe, height, ariaLabel }: DotMapProps): import("react").JSX.Element;
|
|
320
|
+
//#endregion
|
|
321
|
+
export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
|
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { G as hatchPath, J as markerPath, b as isRefusal, h as eventsGap, t as timeline, u as MARK_HEADS, y as markName } from "../timeline-CVXBpZzt.js";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment as Fragment$1, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment as Fragment$1, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
//#region src/react/sparkline.tsx
|
|
5
5
|
const DEFAULT_SPARKLINE_WIDTH = 96;
|
|
6
6
|
const DEFAULT_SPARKLINE_HEIGHT = 24;
|
|
@@ -1736,4 +1736,435 @@ function TimelineView({ lanes, options, defaultCollapsed = [], presentation }) {
|
|
|
1736
1736
|
});
|
|
1737
1737
|
}
|
|
1738
1738
|
//#endregion
|
|
1739
|
-
|
|
1739
|
+
//#region src/react/dot-map.tsx
|
|
1740
|
+
/**
|
|
1741
|
+
* A canvas of many dots (thousands of runs or agents) that pans and zooms at 60 fps: unit dots in group rectangles
|
|
1742
|
+
* (`unitDotLayout`) or a semantic map's x/y. Colours are palette indexes, so a lens change repaints without a new
|
|
1743
|
+
* layout; the caller owns hover and selection and draws its own legend and details.
|
|
1744
|
+
*/
|
|
1745
|
+
/** The categorical lens colours, in slot order, then "other": the palette a DotMap lens indexes. */
|
|
1746
|
+
const DOT_MAP_CATEGORIES = Object.freeze([
|
|
1747
|
+
"var(--chart-category-1)",
|
|
1748
|
+
"var(--chart-category-2)",
|
|
1749
|
+
"var(--chart-category-3)",
|
|
1750
|
+
"var(--chart-category-other)"
|
|
1751
|
+
]);
|
|
1752
|
+
/** Resolves CSS colours (including custom properties and light-dark()) to what the page computes, for the canvas. */
|
|
1753
|
+
function resolveColors(host, palette) {
|
|
1754
|
+
const probe = document.createElement("span");
|
|
1755
|
+
probe.style.display = "none";
|
|
1756
|
+
host.appendChild(probe);
|
|
1757
|
+
const out = palette.map((color) => {
|
|
1758
|
+
probe.style.color = "";
|
|
1759
|
+
probe.style.color = color;
|
|
1760
|
+
return getComputedStyle(probe).color || color;
|
|
1761
|
+
});
|
|
1762
|
+
probe.remove();
|
|
1763
|
+
return out;
|
|
1764
|
+
}
|
|
1765
|
+
/** A uniform grid over the layout, for hit tests in constant time per pointer move. */
|
|
1766
|
+
function gridIndex(positions, extent, cell) {
|
|
1767
|
+
const [x0, y0, x1, y1] = extent;
|
|
1768
|
+
const cols = Math.max(1, Math.ceil((x1 - x0) / cell));
|
|
1769
|
+
const rows = Math.max(1, Math.ceil((y1 - y0) / cell));
|
|
1770
|
+
const cells = /* @__PURE__ */ new Map();
|
|
1771
|
+
for (let i = 0; i < positions.length / 2; i++) {
|
|
1772
|
+
const c = Math.min(cols - 1, Math.max(0, Math.floor((positions[i * 2] - x0) / cell)));
|
|
1773
|
+
const key = Math.min(rows - 1, Math.max(0, Math.floor((positions[i * 2 + 1] - y0) / cell))) * cols + c;
|
|
1774
|
+
const list = cells.get(key);
|
|
1775
|
+
if (list) list.push(i);
|
|
1776
|
+
else cells.set(key, [i]);
|
|
1777
|
+
}
|
|
1778
|
+
return {
|
|
1779
|
+
cols,
|
|
1780
|
+
rows,
|
|
1781
|
+
cell,
|
|
1782
|
+
cells,
|
|
1783
|
+
x0,
|
|
1784
|
+
y0
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius = 1.2, onHover, onSelect, describe, height = 560, ariaLabel }) {
|
|
1788
|
+
const box = useRef(null);
|
|
1789
|
+
const canvas = useRef(null);
|
|
1790
|
+
const [size, setSize] = useState({
|
|
1791
|
+
width: 0,
|
|
1792
|
+
height
|
|
1793
|
+
});
|
|
1794
|
+
const view = useRef({
|
|
1795
|
+
scale: 1,
|
|
1796
|
+
tx: 0,
|
|
1797
|
+
ty: 0
|
|
1798
|
+
});
|
|
1799
|
+
const fitted = useRef("");
|
|
1800
|
+
const frame = useRef(0);
|
|
1801
|
+
const [hover, setHover] = useState(null);
|
|
1802
|
+
const colorsResolved = useRef([]);
|
|
1803
|
+
const count = positions.length / 2;
|
|
1804
|
+
const maxRadius = useMemo(() => typeof radii === "number" ? radii : radii.reduce((max, r) => r > max ? r : max, 0), [radii]);
|
|
1805
|
+
const index = useMemo(() => gridIndex(positions, extent, Math.max(maxRadius * 2, (extent[2] - extent[0]) / 200)), [
|
|
1806
|
+
positions,
|
|
1807
|
+
extent,
|
|
1808
|
+
maxRadius
|
|
1809
|
+
]);
|
|
1810
|
+
useEffect(() => {
|
|
1811
|
+
if (!box.current) return;
|
|
1812
|
+
const observer = new ResizeObserver(([entry]) => setSize({
|
|
1813
|
+
width: Math.floor(entry.contentRect.width),
|
|
1814
|
+
height
|
|
1815
|
+
}));
|
|
1816
|
+
observer.observe(box.current);
|
|
1817
|
+
return () => observer.disconnect();
|
|
1818
|
+
}, [height]);
|
|
1819
|
+
const fit = useCallback(() => {
|
|
1820
|
+
const [x0, y0, x1, y1] = extent;
|
|
1821
|
+
const pad = 8;
|
|
1822
|
+
const scale = Math.min((size.width - 16) / Math.max(1e-9, x1 - x0), (size.height - 16) / Math.max(1e-9, y1 - y0));
|
|
1823
|
+
view.current = {
|
|
1824
|
+
scale,
|
|
1825
|
+
tx: pad - x0 * scale + (size.width - 16 - (x1 - x0) * scale) / 2,
|
|
1826
|
+
ty: pad - y0 * scale + (size.height - 16 - (y1 - y0) * scale) / 2
|
|
1827
|
+
};
|
|
1828
|
+
}, [extent, size]);
|
|
1829
|
+
const draw = useCallback(() => {
|
|
1830
|
+
frame.current = 0;
|
|
1831
|
+
const element = canvas.current;
|
|
1832
|
+
if (!element || !size.width) return;
|
|
1833
|
+
const started = performance.now();
|
|
1834
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1835
|
+
if (element.width !== Math.round(size.width * dpr) || element.height !== Math.round(size.height * dpr)) {
|
|
1836
|
+
element.width = Math.round(size.width * dpr);
|
|
1837
|
+
element.height = Math.round(size.height * dpr);
|
|
1838
|
+
}
|
|
1839
|
+
const ctx = element.getContext("2d");
|
|
1840
|
+
if (!ctx) return;
|
|
1841
|
+
if (!colorsResolved.current.length && box.current) colorsResolved.current = resolveColors(box.current, palette);
|
|
1842
|
+
const { scale, tx, ty } = view.current;
|
|
1843
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1844
|
+
ctx.clearRect(0, 0, size.width, size.height);
|
|
1845
|
+
const style = box.current ? getComputedStyle(box.current) : null;
|
|
1846
|
+
const ink = style?.color || "#888";
|
|
1847
|
+
if (rects?.length) {
|
|
1848
|
+
ctx.lineWidth = 1;
|
|
1849
|
+
ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
|
|
1850
|
+
for (const rect of rects) {
|
|
1851
|
+
if (rect.x1 <= rect.x0) continue;
|
|
1852
|
+
const x = rect.x0 * scale + tx, y = rect.y0 * scale + ty, w = (rect.x1 - rect.x0) * scale, h = (rect.y1 - rect.y0) * scale;
|
|
1853
|
+
if (x > size.width || y > size.height || x + w < 0 || y + h < 0) continue;
|
|
1854
|
+
ctx.globalAlpha = .35;
|
|
1855
|
+
ctx.strokeStyle = ink;
|
|
1856
|
+
ctx.strokeRect(x + .5, y + .5, w, h);
|
|
1857
|
+
if (w > 36) {
|
|
1858
|
+
ctx.globalAlpha = .8;
|
|
1859
|
+
ctx.fillStyle = ink;
|
|
1860
|
+
const text = rect.note ? `${rect.label} · ${rect.note}` : rect.label;
|
|
1861
|
+
ctx.save();
|
|
1862
|
+
ctx.beginPath();
|
|
1863
|
+
ctx.rect(x, y, w - 4, 16);
|
|
1864
|
+
ctx.clip();
|
|
1865
|
+
ctx.fillText(text, x + 4, y + 12);
|
|
1866
|
+
ctx.restore();
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
ctx.globalAlpha = 1;
|
|
1870
|
+
}
|
|
1871
|
+
const paths = /* @__PURE__ */ new Map();
|
|
1872
|
+
const faint = new Path2D();
|
|
1873
|
+
const r0 = typeof radii === "number" ? radii : 0;
|
|
1874
|
+
for (let i = 0; i < count; i++) {
|
|
1875
|
+
const x = positions[i * 2] * scale + tx;
|
|
1876
|
+
const y = positions[i * 2 + 1] * scale + ty;
|
|
1877
|
+
if (x < -8 || y < -8 || x > size.width + 8 || y > size.height + 8) continue;
|
|
1878
|
+
const r = Math.max(minPixelRadius, (typeof radii === "number" ? r0 : radii[i]) * scale);
|
|
1879
|
+
const path = dimmed?.[i] ? faint : paths.get(colors[i]) ?? paths.set(colors[i], new Path2D()).get(colors[i]);
|
|
1880
|
+
path.moveTo(x + r, y);
|
|
1881
|
+
path.arc(x, y, r, 0, Math.PI * 2);
|
|
1882
|
+
}
|
|
1883
|
+
ctx.globalAlpha = .18;
|
|
1884
|
+
ctx.fillStyle = ink;
|
|
1885
|
+
ctx.fill(faint);
|
|
1886
|
+
ctx.globalAlpha = .92;
|
|
1887
|
+
for (const [color, path] of paths) {
|
|
1888
|
+
ctx.fillStyle = colorsResolved.current[color] ?? ink;
|
|
1889
|
+
ctx.fill(path);
|
|
1890
|
+
}
|
|
1891
|
+
ctx.globalAlpha = 1;
|
|
1892
|
+
if (selected !== null && selected !== void 0 && selected < count) {
|
|
1893
|
+
const x = positions[selected * 2] * scale + tx, y = positions[selected * 2 + 1] * scale + ty;
|
|
1894
|
+
const r = Math.max(4, (typeof radii === "number" ? r0 : radii[selected]) * scale + 3);
|
|
1895
|
+
ctx.lineWidth = 2;
|
|
1896
|
+
ctx.strokeStyle = ink;
|
|
1897
|
+
ctx.beginPath();
|
|
1898
|
+
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
1899
|
+
ctx.stroke();
|
|
1900
|
+
}
|
|
1901
|
+
if (labels?.length) {
|
|
1902
|
+
ctx.font = "600 12px ui-sans-serif, system-ui, sans-serif";
|
|
1903
|
+
ctx.textAlign = "center";
|
|
1904
|
+
ctx.lineWidth = 3;
|
|
1905
|
+
const surface = style?.getPropertyValue("--dot-map-surface").trim() || style?.backgroundColor || "#161616";
|
|
1906
|
+
const taken = [];
|
|
1907
|
+
for (const label of labels) {
|
|
1908
|
+
const x = label.x * scale + tx, y = label.y * scale + ty;
|
|
1909
|
+
if (x < 0 || y < 0 || x > size.width || y > size.height) continue;
|
|
1910
|
+
const w = ctx.measureText(label.text).width;
|
|
1911
|
+
const box4 = [
|
|
1912
|
+
x - w / 2 - 2,
|
|
1913
|
+
y - 10,
|
|
1914
|
+
x + w / 2 + 2,
|
|
1915
|
+
y + 4
|
|
1916
|
+
];
|
|
1917
|
+
if (taken.some(([a, b, c, d]) => box4[0] < c && box4[2] > a && box4[1] < d && box4[3] > b)) continue;
|
|
1918
|
+
taken.push(box4);
|
|
1919
|
+
ctx.strokeStyle = surface;
|
|
1920
|
+
ctx.strokeText(label.text, x, y);
|
|
1921
|
+
ctx.fillStyle = ink;
|
|
1922
|
+
ctx.fillText(label.text, x, y);
|
|
1923
|
+
}
|
|
1924
|
+
ctx.textAlign = "start";
|
|
1925
|
+
}
|
|
1926
|
+
element.dataset.frameMs = (performance.now() - started).toFixed(2);
|
|
1927
|
+
}, [
|
|
1928
|
+
size,
|
|
1929
|
+
positions,
|
|
1930
|
+
radii,
|
|
1931
|
+
colors,
|
|
1932
|
+
palette,
|
|
1933
|
+
rects,
|
|
1934
|
+
labels,
|
|
1935
|
+
dimmed,
|
|
1936
|
+
selected,
|
|
1937
|
+
count,
|
|
1938
|
+
minPixelRadius
|
|
1939
|
+
]);
|
|
1940
|
+
const schedule = useCallback(() => {
|
|
1941
|
+
if (!frame.current) frame.current = requestAnimationFrame(draw);
|
|
1942
|
+
}, [draw]);
|
|
1943
|
+
useEffect(() => {
|
|
1944
|
+
colorsResolved.current = [];
|
|
1945
|
+
}, [palette]);
|
|
1946
|
+
useEffect(() => {
|
|
1947
|
+
if (!size.width) return;
|
|
1948
|
+
const key = `${extent.join(",")}:${size.width}x${size.height}`;
|
|
1949
|
+
if (fitted.current !== key) {
|
|
1950
|
+
fit();
|
|
1951
|
+
fitted.current = key;
|
|
1952
|
+
}
|
|
1953
|
+
schedule();
|
|
1954
|
+
}, [
|
|
1955
|
+
size,
|
|
1956
|
+
extent,
|
|
1957
|
+
fit,
|
|
1958
|
+
schedule
|
|
1959
|
+
]);
|
|
1960
|
+
useEffect(() => schedule(), [schedule]);
|
|
1961
|
+
useEffect(() => () => cancelAnimationFrame(frame.current), []);
|
|
1962
|
+
const pick = useCallback((clientX, clientY) => {
|
|
1963
|
+
const element = canvas.current;
|
|
1964
|
+
if (!element) return null;
|
|
1965
|
+
const bounds = element.getBoundingClientRect();
|
|
1966
|
+
const { scale, tx, ty } = view.current;
|
|
1967
|
+
const lx = (clientX - bounds.left - tx) / scale;
|
|
1968
|
+
const ly = (clientY - bounds.top - ty) / scale;
|
|
1969
|
+
const c = Math.floor((lx - index.x0) / index.cell);
|
|
1970
|
+
const r = Math.floor((ly - index.y0) / index.cell);
|
|
1971
|
+
let best = null;
|
|
1972
|
+
let bestD = Infinity;
|
|
1973
|
+
const reach = Math.max(1, Math.ceil(6 / (scale * index.cell)));
|
|
1974
|
+
for (let dr = -reach; dr <= reach; dr++) for (let dc = -reach; dc <= reach; dc++) {
|
|
1975
|
+
if (c + dc < 0 || r + dr < 0 || c + dc >= index.cols || r + dr >= index.rows) continue;
|
|
1976
|
+
for (const i of index.cells.get((r + dr) * index.cols + (c + dc)) ?? []) {
|
|
1977
|
+
const dx = positions[i * 2] - lx, dy = positions[i * 2 + 1] - ly;
|
|
1978
|
+
const d = dx * dx + dy * dy;
|
|
1979
|
+
const hit = Math.max(6 / scale, typeof radii === "number" ? radii : radii[i]);
|
|
1980
|
+
if (d < bestD && d <= hit * hit) {
|
|
1981
|
+
bestD = d;
|
|
1982
|
+
best = i;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return best;
|
|
1987
|
+
}, [
|
|
1988
|
+
index,
|
|
1989
|
+
positions,
|
|
1990
|
+
radii
|
|
1991
|
+
]);
|
|
1992
|
+
const drag = useRef(null);
|
|
1993
|
+
const touches = useRef(/* @__PURE__ */ new Map());
|
|
1994
|
+
const pinch = useRef(null);
|
|
1995
|
+
useEffect(() => {
|
|
1996
|
+
const element = canvas.current;
|
|
1997
|
+
if (!element) return;
|
|
1998
|
+
const wheel = (event) => {
|
|
1999
|
+
event.preventDefault();
|
|
2000
|
+
const bounds = element.getBoundingClientRect();
|
|
2001
|
+
const px = event.clientX - bounds.left, py = event.clientY - bounds.top;
|
|
2002
|
+
const factor = Math.exp(-event.deltaY * .0015);
|
|
2003
|
+
const v = view.current;
|
|
2004
|
+
const scale = Math.min(v.scale * 400, Math.max(v.scale * .0025, v.scale * factor));
|
|
2005
|
+
view.current = {
|
|
2006
|
+
scale,
|
|
2007
|
+
tx: px - (px - v.tx) / v.scale * scale,
|
|
2008
|
+
ty: py - (py - v.ty) / v.scale * scale
|
|
2009
|
+
};
|
|
2010
|
+
schedule();
|
|
2011
|
+
};
|
|
2012
|
+
element.addEventListener("wheel", wheel, { passive: false });
|
|
2013
|
+
return () => element.removeEventListener("wheel", wheel);
|
|
2014
|
+
}, [schedule]);
|
|
2015
|
+
const hovered = hover ? describe?.(hover.index) ?? null : null;
|
|
2016
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2017
|
+
ref: box,
|
|
2018
|
+
className: "tgc-dot-map",
|
|
2019
|
+
style: {
|
|
2020
|
+
position: "relative",
|
|
2021
|
+
height
|
|
2022
|
+
},
|
|
2023
|
+
"data-points": count,
|
|
2024
|
+
children: [/* @__PURE__ */ jsx("canvas", {
|
|
2025
|
+
ref: canvas,
|
|
2026
|
+
role: "img",
|
|
2027
|
+
"aria-label": ariaLabel,
|
|
2028
|
+
tabIndex: 0,
|
|
2029
|
+
style: {
|
|
2030
|
+
width: "100%",
|
|
2031
|
+
height,
|
|
2032
|
+
display: "block",
|
|
2033
|
+
touchAction: "none",
|
|
2034
|
+
cursor: hover ? "pointer" : "grab"
|
|
2035
|
+
},
|
|
2036
|
+
onPointerDown: (event) => {
|
|
2037
|
+
touches.current.set(event.pointerId, {
|
|
2038
|
+
x: event.clientX,
|
|
2039
|
+
y: event.clientY
|
|
2040
|
+
});
|
|
2041
|
+
if (touches.current.size === 2) {
|
|
2042
|
+
const [a, b] = [...touches.current.values()];
|
|
2043
|
+
pinch.current = {
|
|
2044
|
+
distance: Math.hypot(a.x - b.x, a.y - b.y) || 1,
|
|
2045
|
+
view: { ...view.current }
|
|
2046
|
+
};
|
|
2047
|
+
drag.current = null;
|
|
2048
|
+
} else drag.current = {
|
|
2049
|
+
x: event.clientX,
|
|
2050
|
+
y: event.clientY,
|
|
2051
|
+
moved: false
|
|
2052
|
+
};
|
|
2053
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
2054
|
+
},
|
|
2055
|
+
onPointerMove: (event) => {
|
|
2056
|
+
if (touches.current.has(event.pointerId)) touches.current.set(event.pointerId, {
|
|
2057
|
+
x: event.clientX,
|
|
2058
|
+
y: event.clientY
|
|
2059
|
+
});
|
|
2060
|
+
const p = pinch.current;
|
|
2061
|
+
if (p && touches.current.size === 2) {
|
|
2062
|
+
const [a, b] = [...touches.current.values()];
|
|
2063
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
2064
|
+
const mx = (a.x + b.x) / 2 - bounds.left, my = (a.y + b.y) / 2 - bounds.top;
|
|
2065
|
+
const scale = p.view.scale * ((Math.hypot(a.x - b.x, a.y - b.y) || 1) / p.distance);
|
|
2066
|
+
view.current = {
|
|
2067
|
+
scale,
|
|
2068
|
+
tx: mx - (mx - p.view.tx) / p.view.scale * scale,
|
|
2069
|
+
ty: my - (my - p.view.ty) / p.view.scale * scale
|
|
2070
|
+
};
|
|
2071
|
+
schedule();
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const d = drag.current;
|
|
2075
|
+
if (d) {
|
|
2076
|
+
const dx = event.clientX - d.x, dy = event.clientY - d.y;
|
|
2077
|
+
if (d.moved || Math.abs(dx) + Math.abs(dy) > 3) {
|
|
2078
|
+
d.moved = true;
|
|
2079
|
+
view.current = {
|
|
2080
|
+
...view.current,
|
|
2081
|
+
tx: view.current.tx + dx,
|
|
2082
|
+
ty: view.current.ty + dy
|
|
2083
|
+
};
|
|
2084
|
+
d.x = event.clientX;
|
|
2085
|
+
d.y = event.clientY;
|
|
2086
|
+
schedule();
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
const i = pick(event.clientX, event.clientY);
|
|
2091
|
+
const bounds = event.currentTarget.getBoundingClientRect();
|
|
2092
|
+
setHover(i === null ? null : {
|
|
2093
|
+
index: i,
|
|
2094
|
+
x: event.clientX - bounds.left,
|
|
2095
|
+
y: event.clientY - bounds.top
|
|
2096
|
+
});
|
|
2097
|
+
onHover?.(i);
|
|
2098
|
+
},
|
|
2099
|
+
onPointerUp: (event) => {
|
|
2100
|
+
touches.current.delete(event.pointerId);
|
|
2101
|
+
if (pinch.current) {
|
|
2102
|
+
if (touches.current.size < 2) pinch.current = null;
|
|
2103
|
+
drag.current = null;
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
const d = drag.current;
|
|
2107
|
+
drag.current = null;
|
|
2108
|
+
if (d && !d.moved) onSelect?.(pick(event.clientX, event.clientY));
|
|
2109
|
+
},
|
|
2110
|
+
onPointerCancel: (event) => {
|
|
2111
|
+
touches.current.delete(event.pointerId);
|
|
2112
|
+
if (touches.current.size < 2) pinch.current = null;
|
|
2113
|
+
drag.current = null;
|
|
2114
|
+
},
|
|
2115
|
+
onPointerLeave: () => {
|
|
2116
|
+
setHover(null);
|
|
2117
|
+
onHover?.(null);
|
|
2118
|
+
},
|
|
2119
|
+
onDoubleClick: () => {
|
|
2120
|
+
fit();
|
|
2121
|
+
schedule();
|
|
2122
|
+
},
|
|
2123
|
+
onKeyDown: (event) => {
|
|
2124
|
+
const v = view.current;
|
|
2125
|
+
const zoom = (factor) => {
|
|
2126
|
+
const cx = size.width / 2, cy = size.height / 2;
|
|
2127
|
+
view.current = {
|
|
2128
|
+
scale: v.scale * factor,
|
|
2129
|
+
tx: cx - (cx - v.tx) / v.scale * v.scale * factor,
|
|
2130
|
+
ty: cy - (cy - v.ty) / v.scale * v.scale * factor
|
|
2131
|
+
};
|
|
2132
|
+
};
|
|
2133
|
+
if (event.key === "+" || event.key === "=") zoom(1.4);
|
|
2134
|
+
else if (event.key === "-") zoom(1 / 1.4);
|
|
2135
|
+
else if (event.key === "0") fit();
|
|
2136
|
+
else if (event.key === "ArrowLeft") view.current = {
|
|
2137
|
+
...v,
|
|
2138
|
+
tx: v.tx + 60
|
|
2139
|
+
};
|
|
2140
|
+
else if (event.key === "ArrowRight") view.current = {
|
|
2141
|
+
...v,
|
|
2142
|
+
tx: v.tx - 60
|
|
2143
|
+
};
|
|
2144
|
+
else if (event.key === "ArrowUp") view.current = {
|
|
2145
|
+
...v,
|
|
2146
|
+
ty: v.ty + 60
|
|
2147
|
+
};
|
|
2148
|
+
else if (event.key === "ArrowDown") view.current = {
|
|
2149
|
+
...v,
|
|
2150
|
+
ty: v.ty - 60
|
|
2151
|
+
};
|
|
2152
|
+
else return;
|
|
2153
|
+
event.preventDefault();
|
|
2154
|
+
schedule();
|
|
2155
|
+
}
|
|
2156
|
+
}), hover && hovered && /* @__PURE__ */ jsx("div", {
|
|
2157
|
+
className: "tgc-dot-map-tip",
|
|
2158
|
+
role: "tooltip",
|
|
2159
|
+
style: {
|
|
2160
|
+
position: "absolute",
|
|
2161
|
+
left: Math.min(hover.x + 12, Math.max(0, size.width - 300)),
|
|
2162
|
+
top: hover.y + 12,
|
|
2163
|
+
pointerEvents: "none"
|
|
2164
|
+
},
|
|
2165
|
+
children: hovered
|
|
2166
|
+
})]
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
//#endregion
|
|
2170
|
+
export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DOT_MAP_CATEGORIES, DayBars, DotMap, FigureView, Gantt, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
|
package/dist/react/live.css
CHANGED
|
@@ -73,3 +73,18 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
|
|
|
73
73
|
.ov-event-idle { background: var(--chart-live-hover, var(--chart-shade)); }
|
|
74
74
|
.ov-event-missing { background: repeating-linear-gradient(135deg, var(--chart-live-faint, var(--chart-muted)) 0 1px, transparent 1px 4px); }
|
|
75
75
|
.ov-refusal { margin: 0; color: var(--chart-live-muted, var(--chart-ink-muted)); font-size: .9em; }
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* DotMap: thousands of dots on one canvas. A categorical lens names at most three categories in colour, the first
|
|
79
|
+
* three slots of the dataviz reference palette, which pass its all-pairs colour-vision checks in both themes (a
|
|
80
|
+
* scatter puts every pair side by side); every other category is the neutral "other". :where() keeps them overridable.
|
|
81
|
+
*/
|
|
82
|
+
:where(:root) {
|
|
83
|
+
--chart-category-1: light-dark(#2a78d6, #3987e5);
|
|
84
|
+
--chart-category-2: light-dark(#eb6834, #d95926);
|
|
85
|
+
--chart-category-3: light-dark(#1baf7a, #199e70);
|
|
86
|
+
--chart-category-other: #898781;
|
|
87
|
+
}
|
|
88
|
+
.tgc-dot-map { color: var(--chart-live-muted, #8a8a8a); }
|
|
89
|
+
.tgc-dot-map canvas:focus-visible { outline: 2px solid var(--chart-accent, #7760bd); outline-offset: 2px; }
|
|
90
|
+
.tgc-dot-map-tip { z-index: 2; max-width: 300px; padding: 6px 9px; border-radius: 6px; background: var(--chart-tip-bg, #262626); color: var(--chart-tip-ink, #eee); font: 12px/1.4 ui-sans-serif, system-ui, sans-serif; white-space: pre-line; box-shadow: 0 4px 14px rgb(0 0 0 / .3); }
|
|
@@ -225,13 +225,25 @@ interface PointFields {
|
|
|
225
225
|
type TickOutcome = "ok" | "quota" | "lost" | "rate-limit" | "oom" | "refused" | "aborted" | "other";
|
|
226
226
|
/**
|
|
227
227
|
* Outcomes in stack order. Adjacent outcomes pass the colour-vision checks in both themes in this order, so a stacked
|
|
228
|
-
* bar keeps it; `aborted` and `other`
|
|
229
|
-
* `open` and is drawn hatched, never as an outcome.
|
|
228
|
+
* bar keeps it; `ok` is the status green, `aborted` and `other` are two distinct greys and always carry a label. An
|
|
229
|
+
* attempt still running is `open` and is drawn hatched, never as an outcome.
|
|
230
230
|
*/
|
|
231
231
|
declare const OUTCOME_ORDER: readonly TickOutcome[];
|
|
232
232
|
/** The CSS colour of each outcome, for a caller that paints its own marks (a legend, a stacked bar). */
|
|
233
233
|
declare const OUTCOME_COLORS: Readonly<Record<TickOutcome, string>>;
|
|
234
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* How a run ended, in the order a legend lists them: `solved` (its check passed), `not solved` (it ended, its check
|
|
236
|
+
* did not pass), `died` (the platform or driver failed), `stalled`, `cancelled`, `live` and `never started`. Every
|
|
237
|
+
* view that colours runs by their ending uses these words and ENDING_COLORS, so green, red and amber mean one thing.
|
|
238
|
+
*/
|
|
239
|
+
type RunEnding = "solved" | "not solved" | "died" | "stalled" | "cancelled" | "live" | "never started";
|
|
240
|
+
declare const ENDING_ORDER: readonly RunEnding[];
|
|
241
|
+
/** The CSS colour of each run ending (`--chart-ending-*` in figure.css). */
|
|
242
|
+
declare const ENDING_COLORS: Readonly<Record<RunEnding, string>>;
|
|
243
|
+
/**
|
|
244
|
+
* How many checkers have their own hue. Index 0 is the current checker; later indexes are older checkers. Checker hues
|
|
245
|
+
* (blue, magenta, cyan, violet) avoid the status green, red and amber, so a checker's colour never reads as a verdict.
|
|
246
|
+
*/
|
|
235
247
|
declare const CHECKER_HUES = 4;
|
|
236
248
|
/** The CSS colour of checker `index` (0 is current). Indexes past the fourth hue fold into the muted tone. */
|
|
237
249
|
declare function checkerColor(index: number): string;
|
|
@@ -292,4 +304,4 @@ interface TimelineOptions {
|
|
|
292
304
|
}
|
|
293
305
|
declare function timeline(lanes: TimelineLane[], o: TimelineOptions): Figure | Refusal;
|
|
294
306
|
//#endregion
|
|
295
|
-
export {
|
|
307
|
+
export { Exclusion as A, isRefusal as B, YScale as C, ComparisonRow as D, BreakdownInput as E, RateRow as F, Refusal as I, Setup as L, Interval as M, IntervalMethod as N, CostBasis as O, MatrixCell as P, Tier as R, XCategory as S, YTick as T, ChartBand as _, timeline as a, MarkVerdict as b, ENDING_ORDER as c, RunEnding as d, TickOutcome as f, BandKind as g, PointGlyph as h, TimelineTick as i, Figure as j, Estimate as k, OUTCOME_COLORS as l, PointFields as m, TimelineOptions as n, CHECKER_HUES as o, checkerColor as p, TimelineSpan as r, ENDING_COLORS as s, TimelineLane as t, OUTCOME_ORDER as u, ChartMark as v, YTarget as w, MarkerShape as x, MarkKind as y, TierCounts as z };
|
|
@@ -635,13 +635,16 @@ function drawGroups(bands, extent, top, bottom, left, right, format, clip) {
|
|
|
635
635
|
return titled(`${band.label}: ${format(band.from)} to ${format(band.to)}`, `<g data-band="group">${edges}${bracket}</g>`);
|
|
636
636
|
}).join("");
|
|
637
637
|
}
|
|
638
|
-
/**
|
|
638
|
+
/**
|
|
639
|
+
* Marks of one kind and verdict merge when under 3 px apart, or under 12 px apart with the same label (a re-checked
|
|
640
|
+
* tag). A ruling never merges: each ruling made or kept a check, so each keeps its own rule and label.
|
|
641
|
+
*/
|
|
639
642
|
function groupMarks(marks, px) {
|
|
640
643
|
const groups = [];
|
|
641
644
|
for (const mark of [...marks].sort((a, b) => a.at - b.at)) {
|
|
642
645
|
const x = px(mark.at), last = groups.at(-1), lead = last?.marks[0];
|
|
643
646
|
const near = last && px(last.marks.at(-1).at);
|
|
644
|
-
if (last && lead.kind === mark.kind && lead.verdict === mark.verdict && (x - near < 3 || x - near < 12 && markName(lead) === markName(mark))) last.marks.push(mark);
|
|
647
|
+
if (last && mark.kind !== "ruling" && lead.kind === mark.kind && lead.verdict === mark.verdict && (x - near < 3 || x - near < 12 && markName(lead) === markName(mark))) last.marks.push(mark);
|
|
645
648
|
else groups.push({
|
|
646
649
|
x,
|
|
647
650
|
marks: [mark]
|
|
@@ -768,8 +771,8 @@ const EVENTS_READ = "Vertical rules mark recorded events (solid: pass, dashed: d
|
|
|
768
771
|
//#region src/outcomes.ts
|
|
769
772
|
/**
|
|
770
773
|
* Outcomes in stack order. Adjacent outcomes pass the colour-vision checks in both themes in this order, so a stacked
|
|
771
|
-
* bar keeps it; `aborted` and `other`
|
|
772
|
-
* `open` and is drawn hatched, never as an outcome.
|
|
774
|
+
* bar keeps it; `ok` is the status green, `aborted` and `other` are two distinct greys and always carry a label. An
|
|
775
|
+
* attempt still running is `open` and is drawn hatched, never as an outcome.
|
|
773
776
|
*/
|
|
774
777
|
const OUTCOME_ORDER = [
|
|
775
778
|
"ok",
|
|
@@ -782,8 +785,22 @@ const OUTCOME_ORDER = [
|
|
|
782
785
|
"other"
|
|
783
786
|
];
|
|
784
787
|
/** The CSS colour of each outcome, for a caller that paints its own marks (a legend, a stacked bar). */
|
|
785
|
-
const OUTCOME_COLORS = Object.freeze(Object.fromEntries(OUTCOME_ORDER.map((outcome) => [outcome, `var(--chart-outcome-${outcome === "aborted"
|
|
786
|
-
|
|
788
|
+
const OUTCOME_COLORS = Object.freeze(Object.fromEntries(OUTCOME_ORDER.map((outcome) => [outcome, `var(--chart-outcome-${outcome === "aborted" ? "neutral" : outcome})`])));
|
|
789
|
+
const ENDING_ORDER = [
|
|
790
|
+
"solved",
|
|
791
|
+
"not solved",
|
|
792
|
+
"died",
|
|
793
|
+
"stalled",
|
|
794
|
+
"cancelled",
|
|
795
|
+
"live",
|
|
796
|
+
"never started"
|
|
797
|
+
];
|
|
798
|
+
/** The CSS colour of each run ending (`--chart-ending-*` in figure.css). */
|
|
799
|
+
const ENDING_COLORS = Object.freeze(Object.fromEntries(ENDING_ORDER.map((ending) => [ending, `var(--chart-ending-${ending.replace(" ", "-")})`])));
|
|
800
|
+
/**
|
|
801
|
+
* How many checkers have their own hue. Index 0 is the current checker; later indexes are older checkers. Checker hues
|
|
802
|
+
* (blue, magenta, cyan, violet) avoid the status green, red and amber, so a checker's colour never reads as a verdict.
|
|
803
|
+
*/
|
|
787
804
|
const CHECKER_HUES = 4;
|
|
788
805
|
/** The CSS colour of checker `index` (0 is current). Indexes past the fourth hue fold into the muted tone. */
|
|
789
806
|
function checkerColor(index) {
|
|
@@ -1181,4 +1198,4 @@ function axisTicks(t0, t1, px, left, right, label) {
|
|
|
1181
1198
|
return fit([t0, t1]) ? [t0, t1] : [t0];
|
|
1182
1199
|
}
|
|
1183
1200
|
//#endregion
|
|
1184
|
-
export {
|
|
1201
|
+
export { titled as $, pct as A, usdTick as B, costText as C, intervalGap as D, estimateNote as E, rateOrder as F, hatchPath as G, MARKER_SHAPES as H, tierAtLeast as I, markerPath as J, hbar as K, tierSummary as L, pts as M, rateDataGap as N, intervalName as O, rateIntervalGap as P, text as Q, tierTotal as R, TIERS_BEST_FIRST as S, duration as T, circle as U, table as V, hatch as W, rect as X, polyline as Y, svgRoot as Z, groupRowHeight as _, OUTCOME_COLORS as a, plural as at, isRefusal as b, checkerColor as c, wrap as ct, MARK_VERDICTS as d, wideWidth as et, drawBands as f, eventsTable as g, eventsGap as h, ENDING_ORDER as i, listJoin as it, perPass as j, kn as k, EVENTS_READ as l, drawMarks as m, CHECKER_HUES as n, esc as nt, OUTCOME_ORDER as o, textBlock as ot, drawGroups as p, line as q, ENDING_COLORS as r, fitLines as rt, checkerClass as s, textWidth as st, timeline as t, capitalize as tt, MARK_HEADS as u, markLabelRows as v, drawable as w, METHOD_NAME as x, markName as y, usd as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|