@tangle-network/charts 2.0.18 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,9 +138,12 @@ const figure = metricBars([
138
138
  const html = isRefusal(figure) ? renderRefusals([figure]) : renderFigure(figure, 1, { presentation: "chart" });
139
139
  ```
140
140
 
141
- `timeSeries` draws recorded scalar values at strictly increasing, finite positions.
141
+ `timeSeries` draws recorded scalar values at finite positions in order; equal positions, such as two runs recorded at one time, share a place and are never shifted.
142
142
  The caller includes every configured position, with null for a missing observation.
143
143
  Null breaks the line; the library never interpolates a missing value.
144
+ `secondary: true` de-emphasises a series beside a primary in the same `colorIndex`, such as a max beside its p50.
145
+ `crosshair: true` draws a vertical rule at the point whose tip shows.
146
+ A host with its own pointer rule, such as nearest point within 48 px, sets `data-near` on that `[data-point]` group to show its tip, and sets `pointer-events: none` on `.tgc-series-observation` so hover does not show a second one; keyboard focus still shows each tip.
144
147
 
145
148
  ```ts
146
149
  import { isRefusal, renderFigure, renderRefusals, timeSeries } from "@tangle-network/charts";
package/dist/figure.css CHANGED
@@ -329,6 +329,8 @@
329
329
  .tgc-series-observation:focus-visible { outline: none; }
330
330
  .tgc-series-observation:focus-visible .tgc-series-hit { stroke: var(--chart-ink); stroke-width: 2; }
331
331
  .tgc-series-tip { opacity: 0; pointer-events: none; }
332
+ .tgc-series-secondary { opacity: .45; }
333
+ .tgc-crosshair { stroke: var(--chart-rule); stroke-width: 1; }
332
334
 
333
335
  .tgc-scalar-table { min-width: 620px; }
334
336
  .tgc-scalar-table tbody th { min-width: 180px; max-width: 260px; }
package/dist/index.d.ts CHANGED
@@ -199,6 +199,8 @@ interface TimeSeriesRow {
199
199
  points: TimePoint[];
200
200
  /** Draw this series in checker `colorIndex`'s hue (0 is the current checker); past the fourth hue it is muted. */
201
201
  colorIndex?: number;
202
+ /** De-emphasise this series beside a primary in the same hue, such as a max beside its p50. */
203
+ secondary?: boolean;
202
204
  }
203
205
  interface TimeSeriesOptions {
204
206
  x: {
@@ -236,6 +238,8 @@ interface TimeSeriesOptions {
236
238
  width?: number;
237
239
  /** false leaves out the series legend under the plot, for a caller that names its series itself. The verdict key stays. */
238
240
  legend?: boolean;
241
+ /** Draw a vertical rule at the x of the point whose tip shows. */
242
+ crosshair?: boolean;
239
243
  id?: string;
240
244
  }
241
245
  export declare function timeSeries(rows: TimeSeriesRow[], o: TimeSeriesOptions): Figure | Refusal;
package/dist/index.js CHANGED
@@ -1137,7 +1137,7 @@ function plot$1(costed, unplaced, drawLine, o) {
1137
1137
  //#endregion
1138
1138
  //#region src/css.generated.ts
1139
1139
  /** The figure block and mark styles (src/figure.css), for documents that inline their CSS. */
1140
- 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: 24px;\n font-weight: 600;\n color: var(--chart-ink);\n}\n\n.tgc-noteref {\n margin-left: 2px;\n font-size: 14px;\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: 14px;\n line-height: 20px;\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: 14px;\n line-height: 20px;\n color: var(--chart-ink-muted);\n}\n\n.tgc-data {\n margin: 8px 0 0;\n font-size: 14px;\n line-height: 20px;\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: 14px;\n line-height: 20px;\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: 8px 12px 8px 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: 14px;\n line-height: 20px;\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/* One plot text size on the 12/14/18/32 scale; numbers differ by tabular figures, not size. */\n.tgc-figure-plot svg text { font-size: 14px; }\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
+ 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: 24px;\n font-weight: 600;\n color: var(--chart-ink);\n}\n\n.tgc-noteref {\n margin-left: 2px;\n font-size: 14px;\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: 14px;\n line-height: 20px;\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: 14px;\n line-height: 20px;\n color: var(--chart-ink-muted);\n}\n\n.tgc-data {\n margin: 8px 0 0;\n font-size: 14px;\n line-height: 20px;\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: 14px;\n line-height: 20px;\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: 8px 12px 8px 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: 14px;\n line-height: 20px;\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.tgc-series-secondary { opacity: .45; }\n.tgc-crosshair { stroke: var(--chart-rule); stroke-width: 1; }\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/* One plot text size on the 12/14/18/32 scale; numbers differ by tabular figures, not size. */\n.tgc-figure-plot svg text { font-size: 14px; }\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";
1141
1141
  /** --chart-* resolved per theme, for documents without brand tokens: light, dark by preference or [data-theme="dark"], light in print. */
1142
1142
  const chartTokensCss = ":root{--chart-ink:hsl(0 0% 24.7059%);--chart-ink-muted:hsl(0 0% 41.9608%);--chart-muted:hsl(0 0% 36.4706%);--chart-grid:hsl(225 28.5714% 94.5098%);--chart-rule:hsl(222.8571 6.6667% 79.4118%);--chart-shade:color-mix(in srgb, hsl(246.2264 92.1739% 54.902%) 7%, transparent);--chart-accent:hsl(245.9504 54.2601% 56.2745%);--chart-tier-c:hsl(246.2687 54.4715% 75.8824%);--chart-tier-b:hsl(246.055 54.7739% 60.9804%);--chart-tier-a:hsl(246.0465 54.8936% 46.0784%);--chart-tier-s:hsl(245.5172 54.717% 31.1765%);--chart-pass:hsl(154.8148 64.8% 24.5098%);--chart-breach:hsl(0 75.1244% 39.4118%);--chart-canvas:hsl(0 0% 100%);color-scheme:light}\n@media (prefers-color-scheme: dark){:root:not([data-theme=\"light\"]){--chart-ink:hsl(0 0% 84.3137%);--chart-ink-muted:hsl(0 0% 60.3922%);--chart-muted:hsl(0 0% 67.0588%);--chart-grid:hsl(243.3333 20.4545% 17.2549%);--chart-rule:hsl(245 18.4615% 25.4902%);--chart-shade:color-mix(in srgb, hsl(245.5556 93.1034% 88.6275%) 7%, transparent);--chart-accent:hsl(246.0465 85.4305% 70.3922%);--chart-tier-c:hsl(246.1017 55.1402% 41.9608%);--chart-tier-b:hsl(246.1905 54.7826% 54.902%);--chart-tier-a:hsl(245.3933 54.6012% 68.0392%);--chart-tier-s:hsl(245.6604 54.6392% 80.9804%);--chart-pass:hsl(155.1429 64.8148% 78.8235%);--chart-breach:hsl(0 74.4681% 90.7843%);--chart-canvas:hsl(246.6667 31.0345% 5.6863%);color-scheme:dark}}\n:root[data-theme=\"dark\"]{--chart-ink:hsl(0 0% 84.3137%);--chart-ink-muted:hsl(0 0% 60.3922%);--chart-muted:hsl(0 0% 67.0588%);--chart-grid:hsl(243.3333 20.4545% 17.2549%);--chart-rule:hsl(245 18.4615% 25.4902%);--chart-shade:color-mix(in srgb, hsl(245.5556 93.1034% 88.6275%) 7%, transparent);--chart-accent:hsl(246.0465 85.4305% 70.3922%);--chart-tier-c:hsl(246.1017 55.1402% 41.9608%);--chart-tier-b:hsl(246.1905 54.7826% 54.902%);--chart-tier-a:hsl(245.3933 54.6012% 68.0392%);--chart-tier-s:hsl(245.6604 54.6392% 80.9804%);--chart-pass:hsl(155.1429 64.8148% 78.8235%);--chart-breach:hsl(0 74.4681% 90.7843%);--chart-canvas:hsl(246.6667 31.0345% 5.6863%);color-scheme:dark}\n@media print{:root,:root:not([data-theme=\"light\"]),:root[data-theme=\"dark\"]{--chart-ink:hsl(0 0% 24.7059%);--chart-ink-muted:hsl(0 0% 41.9608%);--chart-muted:hsl(0 0% 36.4706%);--chart-grid:hsl(225 28.5714% 94.5098%);--chart-rule:hsl(222.8571 6.6667% 79.4118%);--chart-shade:color-mix(in srgb, hsl(246.2264 92.1739% 54.902%) 7%, transparent);--chart-accent:hsl(245.9504 54.2601% 56.2745%);--chart-tier-c:hsl(246.2687 54.4715% 75.8824%);--chart-tier-b:hsl(246.055 54.7739% 60.9804%);--chart-tier-a:hsl(246.0465 54.8936% 46.0784%);--chart-tier-s:hsl(245.5172 54.717% 31.1765%);--chart-pass:hsl(154.8148 64.8% 24.5098%);--chart-breach:hsl(0 75.1244% 39.4118%);--chart-canvas:hsl(0 0% 100%);color-scheme:light}}";
1143
1143
  //#endregion
@@ -2617,16 +2617,11 @@ function verdictKey(points, markers, x, y, width) {
2617
2617
  /** Recorded trajectories. Explicit null observations break lines without interpolation. */
2618
2618
  function timeSeries(rows, o) {
2619
2619
  const id = o.id ?? "time-series";
2620
- const gap = domainGap(o.x.domain) ?? domainGap(o.y.domain) ?? categoriesGap(o.x.categories, o.x.domain) ?? yAxisGap(o.y);
2620
+ const gap = domainGap(o.x.domain) ?? domainGap(o.y.domain) ?? categoriesGap(o.x.categories, o.x.domain) ?? yAxisGap(o.y) ?? eventsGap(o.marks, o.bands, o.x.domain);
2621
2621
  if (gap) return {
2622
2622
  id,
2623
2623
  refused: gap
2624
2624
  };
2625
- const events = eventsGap(o.marks, o.bands, o.x.domain);
2626
- if (events) return {
2627
- id,
2628
- refused: events
2629
- };
2630
2625
  if (!rows.length || rows.every((row) => !row.points.length)) return {
2631
2626
  id,
2632
2627
  refused: "No trajectory observations recorded."
@@ -2635,26 +2630,16 @@ function timeSeries(rows, o) {
2635
2630
  id,
2636
2631
  refused: "Duplicate trajectory ids."
2637
2632
  };
2638
- const columns = o.x.categories?.length ? new Set(o.x.categories.map((c) => c.at)) : null;
2639
2633
  for (const row of rows) {
2640
2634
  if (row.colorIndex !== void 0 && (!Number.isInteger(row.colorIndex) || row.colorIndex < 0)) return {
2641
2635
  id,
2642
2636
  refused: `${row.label}: a colour index is a whole number of at least 0.`
2643
2637
  };
2644
- let previous = -Infinity;
2645
- for (const point of row.points) {
2646
- const ordered = columns ? point.x >= previous && columns.has(point.x) : point.x > previous;
2647
- if (!Number.isFinite(point.x) || !ordered || point.x < o.x.domain[0] || point.x > o.x.domain[1]) return {
2648
- id,
2649
- refused: columns ? `${row.label}: x values must be categories, in order.` : `${row.label}: x values must be finite, strictly increasing and inside the axis domain.`
2650
- };
2651
- previous = point.x;
2652
- const problem = scalarGap(point.y, point.interval, point.observations, o.y.domain) ?? logValueGap(point.y, o.y.scale) ?? logValueGap(point.interval?.lower ?? null, o.y.scale) ?? pointFieldsGap(point);
2653
- if (problem) return {
2654
- id,
2655
- refused: `${row.label} at ${point.x}: ${problem}`
2656
- };
2657
- }
2638
+ const problem = pointsGap(row.label, row.points, o.x, o.y.domain, o.y.scale);
2639
+ if (problem) return {
2640
+ id,
2641
+ refused: problem
2642
+ };
2658
2643
  }
2659
2644
  const category = new Map((o.x.categories ?? []).map((c) => [c.at, c.label]));
2660
2645
  const xLabel = (x) => o.formatX ? o.formatX(x) : category.get(x) ?? String(x);
@@ -2664,12 +2649,9 @@ function timeSeries(rows, o) {
2664
2649
  row,
2665
2650
  point
2666
2651
  })));
2667
- const measured = flat.filter(({ point }) => point.y !== null);
2668
2652
  const stripped = flat.filter(({ point }) => point.y === null && point.verdict !== void 0);
2669
2653
  const recorded = hasPointFields(flat.map(({ point }) => point));
2670
2654
  const marks = o.marks ?? [], bands = o.bands ?? [];
2671
- const hasEvents = marks.length + bands.length > 0;
2672
- const groups = bands.some((band) => band.kind === "group");
2673
2655
  const stripName = o.y.strip?.trim() || "no value";
2674
2656
  if (axisGutter(grid) >= 270) return {
2675
2657
  id,
@@ -2717,8 +2699,8 @@ function timeSeries(rows, o) {
2717
2699
  return {
2718
2700
  id,
2719
2701
  finding: `${capitalize(o.y.label)} by ${o.x.label}.`,
2720
- lede: `${measured.length} of ${flat.length} positions have a recorded value.${stripped.length ? ` ${stripped.length} recorded ${plural(stripped.length, "verdict")} without a value ${stripped.length === 1 ? "sits" : "sit"} in the strip under the plot.` : ""}`,
2721
- read: `Points show recorded values. Lines connect adjacent measured positions; missing positions break the line. Whiskers show recorded intervals.${recorded ? " A filled mark is a pass, an outline a fail, a dashed outline undecided; * is not comparable with the goal and ? of unknown comparability; ×n counts the recorded rows a mark stands for." : ""}${stripped.length ? ` Hatched squares under the plot are verdicts recorded without a value (${stripName}).` : ""}${o.y.targets?.length ? " Shaded bands are recorded targets; dashed bands are stretch targets." : ""}${groups ? " Brackets above the plot group positions, such as one run's releases." : ""}${hasEvents ? ` ${EVENTS_READ}` : ""}`,
2702
+ lede: `${flat.filter(({ point }) => point.y !== null).length} of ${flat.length} positions have a recorded value.${stripped.length ? ` ${stripped.length} recorded ${plural(stripped.length, "verdict")} without a value ${stripped.length === 1 ? "sits" : "sit"} in the strip under the plot.` : ""}`,
2703
+ read: `Points show recorded values. Lines connect adjacent measured positions; missing positions break the line. Whiskers show recorded intervals.${recorded ? " A filled mark is a pass, an outline a fail, a dashed outline undecided; * is not comparable with the goal and ? of unknown comparability; ×n counts the recorded rows a mark stands for." : ""}${stripped.length ? ` Hatched squares under the plot are verdicts recorded without a value (${stripName}).` : ""}${o.y.targets?.length ? " Shaded bands are recorded targets; dashed bands are stretch targets." : ""}${bands.some((band) => band.kind === "group") ? " Brackets above the plot group positions, such as one run's releases." : ""}${marks.length + bands.length ? ` ${EVENTS_READ}` : ""}`,
2722
2704
  svg: {
2723
2705
  wide: plot(rows, o, wideWidth(o.width), xLabel, yLabel, grid, facts, stripName),
2724
2706
  narrow: plot(rows, o, 358, xLabel, yLabel, grid, facts, stripName),
@@ -2772,18 +2754,8 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
2772
2754
  cls: "tgc-ink-muted",
2773
2755
  size: 12
2774
2756
  }) + line(left, bottom + strip, right, bottom + strip, "tgc-gridline");
2775
- const spread = (inStrip) => {
2776
- const byX = /* @__PURE__ */ new Map();
2777
- for (const row of rows) for (const point of row.points) if (point.y === null === inStrip && (!inStrip || point.verdict !== void 0)) byX.set(point.x, [...byX.get(point.x) ?? [], point]);
2778
- const at = /* @__PURE__ */ new Map();
2779
- for (const [x, list] of byX) {
2780
- const room = axis.column(x);
2781
- const step = room === null ? 0 : Math.min(10, room / (list.length + 1));
2782
- list.forEach((point, k) => at.set(point, px(x) + (k - (list.length - 1) / 2) * step));
2783
- }
2784
- return at;
2785
- };
2786
- const plotX = spread(false), stripX = spread(true);
2757
+ const all = rows.flatMap((row) => row.points);
2758
+ const plotX = spreadX(all.filter((point) => point.y !== null), axis, 10), stripX = spreadX(all.filter((point) => point.y === null && point.verdict !== void 0), axis, 10);
2787
2759
  let legendY = bottom + strip + turn + 72;
2788
2760
  let points = "", tips = "", rules = "";
2789
2761
  const placed = [];
@@ -2793,7 +2765,7 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
2793
2765
  for (const [seriesIndex, row] of rows.entries()) {
2794
2766
  const ord = row.colorIndex ?? seriesIndex;
2795
2767
  const dash = ord === 0 ? "" : `${2 + ord * 2} 3`;
2796
- const group = checkerClass(row.colorIndex);
2768
+ const group = `${checkerClass(row.colorIndex)}${row.secondary ? " tgc-series-secondary" : ""}`.trim();
2797
2769
  const wrapSeries = (inner) => group ? `<g class="${group}">${inner}</g>` : inner;
2798
2770
  let segment = [];
2799
2771
  let lines = "";
@@ -2846,9 +2818,9 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
2846
2818
  point
2847
2819
  });
2848
2820
  const tipWidth = Math.min(280, width - 24), tipX = Math.max(12, Math.min(width - tipWidth - 12, x - tipWidth / 2));
2849
- const tipLines = pointFacts.flatMap((fact) => wrap(fact, tipWidth - 24, 12, "sans", 8).lines);
2850
- tips += `<g class="tgc-series-tip" data-series-tip="${key}" aria-hidden="true">${rect(tipX, top, tipWidth, tipLines.length * 17 + 24, "tgc-tip-bg", "rx=\"7\"")}${textBlock(tipLines, tipX + 12, top + 20, 17, "class=\"tgc-tip-ink\" font-size=\"12\"")}</g>`;
2851
- rules += `.tgc-trajectories:has([data-point="${key}"]:hover) [data-series-tip="${key}"],.tgc-trajectories:not(:has(.tgc-series-observation:hover)):has([data-point="${key}"]:focus-visible) [data-series-tip="${key}"]{opacity:1}`;
2821
+ const tipLines = pointFacts.flatMap((fact) => wrap(fact, tipWidth - 24, 14, "sans", 8).lines);
2822
+ tips += `<g class="tgc-series-tip" data-series-tip="${key}" aria-hidden="true">${o.crosshair ? line(x, top, x, bottom, "tgc-crosshair") : ""}${rect(tipX, top, tipWidth, tipLines.length * 17 + 24, "tgc-tip-bg", "rx=\"7\"")}${textBlock(tipLines, tipX + 12, top + 20, 17, "class=\"tgc-tip-ink\" font-size=\"12\"")}</g>`;
2823
+ rules += `.tgc-trajectories:has([data-point="${key}"]:is(:hover,[data-near])) [data-series-tip="${key}"],.tgc-trajectories:not(:has(.tgc-series-observation:hover)):has([data-point="${key}"]:focus-visible) [data-series-tip="${key}"]{opacity:1}`;
2852
2824
  }
2853
2825
  flush();
2854
2826
  marks += wrapSeries(lines);
@@ -2867,6 +2839,29 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
2867
2839
  const events = drawMarks(o.marks ?? [], px, top - groupRowHeight(o.bands), bottom, left, right, xLabel, 22);
2868
2840
  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>`;
2869
2841
  }
2842
+ /** Several points in one column (a check and a gate, two checkers) are spread across it in input order. */
2843
+ function spreadX(points, axis, maxStep) {
2844
+ const byX = /* @__PURE__ */ new Map();
2845
+ for (const point of points) byX.set(point.x, [...byX.get(point.x) ?? [], point]);
2846
+ const at = /* @__PURE__ */ new Map();
2847
+ for (const [x, list] of byX) {
2848
+ const room = axis.column(x), step = room === null ? 0 : Math.min(maxStep, room / (list.length + 1));
2849
+ list.forEach((point, k) => at.set(point, axis.px(x) + (k - (list.length - 1) / 2) * step));
2850
+ }
2851
+ return at;
2852
+ }
2853
+ /** Why recorded points cannot be drawn on these axes, or null; `timeSeries` and `smallMultiples` refuse alike. */
2854
+ function pointsGap(label, points, x, domain, scale) {
2855
+ const columns = x.categories?.length ? new Set(x.categories.map((c) => c.at)) : null;
2856
+ let previous = -Infinity;
2857
+ for (const point of points) {
2858
+ if (!Number.isFinite(point.x) || point.x < previous || columns && !columns.has(point.x) || point.x < x.domain[0] || point.x > x.domain[1]) return columns ? `${label}: x values must be categories, in order.` : `${label}: x values must be finite, in order and inside the axis domain.`;
2859
+ previous = point.x;
2860
+ const problem = scalarGap(point.y, point.interval, point.observations, domain) ?? logValueGap(point.y, scale) ?? logValueGap(point.interval?.lower ?? null, scale) ?? pointFieldsGap(point);
2861
+ if (problem) return `${label} at ${point.x}: ${problem}`;
2862
+ }
2863
+ return null;
2864
+ }
2870
2865
  function axisGutter(ticks, stripName) {
2871
2866
  return Math.max(64, ...ticks.map((tick) => Math.ceil(textWidth(tick.label, 12, "mono")) + 20), stripName ? Math.ceil(textWidth(stripName, 12)) + 14 : 0);
2872
2867
  }
@@ -2892,7 +2887,6 @@ function smallMultiples(panels, o) {
2892
2887
  id,
2893
2888
  refused: "Columns must be a whole number of at least 1."
2894
2889
  };
2895
- const columns = o.x.categories?.length ? new Set(o.x.categories.map((c) => c.at)) : null;
2896
2890
  if (!panels.length) return {
2897
2891
  id,
2898
2892
  refused: "No panels recorded."
@@ -2952,20 +2946,11 @@ function smallMultiples(panels, o) {
2952
2946
  id,
2953
2947
  refused: `${panel.label}: ${own}`
2954
2948
  };
2955
- let previous = -Infinity;
2956
- for (const point of panel.points) {
2957
- const ordered = columns ? point.x >= previous && columns.has(point.x) : point.x > previous;
2958
- if (!Number.isFinite(point.x) || !ordered || point.x < o.x.domain[0] || point.x > o.x.domain[1]) return {
2959
- id,
2960
- refused: columns ? `${panel.label}: x values must be categories, in order.` : `${panel.label}: x values must be finite, strictly increasing and inside the shared domain.`
2961
- };
2962
- previous = point.x;
2963
- const problem = scalarGap(point.y, point.interval, point.observations, domain) ?? logValueGap(point.y, panel.scale) ?? pointFieldsGap(point);
2964
- if (problem) return {
2965
- id,
2966
- refused: `${panel.label} at ${point.x}: ${problem}`
2967
- };
2968
- }
2949
+ const problem = pointsGap(panel.label, panel.points, o.x, domain, panel.scale);
2950
+ if (problem) return {
2951
+ id,
2952
+ refused: problem
2953
+ };
2969
2954
  if (panel.end) {
2970
2955
  const endGap = eventsGap([{
2971
2956
  at: panel.end.at,
@@ -2976,7 +2961,7 @@ function smallMultiples(panels, o) {
2976
2961
  id,
2977
2962
  refused: `${panel.label}: ${endGap}`
2978
2963
  };
2979
- if (previous > panel.end.at) return {
2964
+ if (panel.points.at(-1).x > panel.end.at) return {
2980
2965
  id,
2981
2966
  refused: `${panel.label}: points are recorded after the panel's end.`
2982
2967
  };
@@ -3144,13 +3129,7 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
3144
3129
  anchor: "end",
3145
3130
  cls: "tgc-ink-muted tgc-num"
3146
3131
  });
3147
- const shared = /* @__PURE__ */ new Map();
3148
- for (const point of panel.points) if (point.y !== null) shared.set(point.x, [...shared.get(point.x) ?? [], point]);
3149
- const xOf = (point) => {
3150
- const list = shared.get(point.x), room = axis.column(point.x);
3151
- const step = room === null ? 0 : Math.min(9, room / (list.length + 1));
3152
- return px(point.x) + (list.indexOf(point) - (list.length - 1) / 2) * step;
3153
- };
3132
+ const xOf = spreadX(panel.points.filter((point) => point.y !== null), axis, 9);
3154
3133
  const placed = [];
3155
3134
  const pending = [];
3156
3135
  let segment = [];
@@ -3163,7 +3142,7 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
3163
3142
  flush();
3164
3143
  continue;
3165
3144
  }
3166
- const x = xOf(point), y = py(point.y);
3145
+ const x = xOf.get(point), y = py(point.y);
3167
3146
  segment.push([x, y]);
3168
3147
  if (!hasPointFields([point]) && point.marker === void 0) {
3169
3148
  body += titled(`${panel.label} · ${o.x.label} ${xLabel(point.x)} · ${yLabel(point.y)} · ${observationLabel(point.observations)}`, circle(x, y, 2.5, "tgc-series-point"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
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",