@tangle-network/charts 0.19.0 → 0.20.1
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 +1 -1
- package/dist/figure.css +8 -8
- package/dist/{flow-trace-D6RDkVrJ.d.ts → flow-trace-DqCMrnTs.d.ts} +31 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -39
- package/dist/react/flow-waterfall.css +74 -24
- package/dist/react/index.d.ts +16 -3
- package/dist/react/index.js +431 -207
- package/dist/react/live.css +24 -23
- package/dist/{timeline-CVXBpZzt.js → timeline-B7c-cede.js} +13 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -335,7 +335,7 @@ const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(cata
|
|
|
335
335
|
<FlowWaterfall trace={trace} live={running} defaultView="agents" renderIcon={brandMark} />;
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
`defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. `detail` becomes the agent's subtitle, and `facts` (`[{ label, value }]`) adds cells to its card. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
|
|
338
|
+
`defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. `detail` becomes the agent's subtitle, and `facts` (`[{ label, value }]`) adds cells to its card. `onTimeChange` reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (`costCoverage`), and `billedUsd` shows the real charge beside the list price. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
|
|
339
339
|
|
|
340
340
|
Live views append spans as they arrive: `traces watch <id|run-dir> --format jsonl --spans` emits a `traces.stream.span` event per new or changed span, and `otlpFlowTrace` keeps the last span per `span_id`. Running agent spans grow to the clock between deltas without React renders (CSS variables carry the clock). Search supports Cmd/Ctrl+F and keeps ancestor context. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row `—` timing mark. The static phase waterfall remains a separate chart for disjoint phases.
|
|
341
341
|
|
package/dist/figure.css
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
.tgc-finding {
|
|
20
20
|
margin: 0 0 8px;
|
|
21
21
|
font-size: 18px;
|
|
22
|
-
line-height:
|
|
23
|
-
font-weight:
|
|
22
|
+
line-height: 24px;
|
|
23
|
+
font-weight: 600;
|
|
24
24
|
color: var(--chart-ink);
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
|
|
41
41
|
.tgc-lede {
|
|
42
42
|
margin: 0 0 16px;
|
|
43
|
-
font-size:
|
|
44
|
-
line-height:
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
line-height: 20px;
|
|
45
45
|
color: var(--text-secondary, var(--chart-ink));
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
|
|
72
72
|
.tgc-read {
|
|
73
73
|
margin: 8px 0 0;
|
|
74
|
-
font-size:
|
|
75
|
-
line-height:
|
|
74
|
+
font-size: 12px;
|
|
75
|
+
line-height: 16px;
|
|
76
76
|
color: var(--chart-ink-muted);
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
}
|
|
120
120
|
.tgc-table th,
|
|
121
121
|
.tgc-table td {
|
|
122
|
-
padding:
|
|
122
|
+
padding: 8px 12px 8px 0;
|
|
123
123
|
border-bottom: 1px solid var(--chart-rule);
|
|
124
124
|
text-align: left;
|
|
125
125
|
vertical-align: top;
|
|
@@ -336,8 +336,8 @@
|
|
|
336
336
|
/* Plot presentation keeps the same complete data available to assistive technology. */
|
|
337
337
|
.tgc-accessible-data { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
338
338
|
.tgc-figure-plot .tgc-wide svg { box-sizing: content-box; width: calc(100% - 48px); padding: 0 24px; }
|
|
339
|
+
/* One plot text size on the 12/14/18/32 scale; numbers differ by tabular figures, not size. */
|
|
339
340
|
.tgc-figure-plot svg text { font-size: 14px; }
|
|
340
|
-
.tgc-figure-plot svg text.tgc-num { font-size: 15px; }
|
|
341
341
|
/* Larger tooltip text needs padding outside the original report backdrop. */
|
|
342
342
|
.tgc-figure-plot .tgc-tip-bg { stroke: var(--chart-ink); stroke-width: 24px; }
|
|
343
343
|
.tgc-figure-plot .tgc-phone-plot svg { min-width: var(--tgc-phone-width); }
|
|
@@ -339,8 +339,23 @@ interface FlowSpan {
|
|
|
339
339
|
label: string;
|
|
340
340
|
value: string;
|
|
341
341
|
}[];
|
|
342
|
+
/** Moments on this span's own bar, such as a restart: a notch at `at`, the state stays the span's. */
|
|
343
|
+
marks?: readonly FlowSpanMark[];
|
|
342
344
|
meta?: Record<string, unknown>;
|
|
343
345
|
}
|
|
346
|
+
/** A moment on one bar. A restart is a mark, never the final state: the span's status and outcome stay authoritative. */
|
|
347
|
+
interface FlowSpanMark {
|
|
348
|
+
/** Trace-relative milliseconds, on the same clock as span startMs. */
|
|
349
|
+
at: number;
|
|
350
|
+
label: string;
|
|
351
|
+
kind?: 'restart' | (string & {});
|
|
352
|
+
}
|
|
353
|
+
/** An interval when the whole run stood still, such as a host guard pause: a labelled band across every row, never blank space. */
|
|
354
|
+
interface FlowPause {
|
|
355
|
+
startMs: number;
|
|
356
|
+
endMs: number;
|
|
357
|
+
label: string;
|
|
358
|
+
}
|
|
344
359
|
/** A host verdict: `label` is the legend text, `tone` the status color. */
|
|
345
360
|
interface FlowOutcome {
|
|
346
361
|
tone: 'ok' | 'neutral' | 'failed' | 'warn';
|
|
@@ -351,7 +366,11 @@ interface FlowMarker {
|
|
|
351
366
|
/** Trace-relative milliseconds, on the same clock as span startMs. */
|
|
352
367
|
at: number;
|
|
353
368
|
label: string;
|
|
354
|
-
tone?: 'neutral' | 'ok' | 'failed';
|
|
369
|
+
tone?: 'neutral' | 'ok' | 'failed' | 'warn';
|
|
370
|
+
/** What happened, such as release, verdict, spawn or crash; shown with the label on the event track. */
|
|
371
|
+
kind?: string;
|
|
372
|
+
/** Where the event lives, for hosts that open it. */
|
|
373
|
+
href?: string;
|
|
355
374
|
}
|
|
356
375
|
/** Describe the structure of a flow trace including spans, timing, tokens, cost, and tool calls */
|
|
357
376
|
interface FlowTrace {
|
|
@@ -363,10 +382,21 @@ interface FlowTrace {
|
|
|
363
382
|
costUsd?: number;
|
|
364
383
|
/** `list`: priced from a model catalog's list prices, not a bill; the summary says so. */
|
|
365
384
|
costBasis?: 'billed' | 'list';
|
|
385
|
+
/** The list cost is at most this: the input counts include cached tokens priced as fresh input. */
|
|
386
|
+
costUpperBound?: boolean;
|
|
387
|
+
/** How many model calls `costUsd` covers; when `priced < total` the total is a lower bound and shown as one. */
|
|
388
|
+
costCoverage?: {
|
|
389
|
+
priced: number;
|
|
390
|
+
total: number;
|
|
391
|
+
};
|
|
392
|
+
/** The real charge when the producer knows it (for example $0 on a subscription seat), shown beside a list price. */
|
|
393
|
+
billedUsd?: number;
|
|
366
394
|
toolCalls: number;
|
|
367
395
|
/** Authoritative LLM request count when supplied; absent means unknown. */
|
|
368
396
|
modelCalls?: number;
|
|
369
397
|
errors?: number;
|
|
398
|
+
/** Intervals when nothing ran, drawn as labelled bands. */
|
|
399
|
+
pauses?: readonly FlowPause[];
|
|
370
400
|
}
|
|
371
401
|
//#endregion
|
|
372
402
|
export { BreakdownInput as A, Refusal as B, MarkKind as C, YScale as D, XCategory as E, Figure as F, Tier as H, Interval as I, IntervalMethod as L, CostBasis as M, Estimate as N, YTarget as O, Exclusion as P, MatrixCell as R, ChartMark as S, MarkerShape as T, TierCounts as U, Setup as V, isRefusal as W, checkerColor as _, TimelineLane as a, BandKind as b, TimelineTick as c, ENDING_COLORS as d, ENDING_ORDER as f, TickOutcome as g, RunEnding as h, FlowTrace as i, ComparisonRow as j, YTick as k, timeline as l, OUTCOME_ORDER as m, FlowOutcome as n, TimelineOptions as o, OUTCOME_COLORS as p, FlowSpan as r, TimelineSpan as s, FlowMarker as t, CHECKER_HUES as u, PointFields as v, MarkVerdict as w, ChartBand as x, PointGlyph as y, RateRow as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-
|
|
1
|
+
import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-DqCMrnTs.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. */
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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-B7c-cede.js";
|
|
2
2
|
//#region src/block.ts
|
|
3
3
|
/**
|
|
4
4
|
* The figure block: one markup for every figure on every surface.
|
|
@@ -260,9 +260,9 @@ function drawWide(columns, rows, fmt) {
|
|
|
260
260
|
const width = Math.ceil(left + columns.length * (cellW + gap));
|
|
261
261
|
let head = "", headHeight = 44;
|
|
262
262
|
columns.forEach((c, i) => {
|
|
263
|
-
const label = wrap(c.label, cellW - 5,
|
|
264
|
-
head += titled(c.label, textBlock(label.lines, left + i * (cellW + gap) + cellW / 2, 14,
|
|
265
|
-
headHeight = Math.max(headHeight, 12 + label.lines.length *
|
|
263
|
+
const label = wrap(c.label, cellW - 5, 12, "sans", 3);
|
|
264
|
+
head += titled(c.label, textBlock(label.lines, left + i * (cellW + gap) + cellW / 2, 14, 16, "class=\"tgc-ink-muted\" font-size=\"12\" text-anchor=\"middle\""));
|
|
265
|
+
headHeight = Math.max(headHeight, 12 + label.lines.length * 16 + 8);
|
|
266
266
|
});
|
|
267
267
|
let body = "";
|
|
268
268
|
rows.forEach((r, j) => {
|
|
@@ -290,9 +290,9 @@ function drawNarrow(columns, rows, fmt) {
|
|
|
290
290
|
});
|
|
291
291
|
columns.forEach((c, i) => {
|
|
292
292
|
const group = Math.floor(i / cols), x = 12 + i % cols * 113.33333333333333, y = top + 29 + group * groupH;
|
|
293
|
-
const label = wrap(c.label, 104.33333333333333,
|
|
293
|
+
const label = wrap(c.label, 104.33333333333333, 12, "sans", 1);
|
|
294
294
|
body += titled(c.label, text(x + 2, y + 9, label.lines[0], {
|
|
295
|
-
size:
|
|
295
|
+
size: 12,
|
|
296
296
|
cls: "tgc-ink-muted"
|
|
297
297
|
}));
|
|
298
298
|
body += cellMark(r.cells[i], x, y + 13, cellW, 29, `${r.label} · ${c.label}`, fmt);
|
|
@@ -375,7 +375,7 @@ function draw$3(rows, o, min, max, width) {
|
|
|
375
375
|
const scale = (ms) => left + (Math.log(ms) - Math.log(min)) / (Math.log(max) - Math.log(min)) * plotW;
|
|
376
376
|
let body = line(scale(o.sloMs), 37, scale(o.sloMs), top + rows.length * rowH, "tgc-threshold");
|
|
377
377
|
body += text(scale(o.sloMs) + 3, 15, `SLO ${(o.sloMs / 1e3).toFixed(0)} s`, {
|
|
378
|
-
size:
|
|
378
|
+
size: 12,
|
|
379
379
|
cls: "tgc-ink-muted tgc-num"
|
|
380
380
|
});
|
|
381
381
|
for (let exponent = Math.floor(Math.log10(min)); exponent <= Math.ceil(Math.log10(max)); exponent++) for (const step of [1, 5]) {
|
|
@@ -383,16 +383,16 @@ function draw$3(rows, o, min, max, width) {
|
|
|
383
383
|
if (tick <= min || tick >= max || Math.abs(scale(tick) - scale(o.sloMs)) < 19) continue;
|
|
384
384
|
body += line(scale(tick), 40, scale(tick), top + rows.length * rowH, "tgc-gridline");
|
|
385
385
|
body += text(scale(tick), 35, `${Number((tick / 1e3).toPrecision(2))} s`, {
|
|
386
|
-
size:
|
|
386
|
+
size: 12,
|
|
387
387
|
cls: "tgc-ink-muted tgc-num",
|
|
388
388
|
anchor: "middle"
|
|
389
389
|
});
|
|
390
390
|
}
|
|
391
391
|
rows.forEach((r, i) => {
|
|
392
392
|
const y = top + i * rowH;
|
|
393
|
-
const label = wrap(r.label, left - 10, phone ?
|
|
393
|
+
const label = wrap(r.label, left - 10, phone ? 12 : 14, "sans", phone ? 2 : 1);
|
|
394
394
|
body += titled(r.label, label.lines.map((part, j) => text(2, y + 5 + j * 14, part, {
|
|
395
|
-
size: phone ?
|
|
395
|
+
size: phone ? 12 : 14,
|
|
396
396
|
weight: 600
|
|
397
397
|
})).join(""));
|
|
398
398
|
const cy = y + (phone ? 7 : 0), status = r.p95Ms > o.sloMs ? "breach" : "pass";
|
|
@@ -502,16 +502,16 @@ function draw$2(rows, total, width, short) {
|
|
|
502
502
|
const x = left + plotW * i / 4;
|
|
503
503
|
body += line(x, 23, x, top + rows.length * rowH, "tgc-gridline");
|
|
504
504
|
body += text(x, 15, `${(total * i / 4e3).toFixed(0)} s`, {
|
|
505
|
-
size:
|
|
505
|
+
size: 12,
|
|
506
506
|
cls: "tgc-ink-muted tgc-num",
|
|
507
507
|
anchor: i === 0 ? "start" : i === 4 ? "end" : "middle"
|
|
508
508
|
});
|
|
509
509
|
}
|
|
510
510
|
rows.forEach((p, i) => {
|
|
511
511
|
const y = top + i * rowH, start = left + p.start / total * plotW, w = p.ms / total * plotW;
|
|
512
|
-
const label = wrap(p.label, phone ? width - 65 : left - 6,
|
|
512
|
+
const label = wrap(p.label, phone ? width - 65 : left - 6, 12, "sans", 1);
|
|
513
513
|
body += titled(p.label, text(phone ? 12 : 2, y + (phone ? 13 : 18), label.lines[0], {
|
|
514
|
-
size:
|
|
514
|
+
size: 12,
|
|
515
515
|
cls: p.id === "__unaccounted" ? "tgc-ink-muted" : "tgc-ink"
|
|
516
516
|
}));
|
|
517
517
|
const barY = y + (phone ? 21 : 5), barW = Math.max(p.ms > 0 ? 1 : 0, w);
|
|
@@ -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 { 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";
|
|
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: 24px;\n font-weight: 600;\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: 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: 12px;\n line-height: 16px;\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: 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: 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/* 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
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
|
|
@@ -2278,7 +2278,7 @@ function drawTargets(targets, py, left, right, yLabel, labels = true) {
|
|
|
2278
2278
|
const mark = y1 - y0 < 1.5 ? line(left, y0, right, y0, `${cls} tgc-target-line`) : rect(left, y0, right - left, y1 - y0, cls) + line(left, y0, right, y0, `${cls}-edge`) + line(left, y1, right, y1, `${cls}-edge`);
|
|
2279
2279
|
return titled(`${target.label}: ${target.from === target.to ? yLabel(target.from) : `${yLabel(target.from)} to ${yLabel(target.to)}`}`, `<g data-target="${target.kind}">${mark}</g>`);
|
|
2280
2280
|
}).join("") + (labels ? targets.map((target) => text(target.kind === "stretch" ? left + 4 : right - 4, py(target.to) - 4, target.label, {
|
|
2281
|
-
size:
|
|
2281
|
+
size: 12,
|
|
2282
2282
|
anchor: target.kind === "stretch" ? "start" : "end",
|
|
2283
2283
|
cls: "tgc-target-label"
|
|
2284
2284
|
})).join("") : "");
|
|
@@ -2287,10 +2287,10 @@ function drawTargets(targets, py, left, right, yLabel, labels = true) {
|
|
|
2287
2287
|
function targetKey(targets, x, y, maxWidth) {
|
|
2288
2288
|
let out = "", at = x;
|
|
2289
2289
|
for (const target of targets) {
|
|
2290
|
-
const w = textWidth(target.label,
|
|
2290
|
+
const w = textWidth(target.label, 12) * 1.1 + 34;
|
|
2291
2291
|
if (at + w - x > maxWidth) break;
|
|
2292
2292
|
out += line(at, y - 3.5, at + 14, y - 3.5, `tgc-target-${target.kind}-edge`) + text(at + 18, y, target.label, {
|
|
2293
|
-
size:
|
|
2293
|
+
size: 12,
|
|
2294
2294
|
cls: "tgc-target-label"
|
|
2295
2295
|
});
|
|
2296
2296
|
at += w;
|
|
@@ -2473,16 +2473,16 @@ function pointBadge(x, y, r, p, limit, above = false) {
|
|
|
2473
2473
|
svg: "",
|
|
2474
2474
|
width: 0
|
|
2475
2475
|
};
|
|
2476
|
-
const width = textWidth(parts.map(([t]) => t).join(" "),
|
|
2476
|
+
const width = textWidth(parts.map(([t]) => t).join(" "), 12, "mono");
|
|
2477
2477
|
const spans = parts.map(([t, cls], i) => `<tspan class="${cls}">${i ? " " : ""}${esc(t)}</tspan>`).join("");
|
|
2478
2478
|
if (above) return {
|
|
2479
|
-
svg: `<text class="tgc-point-badge" x="${Math.round(x * 100) / 100}" y="${Math.round((y - r - 5) * 100) / 100}" font-size="
|
|
2479
|
+
svg: `<text class="tgc-point-badge" x="${Math.round(x * 100) / 100}" y="${Math.round((y - r - 5) * 100) / 100}" font-size="12" text-anchor="middle">${spans}</text>`,
|
|
2480
2480
|
width
|
|
2481
2481
|
};
|
|
2482
2482
|
const left = x + r + 3 + width > limit;
|
|
2483
2483
|
const tx = left ? x - r - 3 : x + r + 3;
|
|
2484
2484
|
return {
|
|
2485
|
-
svg: `<text class="tgc-point-badge" x="${Math.round(tx * 100) / 100}" y="${Math.round((y + 3.5) * 100) / 100}" font-size="
|
|
2485
|
+
svg: `<text class="tgc-point-badge" x="${Math.round(tx * 100) / 100}" y="${Math.round((y + 3.5) * 100) / 100}" font-size="12"${left ? " text-anchor=\"end\"" : ""}>${spans}</text>`,
|
|
2486
2486
|
width
|
|
2487
2487
|
};
|
|
2488
2488
|
}
|
|
@@ -2546,7 +2546,7 @@ function verdictKey(points, markers, x, y, width) {
|
|
|
2546
2546
|
const has = (test) => points.some(test);
|
|
2547
2547
|
const shape = (s, cls) => (gx, gy) => `<path class="tgc-key-mark ${cls}" d="${markerPath(s, gx + 5, gy - 4, 4)}"/>`;
|
|
2548
2548
|
const sign = (t, cls) => (gx, gy) => text(gx + 1, gy, t, {
|
|
2549
|
-
size:
|
|
2549
|
+
size: 12,
|
|
2550
2550
|
cls
|
|
2551
2551
|
});
|
|
2552
2552
|
const items = [
|
|
@@ -2594,14 +2594,14 @@ function verdictKey(points, markers, x, y, width) {
|
|
|
2594
2594
|
let svg = "", at = x, line = 0;
|
|
2595
2595
|
for (const item of items) {
|
|
2596
2596
|
const lead = item.label === "recorded rows" ? 24 : 16;
|
|
2597
|
-
const w = lead + textWidth(item.label,
|
|
2597
|
+
const w = lead + textWidth(item.label, 12) * 1.1 + 16;
|
|
2598
2598
|
if (at > x && at + w - x > width) {
|
|
2599
2599
|
at = x;
|
|
2600
2600
|
line++;
|
|
2601
2601
|
}
|
|
2602
2602
|
const gy = y + 12 + line * 16;
|
|
2603
2603
|
svg += item.glyph(at, gy) + text(at + lead, gy, item.label, {
|
|
2604
|
-
size:
|
|
2604
|
+
size: 12,
|
|
2605
2605
|
cls: "tgc-ink-muted"
|
|
2606
2606
|
});
|
|
2607
2607
|
at += w;
|
|
@@ -2769,7 +2769,7 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
|
|
|
2769
2769
|
if (strip) marks += text(left - 8, bottom + 16, stripName, {
|
|
2770
2770
|
anchor: "end",
|
|
2771
2771
|
cls: "tgc-ink-muted",
|
|
2772
|
-
size:
|
|
2772
|
+
size: 12
|
|
2773
2773
|
}) + line(left, bottom + strip, right, bottom + strip, "tgc-gridline");
|
|
2774
2774
|
const spread = (inStrip) => {
|
|
2775
2775
|
const byX = /* @__PURE__ */ new Map();
|
|
@@ -2867,7 +2867,7 @@ function plot(rows, o, width, xLabel, yLabel, grid, facts, stripName) {
|
|
|
2867
2867
|
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>`;
|
|
2868
2868
|
}
|
|
2869
2869
|
function axisGutter(ticks, stripName) {
|
|
2870
|
-
return Math.max(64, ...ticks.map((tick) => Math.ceil(textWidth(tick.label, 12, "mono")) + 20), stripName ? Math.ceil(textWidth(stripName,
|
|
2870
|
+
return Math.max(64, ...ticks.map((tick) => Math.ceil(textWidth(tick.label, 12, "mono")) + 20), stripName ? Math.ceil(textWidth(stripName, 12)) + 14 : 0);
|
|
2871
2871
|
}
|
|
2872
2872
|
//#endregion
|
|
2873
2873
|
//#region src/small-multiples.ts
|
|
@@ -3074,7 +3074,7 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3074
3074
|
const ticks = panel.scale === "log" ? decades.length >= 2 ? decades : logged.length >= 2 ? [logged[0], logged.at(-1)] : [domain[0], domain[1]] : [domain[0], domain[1]];
|
|
3075
3075
|
return {
|
|
3076
3076
|
ticks,
|
|
3077
|
-
gutter: Math.max(...ticks.map((v) => Math.ceil(textWidth(yLabel(v),
|
|
3077
|
+
gutter: Math.max(...ticks.map((v) => Math.ceil(textWidth(yLabel(v), 12, "mono")) + 10), 40)
|
|
3078
3078
|
};
|
|
3079
3079
|
};
|
|
3080
3080
|
const narrowestPlot = panelW - 6 - Math.max(...panels.map((p) => gutterOf(p).gutter));
|
|
@@ -3102,7 +3102,7 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3102
3102
|
weight: 600
|
|
3103
3103
|
}));
|
|
3104
3104
|
if (log) body += text(right, oy + 14, "log scale", {
|
|
3105
|
-
size:
|
|
3105
|
+
size: 12,
|
|
3106
3106
|
anchor: "end",
|
|
3107
3107
|
cls: "tgc-ink-muted"
|
|
3108
3108
|
});
|
|
@@ -3117,7 +3117,7 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3117
3117
|
if (log) {
|
|
3118
3118
|
body += line(left, bottom, right, bottom, "tgc-axis");
|
|
3119
3119
|
for (const y of yTicks) body += (y === domain[0] ? "" : line(left, py(y), right, py(y), "tgc-gridline")) + text(left - 6, py(y) + 4, yLabel(y), {
|
|
3120
|
-
size:
|
|
3120
|
+
size: 12,
|
|
3121
3121
|
anchor: "end",
|
|
3122
3122
|
cls: "tgc-ink-muted tgc-num"
|
|
3123
3123
|
});
|
|
@@ -3128,7 +3128,7 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3128
3128
|
].entries()) {
|
|
3129
3129
|
body += line(left, py(y), right, py(y), j === 0 ? "tgc-axis" : "tgc-gridline");
|
|
3130
3130
|
if (j !== 1) body += text(left - 6, py(y) + 4, yLabel(y), {
|
|
3131
|
-
size:
|
|
3131
|
+
size: 12,
|
|
3132
3132
|
anchor: "end",
|
|
3133
3133
|
cls: "tgc-ink-muted tgc-num"
|
|
3134
3134
|
});
|
|
@@ -3136,10 +3136,10 @@ function draw(panels, o, width, xLabel, yLabelOf, marksOf) {
|
|
|
3136
3136
|
body += drawTargets(panel.targets ?? [], py, left, right, yLabel, false) + targetKey(panel.targets ?? [], ox + 2, oy + 30, panelW - 4);
|
|
3137
3137
|
if (labelled.length) body += columnLabels(labelled, px, labelFit.fit, left, right, right + 4, xLabel, 10, bottom + 16);
|
|
3138
3138
|
else body += text(left, bottom + 16, xLabel(o.x.domain[0]), {
|
|
3139
|
-
size:
|
|
3139
|
+
size: 12,
|
|
3140
3140
|
cls: "tgc-ink-muted tgc-num"
|
|
3141
3141
|
}) + text(right, bottom + 16, xLabel(o.x.domain[1]), {
|
|
3142
|
-
size:
|
|
3142
|
+
size: 12,
|
|
3143
3143
|
anchor: "end",
|
|
3144
3144
|
cls: "tgc-ink-muted tgc-num"
|
|
3145
3145
|
});
|
|
@@ -3337,6 +3337,13 @@ function unitDotLayout(groups, width, height, o = {}) {
|
|
|
3337
3337
|
}
|
|
3338
3338
|
//#endregion
|
|
3339
3339
|
//#region src/otlp-trace.ts
|
|
3340
|
+
/** Attributes that carry cached input apart from fresh input (OpenTelemetry GenAI and Anthropic-style names). */
|
|
3341
|
+
const CACHE_KEYS = [
|
|
3342
|
+
"gen_ai.usage.cache_read_input_tokens",
|
|
3343
|
+
"gen_ai.usage.cache_creation_input_tokens",
|
|
3344
|
+
"gen_ai.usage.input_tokens.cached",
|
|
3345
|
+
"tangle.llm.cache_read_tokens"
|
|
3346
|
+
];
|
|
3340
3347
|
/** Price model calls from an OpenAI-compatible model catalog (`data[].pricing.prompt|completion`, USD per token), such as
|
|
3341
3348
|
* the Router's `/v1/models`. Read the catalog at runtime; never copy its numbers. */
|
|
3342
3349
|
function catalogPrice(catalog) {
|
|
@@ -3474,6 +3481,8 @@ function otlpFlowTrace(input, options = {}) {
|
|
|
3474
3481
|
let errors = 0;
|
|
3475
3482
|
let cost = 0;
|
|
3476
3483
|
let unpriced = 0;
|
|
3484
|
+
let priced = 0;
|
|
3485
|
+
let cacheSplit = false;
|
|
3477
3486
|
const offset = (iso) => time(iso) - origin;
|
|
3478
3487
|
const isSpawn = (span) => span.kind === "TOOL" && (attrs(span)["traces.agent.spawn"] === true || /spawn/i.test(str(attrs(span)["gen_ai.tool.name"]) ?? span.name));
|
|
3479
3488
|
const groupOfSpan = (span) => groupOf.get(rootOf(span).span_id);
|
|
@@ -3528,14 +3537,20 @@ function otlpFlowTrace(input, options = {}) {
|
|
|
3528
3537
|
let name = str(a["gen_ai.tool.name"]) ?? span.name;
|
|
3529
3538
|
if (isAgent(span)) name = pick(a, agentKeys) ?? (subagentNumber.has(span.span_id) ? `Subagent ${subagentNumber.get(span.span_id)}` : str(a["service.name"]) ?? str(span.resource?.attributes?.["service.name"]) ?? span.name);
|
|
3530
3539
|
else if (kind === "model" && (span.name === "llm.call" || span.name.startsWith("chat "))) name = "Model call";
|
|
3531
|
-
|
|
3540
|
+
const aggregate = a["cli_bridge.usage.scope"] === "sum_of_children";
|
|
3541
|
+
if (kind === "model" && !aggregate) modelCalls++;
|
|
3532
3542
|
if (kind === "tool" || kind === "handoff") toolCalls++;
|
|
3533
3543
|
if (failed) errors++;
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3544
|
+
if (!aggregate) {
|
|
3545
|
+
promptTokens += prompt ?? 0;
|
|
3546
|
+
completionTokens += completion ?? 0;
|
|
3547
|
+
}
|
|
3548
|
+
const listPrice = kind === "model" && !aggregate && model && options.price && (prompt !== void 0 || completion !== void 0) ? options.price(model, prompt ?? 0, completion ?? 0) : void 0;
|
|
3549
|
+
if (listPrice !== void 0) {
|
|
3550
|
+
cost += listPrice;
|
|
3551
|
+
priced++;
|
|
3552
|
+
if (CACHE_KEYS.some((key) => a[key] !== void 0)) cacheSplit = true;
|
|
3553
|
+
} else if (kind === "model" && !aggregate && (prompt || completion)) unpriced++;
|
|
3539
3554
|
out.push({
|
|
3540
3555
|
id: span.span_id,
|
|
3541
3556
|
...parentId ? { parentId } : {},
|
|
@@ -3562,9 +3577,14 @@ function otlpFlowTrace(input, options = {}) {
|
|
|
3562
3577
|
modelCalls,
|
|
3563
3578
|
errors,
|
|
3564
3579
|
omitted,
|
|
3565
|
-
...options.price &&
|
|
3580
|
+
...options.price && priced > 0 ? {
|
|
3566
3581
|
costUsd: cost,
|
|
3567
|
-
costBasis: "list"
|
|
3582
|
+
costBasis: "list",
|
|
3583
|
+
costCoverage: {
|
|
3584
|
+
priced,
|
|
3585
|
+
total: priced + unpriced
|
|
3586
|
+
},
|
|
3587
|
+
...cacheSplit ? {} : { costUpperBound: true }
|
|
3568
3588
|
} : {}
|
|
3569
3589
|
};
|
|
3570
3590
|
}
|