@tangle-network/charts 0.1.8 → 0.1.10
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 +14 -0
- package/dist/figure.css +16 -0
- package/dist/index.d.ts +22 -3
- package/dist/index.js +165 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,3 +68,17 @@ pnpm --filter @tangle-network/charts build # fails when src/css.generated.ts
|
|
|
68
68
|
## Release
|
|
69
69
|
|
|
70
70
|
Versions publish from the brand release workflow through npm trusted publishing, like `@tangle-network/brand` and `@tangle-network/ui`.
|
|
71
|
+
|
|
72
|
+
### Chart-first vertical rates
|
|
73
|
+
|
|
74
|
+
`rankedRates(rows, { measure: 'pass rate', orientation: 'vertical', order: 'input', identities })` draws vertical bars on a full 0–100% axis.
|
|
75
|
+
`order: 'input'` keeps unranked producer order; omit it to keep the existing ordering behavior.
|
|
76
|
+
Optional `identities` maps setup IDs to `model` and `harness` marks, each with an explicit `{ label, src }`.
|
|
77
|
+
Images must use same-origin absolute paths or HTTPS URLs; the library never infers a provider from a model name.
|
|
78
|
+
Hover or focus a column to read its actual count, eligible interval, known cost and run time.
|
|
79
|
+
Zero has no positive bar, and ineligible estimates keep their count without a bar.
|
|
80
|
+
|
|
81
|
+
Use `renderFigure(figure, noteNumber, { presentation: 'chart' })` for a chart-first surface.
|
|
82
|
+
The plot fills its container; data, reading instructions and method stay in a closed disclosure.
|
|
83
|
+
The default report presentation, underlying table and note links remain available.
|
|
84
|
+
Many columns keep a readable minimum width and scroll horizontally.
|
package/dist/figure.css
CHANGED
|
@@ -293,3 +293,19 @@
|
|
|
293
293
|
break-inside: avoid;
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
|
+
|
|
297
|
+
/* Chart-led surfaces keep supporting text in the native disclosure. */
|
|
298
|
+
.tgc-figure-chart { max-width: none; margin: 0; }
|
|
299
|
+
.tgc-figure-chart .tgc-wide svg { width: 100%; height: auto; }
|
|
300
|
+
.tgc-wide:has(.tgc-vertical), .tgc-narrow:has(.tgc-vertical) { overflow-x: auto; }
|
|
301
|
+
.tgc-vertical { width: 100%; }
|
|
302
|
+
.tgc-narrow .tgc-vertical { max-width: none; }
|
|
303
|
+
.tgc-column-hit { fill: transparent; stroke: transparent; stroke-width: 2; }
|
|
304
|
+
.tgc-column:focus-visible { outline: none; }
|
|
305
|
+
.tgc-column:focus-visible .tgc-column-hit { stroke: var(--chart-accent); }
|
|
306
|
+
.tgc-vertical-bar { fill: var(--chart-accent); }
|
|
307
|
+
/* Identity assets keep their approved colors on a white plate in either theme. */
|
|
308
|
+
.tgc-logo-plate { fill: #fff; stroke: var(--chart-rule); }
|
|
309
|
+
.tgc-column-tip { opacity: 0; pointer-events: none; }
|
|
310
|
+
.tgc-tip-bg { fill: var(--chart-ink); }
|
|
311
|
+
.tgc-tip-ink { fill: var(--chart-canvas); }
|
package/dist/index.d.ts
CHANGED
|
@@ -112,10 +112,11 @@ interface Figure {
|
|
|
112
112
|
lede: string;
|
|
113
113
|
/** How to read the marks. */
|
|
114
114
|
read: string;
|
|
115
|
-
/**
|
|
115
|
+
/** Wide and phone renders; vertical columns retain a readable minimum width. Null means the phone view uses the table. */
|
|
116
116
|
svg: {
|
|
117
117
|
wide: string;
|
|
118
118
|
narrow: string | null;
|
|
119
|
+
interactive?: boolean;
|
|
119
120
|
};
|
|
120
121
|
/** An HTML table with every number the plot shows. */
|
|
121
122
|
table: string;
|
|
@@ -132,7 +133,11 @@ interface Refusal {
|
|
|
132
133
|
export declare function isRefusal(value: Figure | Refusal): value is Refusal;
|
|
133
134
|
//#endregion
|
|
134
135
|
//#region src/block.d.ts
|
|
135
|
-
|
|
136
|
+
interface RenderFigureOptions {
|
|
137
|
+
/** Plot first, with interpretation and source details in a closed disclosure. */
|
|
138
|
+
presentation?: "chart";
|
|
139
|
+
}
|
|
140
|
+
export declare function renderFigure(f: Figure, noteNumber: number, options?: RenderFigureOptions): string;
|
|
136
141
|
/** Numbered notes for `figures`, in the order their blocks were numbered (1-based). */
|
|
137
142
|
export declare function renderNotes(figures: Figure[]): string;
|
|
138
143
|
/** What a page leaves out, and why. */
|
|
@@ -177,7 +182,21 @@ export declare const chartTokensCss: string;
|
|
|
177
182
|
export declare function rateOrder<T extends Pick<RateRow, "rank" | "rate" | "estimate" | "attempts">>(rows: readonly T[]): T[];
|
|
178
183
|
//#endregion
|
|
179
184
|
//#region src/ranked-rates.d.ts
|
|
185
|
+
interface RateIdentity {
|
|
186
|
+
model?: {
|
|
187
|
+
label: string;
|
|
188
|
+
src: string;
|
|
189
|
+
};
|
|
190
|
+
harness?: {
|
|
191
|
+
label: string;
|
|
192
|
+
src: string;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
180
195
|
interface RankedRatesOptions {
|
|
196
|
+
/** Vertical columns on the same full 0–100% scale; horizontal is the default. */
|
|
197
|
+
orientation?: "vertical";
|
|
198
|
+
/** Explicit display identities keyed by setup id; never inferred from labels. */
|
|
199
|
+
identities?: Readonly<Record<string, RateIdentity>>;
|
|
181
200
|
/** What the rate measures, lower case: "solve rate". */
|
|
182
201
|
measure: string;
|
|
183
202
|
/** Preserve unranked input order instead of sorting observed rates. */
|
|
@@ -213,4 +232,4 @@ interface TaskMatrixOptions {
|
|
|
213
232
|
}
|
|
214
233
|
export declare function taskMatrix(tasks: string[], setupsIn: Setup[], cells: MatrixCell[], o?: TaskMatrixOptions): Figure | Refusal;
|
|
215
234
|
//#endregion
|
|
216
|
-
export type { BreakdownInput, BreakdownOptions, ComparisonIntervalsOptions, ComparisonRow, CostBasis, CostFrontierOptions, Estimate, Exclusion, Figure, Interval, IntervalMethod, MatrixCell, RankedRatesOptions, RateRow, Refusal, Setup, TaskMatrixOptions, Tier, TierCounts };
|
|
235
|
+
export type { BreakdownInput, BreakdownOptions, ComparisonIntervalsOptions, ComparisonRow, CostBasis, CostFrontierOptions, Estimate, Exclusion, Figure, Interval, IntervalMethod, MatrixCell, RankedRatesOptions, RateIdentity, RateRow, Refusal, RenderFigureOptions, Setup, TaskMatrixOptions, Tier, TierCounts };
|
package/dist/index.js
CHANGED
|
@@ -311,15 +311,28 @@ function capitalize(text) {
|
|
|
311
311
|
* once per page, numbered, each with a way back to its figure.
|
|
312
312
|
*/
|
|
313
313
|
const slug = (id) => id.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
|
|
314
|
-
function renderFigure(f, noteNumber) {
|
|
314
|
+
function renderFigure(f, noteNumber, options = {}) {
|
|
315
|
+
if (options.presentation !== void 0 && options.presentation !== "chart") throw new Error("renderFigure: unknown presentation.");
|
|
315
316
|
const id = slug(f.id);
|
|
316
317
|
const plotLabel = `${f.finding} ${f.lede}`;
|
|
317
318
|
const narrow = f.svg.narrow ? `<div class="tgc-narrow">${f.svg.narrow}</div>` : `<div class="tgc-narrow tgc-narrow-table">${f.table}</div>`;
|
|
319
|
+
if (options.presentation === "chart") return [
|
|
320
|
+
`<figure class="tgc-figure tgc-figure-chart" id="fig-${id}" aria-label="${esc(f.finding)}">`,
|
|
321
|
+
`<div class="tgc-plot" id="ref-${noteNumber}" role="group" aria-label="${esc(plotLabel)}"><div class="tgc-wide">${f.svg.wide}</div>${narrow}</div>`,
|
|
322
|
+
`<details class="tgc-data"><summary>Data and method</summary>`,
|
|
323
|
+
`<p class="tgc-lede">${esc(f.lede)}</p><p class="tgc-read">${esc(f.read)}</p>`,
|
|
324
|
+
`<p class="tgc-read">${esc([
|
|
325
|
+
f.note.method,
|
|
326
|
+
f.note.n,
|
|
327
|
+
...f.note.exclusions
|
|
328
|
+
].join(" "))}</p>`,
|
|
329
|
+
`<div class="tgc-scroll" role="region" aria-label="${esc(f.finding)} data" tabindex="0">${f.table}</div></details></figure>`
|
|
330
|
+
].join("");
|
|
318
331
|
return [
|
|
319
332
|
`<figure class="tgc-figure" id="fig-${id}" aria-labelledby="fig-${id}-finding">`,
|
|
320
333
|
`<h3 class="tgc-finding" id="fig-${id}-finding">${esc(f.finding)}<sup><a class="tgc-noteref" href="#note-${noteNumber}" id="ref-${noteNumber}" role="doc-noteref" aria-label="Note ${noteNumber}">${noteNumber}</a></sup></h3>`,
|
|
321
334
|
`<p class="tgc-lede">${esc(f.lede)}</p>`,
|
|
322
|
-
`<div class="tgc-plot" role="img" aria-label="${esc(plotLabel)}">`,
|
|
335
|
+
`<div class="tgc-plot" role="${f.svg.interactive ? "group" : "img"}" aria-label="${esc(plotLabel)}">`,
|
|
323
336
|
`<div class="tgc-wide">${f.svg.wide}</div>`,
|
|
324
337
|
narrow,
|
|
325
338
|
`</div>`,
|
|
@@ -776,7 +789,12 @@ function comparisonIntervals(rows, setups, o) {
|
|
|
776
789
|
refused: rows.every((r) => r.interval === null) ? `${rows.length} ${plural(rows.length, "comparison")} listed; none carries an interval.` : `No comparison can be drawn. ${refused.join("; ")}.`
|
|
777
790
|
};
|
|
778
791
|
const clears = drawn.filter(({ row }) => row.interval.lower > row.minimumEffect && (intervalGap(row.exactInterval) !== null || row.exactInterval.lower > row.minimumEffect));
|
|
779
|
-
const
|
|
792
|
+
const minimum = drawn[0].row.minimumEffect;
|
|
793
|
+
const sameMinimum = drawn.every(({ row }) => row.minimumEffect === minimum);
|
|
794
|
+
const pluralSubject = clears.length > 1 && clears.length < drawn.length;
|
|
795
|
+
const difference = drawn.every(({ row }) => row.minimumEffect >= 0) ? "improvement" : "difference";
|
|
796
|
+
const requirement = sameMinimum ? `the required ${difference} of more than ${new Intl.NumberFormat("en-US", { maximumFractionDigits: 12 }).format(minimum * 100)} percentage points in ${o.measure}` : `${pluralSubject ? "their" : "its"} required ${difference}${pluralSubject ? "s" : ""}`;
|
|
797
|
+
const finding = `${clears.length === drawn.length ? drawn.length === 1 ? "The comparison" : "Every comparison" : clears.length === 0 ? "No comparison" : `${clears.length} of ${drawn.length} comparisons`} ${pluralSubject ? "support" : "supports"} ${requirement}.`;
|
|
780
798
|
const pairs = drawn.reduce((s, d) => s + d.row.pairs, 0);
|
|
781
799
|
const lede = `${drawn.length} ${plural(drawn.length, "comparison")} over ${pairs} paired ${plural(pairs, "attempt")}${refused.length ? `; ${refused.length} not drawn` : ""}.`;
|
|
782
800
|
const names = [...new Set(drawn.map((d) => intervalName(d.row.interval)))];
|
|
@@ -966,12 +984,14 @@ function costFrontier(rows, o) {
|
|
|
966
984
|
const cheapest = [...costed].sort((a, b) => cost(a) - cost(b))[0];
|
|
967
985
|
finding = `${cheapest.label} has the lowest cost per pass, ${usd(cost(cheapest))}${est(cheapest)}.`;
|
|
968
986
|
}
|
|
969
|
-
const
|
|
987
|
+
const lowest = Math.min(...costed.map(cost));
|
|
988
|
+
const highest = Math.max(...costed.map(cost));
|
|
989
|
+
const lede = (lowest === highest ? `Cost per pass is ${usd(lowest)}.` : `Cost per pass ranges from ${usd(lowest)} to ${usd(highest)}.`) + (costed.some((row) => row.cost.basis === "estimated") ? " Costs include estimates." : "");
|
|
970
990
|
const read = `Higher ${o.measure} and lower cost per pass are better; on the wide plot, better is up and right.` + (drawLine ? " Filled points sit on the frontier; the line joins them." : "") + (costed.some((r) => r.onFront && r.cost.basis !== "receipts") ? " A hollow accent ring is a frontier point with an estimated cost." : "");
|
|
971
991
|
const intervals = [...new Set(costed.filter((r) => rateIntervalGap(r) === null).map((r) => intervalName(r.interval)))];
|
|
972
992
|
const method = [
|
|
973
993
|
`Cost per pass is model spend over passed attempts, ${[...new Set(costed.map((r) => r.cost.basis))].map((b) => b === "receipts" ? "from router receipts" : b === "estimated" ? "estimated, not billed" : "from an unrecorded source").join(" or ")}.`,
|
|
974
|
-
front.length ?
|
|
994
|
+
front.length ? `A frontier setup has no alternative with both lower cost and higher ${o.measure}.` : "",
|
|
975
995
|
intervals.length ? `Vertical lines: ${intervals.join(", ")} on the ${o.measure}.` : ""
|
|
976
996
|
].filter(Boolean).join(" ");
|
|
977
997
|
const columns = [
|
|
@@ -1243,10 +1263,135 @@ function plot(costed, unplaced, drawLine, o) {
|
|
|
1243
1263
|
//#endregion
|
|
1244
1264
|
//#region src/css.generated.ts
|
|
1245
1265
|
/** The figure block and mark styles (src/figure.css), for documents that inline their CSS. */
|
|
1246
|
-
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: 16px;\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: 22px;\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: 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: 1.5;\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";
|
|
1266
|
+
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: 16px;\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: 22px;\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: 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: 1.5;\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";
|
|
1247
1267
|
/** --chart-* resolved per theme, for documents without brand tokens: light, dark by preference or [data-theme="dark"], light in print. */
|
|
1248
1268
|
const chartTokensCss = ":root{--chart-ink:#2d2d2d;--chart-ink-muted:#686868;--chart-muted:#787878;--chart-grid:#f2f2f2;--chart-rule:#d0d0d0;--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-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:#353535;--chart-rule:#4c4c4c;--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-canvas:#161616;color-scheme:dark}}\n:root[data-theme=\"dark\"]{--chart-ink:#e6e6e6;--chart-ink-muted:#a5a5a5;--chart-muted:#8e8e8e;--chart-grid:#353535;--chart-rule:#4c4c4c;--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-canvas:#161616;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:#f2f2f2;--chart-rule:#d0d0d0;--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-canvas:#ffffff;color-scheme:light}}";
|
|
1249
1269
|
//#endregion
|
|
1270
|
+
//#region src/vertical-rates.ts
|
|
1271
|
+
/** Vertical rate geometry; values, eligibility and ordering stay with rankedRates. */
|
|
1272
|
+
function identityGap(rows, identities) {
|
|
1273
|
+
if (!identities) return null;
|
|
1274
|
+
for (const [id, identity] of Object.entries(identities)) {
|
|
1275
|
+
if (!rows.some((row) => row.id === id)) return `Identity names unknown setup ${id}.`;
|
|
1276
|
+
if (!identity || typeof identity !== "object") return `Invalid display identity for ${id}.`;
|
|
1277
|
+
for (const mark of [identity.model, identity.harness]) {
|
|
1278
|
+
if (!mark) continue;
|
|
1279
|
+
if (typeof mark.label !== "string" || !mark.label.trim() || typeof mark.src !== "string") return `Invalid display identity for ${id}.`;
|
|
1280
|
+
const local = /^\/(?!\/)[^\s<>"']+$/.test(mark.src);
|
|
1281
|
+
const https = /^https:\/\/[a-z0-9.-]+(?::\d+)?\/[^\s<>"']*$/i.test(mark.src);
|
|
1282
|
+
if (!local && !https) return `Invalid identity image for ${id}.`;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
function image(mark, x, y, size) {
|
|
1288
|
+
return `<image href="${esc(mark.src)}" x="${x}" y="${y}" width="${size}" height="${size}" preserveAspectRatio="xMidYMid meet"><title>${esc(mark.label)}</title></image>`;
|
|
1289
|
+
}
|
|
1290
|
+
const rateLabel = (rate) => `${Number((rate * 100).toFixed(1))}%`;
|
|
1291
|
+
function verticalRates(rows, viewport, ranked, o) {
|
|
1292
|
+
const width = Math.max(viewport, 64 + rows.length * 112);
|
|
1293
|
+
const left = 48;
|
|
1294
|
+
const top = 42;
|
|
1295
|
+
const bottom = 302;
|
|
1296
|
+
const band = 260;
|
|
1297
|
+
const slot = 112;
|
|
1298
|
+
const barWidth = Math.min(viewport < 720 ? 44 : 64, slot * .55);
|
|
1299
|
+
const labelWidth = Math.min(100, 170);
|
|
1300
|
+
const labels = rows.map((row) => wrap(o.identities?.[row.id]?.model?.label ?? row.label, labelWidth, 14, "sans", 8));
|
|
1301
|
+
const labelLines = Math.max(...labels.map((label) => label.lines.length));
|
|
1302
|
+
const height = 408 + Math.max(0, labelLines - 1) * 18;
|
|
1303
|
+
let grid = "";
|
|
1304
|
+
for (const tick of [
|
|
1305
|
+
0,
|
|
1306
|
+
.2,
|
|
1307
|
+
.4,
|
|
1308
|
+
.6,
|
|
1309
|
+
.8,
|
|
1310
|
+
1
|
|
1311
|
+
]) {
|
|
1312
|
+
const y = bottom - tick * band;
|
|
1313
|
+
grid += `<line class="${tick === 0 ? "tgc-axis" : "tgc-gridline"}" x1="${left}" y1="${y}" x2="100%" y2="${y}"/>`;
|
|
1314
|
+
grid += text(38, y + 4, pct(tick), {
|
|
1315
|
+
cls: "tgc-ink-muted tgc-num",
|
|
1316
|
+
anchor: "end"
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
grid += text(left, 18, o.measure, { cls: "tgc-ink-muted" });
|
|
1320
|
+
let columns = "";
|
|
1321
|
+
let tips = "";
|
|
1322
|
+
let rules = "";
|
|
1323
|
+
for (const [i, row] of rows.entries()) {
|
|
1324
|
+
const x = left + slot * (i + .5);
|
|
1325
|
+
const y = bottom - row.rate * band;
|
|
1326
|
+
const identity = o.identities?.[row.id];
|
|
1327
|
+
const facts = [
|
|
1328
|
+
row.label,
|
|
1329
|
+
`${kn(row.solved, row.attempts)} passed`,
|
|
1330
|
+
drawable(row.estimate) ? rateLabel(row.rate) : estimateNote(row.estimate, row.attempts)
|
|
1331
|
+
];
|
|
1332
|
+
if (row.interval && rateIntervalGap(row) === null) facts.push(`${intervalName(row.interval)}: ${rateLabel(row.interval.lower)}–${rateLabel(row.interval.upper)}`);
|
|
1333
|
+
if (row.solved > 0 && perPass(row.cost) !== null) facts.push(`${costText(row.cost, row.solved)} per pass`);
|
|
1334
|
+
if (row.medianWallMs !== null) facts.push(`${duration(row.medianWallMs)} median run`);
|
|
1335
|
+
let marks = rect(x - slot / 2 + 3, 18, 106, height - top + 12, "tgc-column-hit", "rx=\"6\"");
|
|
1336
|
+
if (drawable(row.estimate)) {
|
|
1337
|
+
marks += rect(x - barWidth / 2, y, barWidth, row.rate * band, "tgc-vertical-bar", "rx=\"5\"");
|
|
1338
|
+
if (row.interval && rateIntervalGap(row) === null) {
|
|
1339
|
+
const lo = bottom - row.interval.lower * band;
|
|
1340
|
+
const hi = bottom - row.interval.upper * band;
|
|
1341
|
+
marks += line(x, lo, x, hi, "tgc-whisker");
|
|
1342
|
+
marks += line(x - 5, lo, x + 5, lo, "tgc-whisker") + line(x - 5, hi, x + 5, hi, "tgc-whisker");
|
|
1343
|
+
}
|
|
1344
|
+
marks += text(x + barWidth / 2 + 7, Math.max(34, y - 9), rateLabel(row.rate), {
|
|
1345
|
+
size: 15,
|
|
1346
|
+
weight: 600,
|
|
1347
|
+
anchor: "middle",
|
|
1348
|
+
cls: "tgc-ink tgc-num"
|
|
1349
|
+
});
|
|
1350
|
+
} else marks += text(x, 290, estimateNote(row.estimate, row.attempts), {
|
|
1351
|
+
anchor: "middle",
|
|
1352
|
+
cls: "tgc-ink-muted"
|
|
1353
|
+
});
|
|
1354
|
+
if (row.tiers && row.attempts > 0 && drawable(row.estimate)) {
|
|
1355
|
+
let cursor = x - barWidth / 2;
|
|
1356
|
+
for (const tier of TIERS_BEST_FIRST) {
|
|
1357
|
+
const segment = row.tiers[tier] / row.attempts * barWidth;
|
|
1358
|
+
if (segment > 0) marks += rect(cursor, 311, segment, 5, tier === "F" ? "tgc-open" : `tgc-tier-${tier.toLowerCase()}`);
|
|
1359
|
+
cursor += segment;
|
|
1360
|
+
}
|
|
1361
|
+
if (o.passTier) {
|
|
1362
|
+
const count = TIERS_BEST_FIRST.filter((tier) => tierAtLeast(tier, o.passTier)).reduce((sum, tier) => sum + row.tiers[tier], 0);
|
|
1363
|
+
const tx = x - barWidth / 2 + count / row.attempts * barWidth;
|
|
1364
|
+
marks += line(tx, 308, tx, 319, "tgc-tick");
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (identity?.model) {
|
|
1368
|
+
marks += rect(x - 18, 325, 36, 36, "tgc-logo-plate", "rx=\"6\"");
|
|
1369
|
+
marks += image(identity.model, x - 16, 327, 32);
|
|
1370
|
+
}
|
|
1371
|
+
if (identity?.harness) {
|
|
1372
|
+
marks += rect(x + 3, 345, 22, 22, "tgc-logo-plate", "rx=\"5\"");
|
|
1373
|
+
marks += image(identity.harness, x + 6, 348, 16);
|
|
1374
|
+
}
|
|
1375
|
+
marks += textBlock(labels[i].lines, x, 386, 18, "class=\"tgc-ink\" font-size=\"14\" text-anchor=\"middle\"");
|
|
1376
|
+
if (identity?.harness) marks += text(x, 405 + (labels[i].lines.length - 1) * 18, identity.harness.label, {
|
|
1377
|
+
anchor: "middle",
|
|
1378
|
+
cls: "tgc-ink-muted"
|
|
1379
|
+
});
|
|
1380
|
+
if (ranked) marks += text(x, 33, `Rank ${row.rank}`, {
|
|
1381
|
+
anchor: "middle",
|
|
1382
|
+
cls: "tgc-ink-muted tgc-num"
|
|
1383
|
+
});
|
|
1384
|
+
columns += `<g class="tgc-column" data-column="${i}" tabindex="0" role="img" aria-label="${esc(facts.join(" · "))}"><title>${esc(facts.join(" · "))}</title>${marks}</g>`;
|
|
1385
|
+
const tipWidth = Math.min(280, width - 24);
|
|
1386
|
+
const tipX = Math.max(12, Math.min(width - tipWidth - 12, x - tipWidth / 2));
|
|
1387
|
+
const tipLines = facts.flatMap((fact) => wrap(fact, tipWidth - 24, 12, "sans", 8).lines);
|
|
1388
|
+
const tipHeight = tipLines.length * 17 + 24;
|
|
1389
|
+
tips += `<g class="tgc-column-tip" data-tip="${i}" aria-hidden="true">${rect(tipX, top, tipWidth, tipHeight, "tgc-tip-bg", "rx=\"7\"")}${textBlock(tipLines, tipX + 12, 62, 17, "class=\"tgc-tip-ink\" font-size=\"12\"")}</g>`;
|
|
1390
|
+
rules += `.tgc-vertical:has(.tgc-column[data-column="${i}"]:hover) [data-tip="${i}"],.tgc-vertical:not(:has(.tgc-column:hover)):has(.tgc-column[data-column="${i}"]:focus-visible) [data-tip="${i}"]{opacity:1}`;
|
|
1391
|
+
}
|
|
1392
|
+
return `<svg class="tgc-svg tgc-vertical" xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" style="height:${height}px;min-width:${64 + rows.length * 112}px" role="group" aria-label="${esc(o.measure)} by setup"><style>${rules}</style>${grid}${columns}${tips}</svg>`;
|
|
1393
|
+
}
|
|
1394
|
+
//#endregion
|
|
1250
1395
|
//#region src/ranked-rates.ts
|
|
1251
1396
|
/**
|
|
1252
1397
|
* `rankedRates`: which setup passes most, and how uncertain is each rate.
|
|
@@ -1273,6 +1418,15 @@ const TICKS = [
|
|
|
1273
1418
|
];
|
|
1274
1419
|
function rankedRates(input, o) {
|
|
1275
1420
|
const id = o.id ?? "ranked-rates";
|
|
1421
|
+
if (o.orientation !== void 0 && o.orientation !== "vertical") return {
|
|
1422
|
+
id,
|
|
1423
|
+
refused: "Unknown bar orientation."
|
|
1424
|
+
};
|
|
1425
|
+
const identityProblem = identityGap(input, o.identities);
|
|
1426
|
+
if (identityProblem) return {
|
|
1427
|
+
id,
|
|
1428
|
+
refused: identityProblem
|
|
1429
|
+
};
|
|
1276
1430
|
const noun = o.nouns ?? {
|
|
1277
1431
|
attempt: "attempt",
|
|
1278
1432
|
setup: "setup"
|
|
@@ -1397,7 +1551,11 @@ function rankedRates(input, o) {
|
|
|
1397
1551
|
finding,
|
|
1398
1552
|
lede,
|
|
1399
1553
|
read,
|
|
1400
|
-
svg: {
|
|
1554
|
+
svg: o.orientation === "vertical" ? {
|
|
1555
|
+
wide: verticalRates(rows, 720, layout.ranked, o),
|
|
1556
|
+
narrow: verticalRates(rows, 358, layout.ranked, o),
|
|
1557
|
+
interactive: true
|
|
1558
|
+
} : {
|
|
1401
1559
|
wide: wide(rows, layout, noun, o),
|
|
1402
1560
|
narrow: narrow(rows, layout, o)
|
|
1403
1561
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/charts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Benchmark figures as static SVG and HTML tables: ranked rates, cost frontier, comparison intervals, task matrix and breakdown. Draws from producer data and refuses what it cannot draw honestly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|