@tangle-network/charts 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -1
- package/dist/figure.css +7 -2
- package/dist/index.js +18 -12
- package/dist/react/index.d.ts +84 -0
- package/dist/react/index.js +538 -0
- package/package.json +15 -2
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Malformed rates, intervals, matrix counts, and breakdown counts return a refusal
|
|
|
27
27
|
Unmeasured and flagged matrix attempts keep distinct marks and table counts, even without tier grades.
|
|
28
28
|
An interval whisker is omitted when its bounds exclude the row's rate.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
The static root has no runtime dependencies and uses no DOM, so it runs in a Worker, a build step or a browser. The separate opt-in `@tangle-network/charts/react` entrypoint requires React; it is never re-exported by the root.
|
|
31
31
|
|
|
32
32
|
## Use
|
|
33
33
|
|
|
@@ -129,3 +129,77 @@ The complete data table remains available to assistive technology.
|
|
|
129
129
|
This presentation has no visible finding, method text, disclosure or table control.
|
|
130
130
|
Vertical plot axes use larger tick labels.
|
|
131
131
|
The existing report and chart presentations keep their output and paper behavior.
|
|
132
|
+
|
|
133
|
+
Plot presentation uses labels of at least 14px and numeric axes of 15px.
|
|
134
|
+
Dedicated phone SVGs retain their intended width inside a keyboard-accessible scroll region.
|
|
135
|
+
Wide plots reserve side gutters so enlarged labels remain visible.
|
|
136
|
+
These typography rules do not apply to report or chart presentation.
|
|
137
|
+
|
|
138
|
+
## Optional React renderers
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { useState } from "react";
|
|
142
|
+
import { Sparkline, StackedBarChart } from "@tangle-network/charts/react";
|
|
143
|
+
|
|
144
|
+
export function RecordedUnits() {
|
|
145
|
+
const [selectedBucketId, onSelectionChange] = useState<string | null>(null);
|
|
146
|
+
return <>
|
|
147
|
+
<Sparkline label="Completion delta" values={[-2, 0, null, 3]} format={String} />
|
|
148
|
+
<StackedBarChart
|
|
149
|
+
label="Recorded units"
|
|
150
|
+
series={[{ id: "a", label: "Series A", color: "var(--chart-accent)" }]}
|
|
151
|
+
buckets={[
|
|
152
|
+
{ id: "first", label: "First window", total: 3, segments: [{ seriesId: "a", value: 3 }] },
|
|
153
|
+
{ id: "missing", label: "Second window", total: null, segments: [{ seriesId: "a", value: null }] },
|
|
154
|
+
]}
|
|
155
|
+
maxValue={10}
|
|
156
|
+
formatValue={(value) => `${value} units`}
|
|
157
|
+
selectedBucketId={selectedBucketId}
|
|
158
|
+
onSelectionChange={onSelectionChange}
|
|
159
|
+
/>
|
|
160
|
+
</>;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Install React 18 or 19 in the consuming app. It is an **optional peer** so static-only consumers need neither React nor its types. There is no React DOM, chart framework or UI-package runtime dependency. The renderers need no generated stylesheet; `figure.css` remains exclusively the unchanged static-figure stylesheet. Supply application font/color styles and readable series colors. Apps with server/client component boundaries mark their interactive wrapper as a client component.
|
|
165
|
+
|
|
166
|
+
**Different scales, on purpose.** `Sparkline` ports Agent App's observed-extent scale; it does not implicitly include zero. Equal readings sit at mid-height, a single reading is a point, null/nonfinite samples retain their sample index and break the line, and finite negative readings are supported. Width/height props control its viewBox. Its accessible name describes the count, gaps, range and direction; the SVG description contains each sample's formatted value. It is not animated and has no selection interaction. The default `en-US` formatter retains Agent App's two-decimal behavior; pass `format={String}` or a domain-specific formatter when exact precision is important.
|
|
167
|
+
|
|
168
|
+
`StackedBarChart` always uses the caller's **explicit `[0, maxValue]` domain**. It accepts already-grouped buckets with unique IDs, labels, supplied totals and ordered segments referencing declared series IDs. It never fetches, sorts, groups, calculates billed amounts, chooses a currency, maps products/categories, or invents a maximum. Input bucket order is the sample axis, not an inferred date axis; supply every intended bucket, including gaps. Omitted segments are not synthesized as zero.
|
|
169
|
+
|
|
170
|
+
A null/nonfinite total or segment makes the whole stack a gap rather than a misleading complete bar. The known values remain in the readout and data table. Zero remains measured zero and draws no positive segment. As in the handoff, positive subpixel segments have a one-pixel visual minimum; their reported values and the total are not changed. Negative or out-of-domain finite values, duplicate/unknown identities, and inconsistent complete totals throw `RangeError` rather than clamp or silently aggregate. Nonnegative stacks do not model credits or diverging signed stacks. The existing static `Refusal` API is unchanged; it is not repurposed as React component state.
|
|
171
|
+
|
|
172
|
+
Selection is controlled by bucket ID. Hover, focus, click, Enter, Space and touch activation select rather than toggle; mouse leave, blur and Escape dismiss. A removed selected ID shows no stale readout, and reordering never selects another bucket by index. The stabilized SVG keeps full-height hit targets and highlights the actual stack. A named keyboard-scrollable region contains its fluid SVG with a 400px minimum. A cursor-adjacent tooltip repeats the selected values for mouse and pen users. It flips at viewport edges and uses a native manual popover to escape transformed or clipped panels. Browsers without popover support use a fixed-position fallback, which a transformed ancestor can constrain. The selected breakdown remains in flow and live-announced; every bucket also carries its formatted values in its accessible name, and a native “View data” disclosure contains the full table. No Tailwind build or hover-only tooltip is required to read the values.
|
|
173
|
+
|
|
174
|
+
**Provenance and extraction boundary.** Sparkline comes from `tangle-network/agent-app`, `src/web-react/sparkline.tsx` and its tests at `a8947683a050394d13848bb7e4cbbe7cf08d208e`. Stacked SVG rendering and select-never-toggle regressions come from merged ADC [PR #8746](https://github.com/tangle-network/agent-dev-container/pull/8746), `products/platform/web/src/client/components/UsageChartSvg.tsx` and `UsageChart.test.tsx` at `a1c3958fd73a3894d4515f9dc476dae9ace30ce0`. Platform's aggregation, currency/product/category functions, legend composition and portal-tooltip positioning are **not** copied. The persistent readout and native pointer popover avoid a new React DOM peer. This is not a new universal usage-chart API, a zero-baseline replacement for SpendCard's sparkline, or a consumer migration; Platform and Agent App are not edited.
|
|
175
|
+
|
|
176
|
+
### React and package-boundary checks
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
pnpm exec vitest run packages/charts/src/react/sparkline.test.tsx packages/charts/src/react/stacked-bar-chart.test.tsx
|
|
180
|
+
pnpm exec vitest run packages/charts/src/package-boundary.test.ts
|
|
181
|
+
pnpm exec vitest run packages/charts
|
|
182
|
+
pnpm --filter @tangle-network/charts exec tsc --noEmit
|
|
183
|
+
pnpm --filter @tangle-network/charts build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The package-boundary test builds and packs Charts, installs the tarball in an OS-temporary consumer with a normal offline npm install (no peer-omission flags), asserts React/React DOM/React types cannot resolve, and executes the static root without DOM globals. It compares root exports, static SVG/tables, Refusals, report/chart/plot HTML and CSS with the untouched source implementation, then compiles a static-only consumer with `lib: ["ES2022"]`, `types: []` and `skipLibCheck: false`. Only afterward does it supply the workspace React peer/types to verify the packed React runtime and declarations. It runs under the existing root `pnpm test`; no CI workflow or shared UI smoke-script changes are needed.
|
|
187
|
+
|
|
188
|
+
The component tests cover gap/zero/one/equal/negative cases, controlled identity, keyboard/touch selection, nonvisual values and resize contracts. The jsdom resize check verifies fluid/minimum-width attributes and preserved selection, **not browser-computed layout**; The retained packed-browser fixture checks Chromium at 1100px and 390px, keyboard selection, emulated touch, light/dark Brand tokens, reduced motion, and top-layer positioning inside a transformed, paint-contained panel. It passes with React 18 and React 19; the React 18 consumer also compiles the packed declarations with React 18 types. Physical touch hardware and screen-reader audio remain unchecked.
|
|
189
|
+
|
|
190
|
+
### Packed browser proof
|
|
191
|
+
|
|
192
|
+
Build Charts, then run the reusable fixture from the repository root.
|
|
193
|
+
Pass a tarball path to test a published artifact after downloading it with npm pack.
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
pnpm --filter @tangle-network/charts build
|
|
197
|
+
PORT=46489 node scripts/charts-browser-smoke.mjs /absolute/path/to/charts.tgz
|
|
198
|
+
python3 scripts/charts-browser-proof.py http://127.0.0.1:46489 /tmp/charts-proof
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Set `CHARTS_REACT_VERSION=18` to install and check an isolated React 18 runtime and type consumer.
|
|
202
|
+
The default fixture uses the installed workspace React peer and bundles only the packed Charts export.
|
|
203
|
+
The fixture imports Brand token CSS and creates a transformed, paint-contained panel.
|
|
204
|
+
It needs Python Playwright and Chromium at `/snap/bin/chromium`.
|
|
205
|
+
Receipts and screenshots are in [the retained proof](../../docs/evidence/charts-react-189/README.md).
|
package/dist/figure.css
CHANGED
|
@@ -322,8 +322,13 @@
|
|
|
322
322
|
|
|
323
323
|
/* Plot presentation keeps the same complete data available to assistive technology. */
|
|
324
324
|
.tgc-accessible-data { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
325
|
-
.tgc-figure-plot .tgc-
|
|
325
|
+
.tgc-figure-plot .tgc-wide svg { box-sizing: content-box; width: calc(100% - 48px); padding: 0 24px; }
|
|
326
|
+
.tgc-figure-plot svg text { font-size: 14px; }
|
|
327
|
+
.tgc-figure-plot svg text.tgc-num { font-size: 15px; }
|
|
328
|
+
/* Larger tooltip text needs padding outside the original report backdrop. */
|
|
329
|
+
.tgc-figure-plot .tgc-tip-bg { stroke: var(--chart-ink); stroke-width: 24px; }
|
|
330
|
+
.tgc-figure-plot .tgc-phone-plot svg { min-width: var(--tgc-phone-width); }
|
|
326
331
|
.tgc-figure-plot .tgc-wide, .tgc-figure-plot .tgc-narrow { overflow-x: auto; }
|
|
327
332
|
|
|
328
333
|
.tgc-figure-plot .tgc-wide-fallback svg { min-width: var(--tgc-fallback-width); }
|
|
329
|
-
.tgc-wide-fallback:focus-visible { outline: 2px solid var(--chart-accent); outline-offset: 2px; }
|
|
334
|
+
.tgc-phone-plot:focus-visible, .tgc-wide-fallback:focus-visible { outline: 2px solid var(--chart-accent); outline-offset: 2px; }
|
package/dist/index.js
CHANGED
|
@@ -358,7 +358,7 @@ function renderFigure(f, noteNumber, options = {}) {
|
|
|
358
358
|
const plotLabel = `${f.finding} ${f.lede}`;
|
|
359
359
|
const narrow = f.svg.narrow ? `<div class="tgc-narrow">${f.svg.narrow}</div>` : `<div class="tgc-narrow tgc-narrow-table">${f.table}</div>`;
|
|
360
360
|
if (options.presentation === "plot") {
|
|
361
|
-
const phonePlot = f.svg.narrow ? `<div class="tgc-narrow">${f.svg.narrow}</div>` : `<div class="tgc-narrow tgc-wide-fallback" role="region" aria-label="${esc(f.finding)} chart" tabindex="0" style="--tgc-fallback-width:720px">${f.svg.wide}</div>`;
|
|
361
|
+
const phonePlot = f.svg.narrow ? `<div class="tgc-narrow tgc-phone-plot" role="region" aria-label="${esc(f.finding)} chart" tabindex="0" style="--tgc-phone-width:358px">${f.svg.narrow}</div>` : `<div class="tgc-narrow tgc-wide-fallback" role="region" aria-label="${esc(f.finding)} chart" tabindex="0" style="--tgc-fallback-width:720px">${f.svg.wide}</div>`;
|
|
362
362
|
return [
|
|
363
363
|
`<figure class="tgc-figure tgc-figure-chart tgc-figure-plot" id="fig-${id}" aria-label="${esc(f.finding)}">`,
|
|
364
364
|
`<div class="tgc-plot" role="${f.svg.interactive ? "group" : "img"}" aria-label="${esc(plotLabel)}"><div class="tgc-wide">${f.svg.wide}</div>${phonePlot}</div>`,
|
|
@@ -1271,7 +1271,7 @@ function plot$1(costed, unplaced, drawLine, o) {
|
|
|
1271
1271
|
//#endregion
|
|
1272
1272
|
//#region src/css.generated.ts
|
|
1273
1273
|
/** The figure block and mark styles (src/figure.css), for documents that inline their CSS. */
|
|
1274
|
-
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\n.tgc-series-line { fill: none; stroke: var(--chart-accent); stroke-width: 2; stroke-linejoin: round; }\n.tgc-series-point { fill: var(--chart-accent); stroke: var(--chart-canvas); stroke-width: 1; }\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-
|
|
1274
|
+
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\n.tgc-series-line { fill: none; stroke: var(--chart-accent); stroke-width: 2; stroke-linejoin: round; }\n.tgc-series-point { fill: var(--chart-accent); stroke: var(--chart-canvas); stroke-width: 1; }\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";
|
|
1275
1275
|
/** --chart-* resolved per theme, for documents without brand tokens: light, dark by preference or [data-theme="dark"], light in print. */
|
|
1276
1276
|
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}}";
|
|
1277
1277
|
//#endregion
|
|
@@ -2345,6 +2345,10 @@ function timeSeries(rows, o) {
|
|
|
2345
2345
|
}
|
|
2346
2346
|
const xLabel = (x) => o.formatX ? o.formatX(x) : String(x);
|
|
2347
2347
|
const yLabel = (y) => scalarLabel(y, o.y.unit, o.formatY);
|
|
2348
|
+
if (axisGutter(Array.from({ length: 5 }, (_, i) => o.y.domain[0] + i / 4 * (o.y.domain[1] - o.y.domain[0])), yLabel) >= 270) return {
|
|
2349
|
+
id,
|
|
2350
|
+
refused: "Formatted y-axis labels leave no room for the phone chart. Use a shorter formatter or unit."
|
|
2351
|
+
};
|
|
2348
2352
|
const flat = rows.flatMap((row) => row.points.map((point) => ({
|
|
2349
2353
|
row,
|
|
2350
2354
|
point
|
|
@@ -2397,18 +2401,17 @@ function timeSeries(rows, o) {
|
|
|
2397
2401
|
};
|
|
2398
2402
|
}
|
|
2399
2403
|
function plot(rows, o, width, xLabel, yLabel, facts) {
|
|
2400
|
-
const
|
|
2404
|
+
const ticks = Array.from({ length: 5 }, (_, i) => o.y.domain[0] + i / 4 * (o.y.domain[1] - o.y.domain[0]));
|
|
2405
|
+
const left = axisGutter(ticks, yLabel);
|
|
2406
|
+
const right = width - 24, top = 36, bottom = 260;
|
|
2401
2407
|
const px = (x) => left + (x - o.x.domain[0]) / (o.x.domain[1] - o.x.domain[0]) * (right - left);
|
|
2402
2408
|
const py = (y) => bottom - (y - o.y.domain[0]) / (o.y.domain[1] - o.y.domain[0]) * 224;
|
|
2403
2409
|
let marks = text(left, 18, o.y.label, { cls: "tgc-ink-muted" });
|
|
2404
|
-
for (
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
size: 11
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2410
|
+
for (const [i, y] of ticks.entries()) marks += line(left, py(y), right, py(y), i === 0 ? "tgc-axis" : "tgc-gridline") + text(left - 8, py(y) + 4, yLabel(y), {
|
|
2411
|
+
anchor: "end",
|
|
2412
|
+
cls: "tgc-ink-muted tgc-num",
|
|
2413
|
+
size: 12
|
|
2414
|
+
});
|
|
2412
2415
|
const xs = [...new Set(rows.flatMap((row) => row.points.map((point) => point.x)))].sort((a, b) => a - b);
|
|
2413
2416
|
const tickStep = Math.max(1, Math.ceil(xs.length / (width < 720 ? 5 : 10)));
|
|
2414
2417
|
for (const [i, x] of xs.entries()) if (i % tickStep === 0 || i === xs.length - 1) marks += text(px(x), 280, xLabel(x), {
|
|
@@ -2448,10 +2451,13 @@ function plot(rows, o, width, xLabel, yLabel, facts) {
|
|
|
2448
2451
|
}
|
|
2449
2452
|
flush();
|
|
2450
2453
|
const label = wrap(row.label, right - left - 32, 12, "sans", 8);
|
|
2451
|
-
marks += `<g style="stroke-dasharray:${dash}">${line(left, legendY - 4,
|
|
2454
|
+
marks += `<g style="stroke-dasharray:${dash}">${line(left, legendY - 4, left + 24, legendY - 4, "tgc-series-line")}</g>` + textBlock(label.lines, left + 32, legendY, 16, "class=\"tgc-ink\" font-size=\"12\"");
|
|
2452
2455
|
legendY += label.lines.length * 16 + 8;
|
|
2453
2456
|
}
|
|
2454
2457
|
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}${points}${tips}</svg>`;
|
|
2455
2458
|
}
|
|
2459
|
+
function axisGutter(ticks, label) {
|
|
2460
|
+
return Math.max(64, ...ticks.map((value) => Math.ceil(textWidth(label(value), 12, "mono")) + 20));
|
|
2461
|
+
}
|
|
2456
2462
|
//#endregion
|
|
2457
2463
|
export { breakdown, chartTokensCss, comparisonIntervals, costFrontier, figureCss, isRefusal, metricBars, rankedRates, rateOrder, renderFigure, renderNotes, renderRefusals, taskMatrix, timeSeries };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
//#region src/react/sparkline.d.ts
|
|
3
|
+
type SparklineDirection = "rising" | "falling" | "flat";
|
|
4
|
+
type SparklineValues = readonly (number | null)[];
|
|
5
|
+
interface SparklinePoint {
|
|
6
|
+
readonly x: number;
|
|
7
|
+
readonly y: number;
|
|
8
|
+
}
|
|
9
|
+
interface SparklineGeometry {
|
|
10
|
+
readonly readings: readonly number[];
|
|
11
|
+
readonly points: readonly SparklinePoint[];
|
|
12
|
+
/** One run per set of consecutive, finite samples. */
|
|
13
|
+
readonly segments: readonly (readonly SparklinePoint[])[];
|
|
14
|
+
readonly gaps: number;
|
|
15
|
+
readonly min: number;
|
|
16
|
+
readonly max: number;
|
|
17
|
+
readonly first: number;
|
|
18
|
+
readonly last: number;
|
|
19
|
+
readonly direction: SparklineDirection;
|
|
20
|
+
}
|
|
21
|
+
interface SparklineGeometryOptions {
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
inset?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_SPARKLINE_WIDTH = 96;
|
|
27
|
+
export declare const DEFAULT_SPARKLINE_HEIGHT = 24;
|
|
28
|
+
export declare const DEFAULT_SPARKLINE_LABEL = "Trend";
|
|
29
|
+
export declare const DEFAULT_SPARKLINE_EMPTY_LABEL = "No history yet";
|
|
30
|
+
export declare const DEFAULT_SPARKLINE_UNAVAILABLE_LABEL = "No readings available";
|
|
31
|
+
export declare function formatSparklineValue(value: number): string;
|
|
32
|
+
export declare function sparklineReadings(values: SparklineValues): number[];
|
|
33
|
+
export declare function sparklineGeometry(values: SparklineValues, { width, height, inset }?: SparklineGeometryOptions): SparklineGeometry;
|
|
34
|
+
export declare function sparklinePointsAttribute(points: readonly SparklinePoint[]): string;
|
|
35
|
+
interface SparklineLabelOptions {
|
|
36
|
+
label?: string;
|
|
37
|
+
format?: (value: number) => string;
|
|
38
|
+
}
|
|
39
|
+
export declare function sparklineLabel(values: SparklineValues, { label, format }?: SparklineLabelOptions): string;
|
|
40
|
+
interface SparklineProps extends SparklineLabelOptions {
|
|
41
|
+
values: SparklineValues;
|
|
42
|
+
width?: number;
|
|
43
|
+
height?: number;
|
|
44
|
+
emptyLabel?: string;
|
|
45
|
+
unavailableLabel?: string;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function Sparkline({ values, label, format, width, height, emptyLabel, unavailableLabel, className }: SparklineProps): ReactElement;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/react/stacked-bar-chart.d.ts
|
|
51
|
+
interface StackedBarSeries {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly label: string;
|
|
54
|
+
/** Caller-owned CSS color, e.g. a brand token. Never inferred from an ID. */
|
|
55
|
+
readonly color: string;
|
|
56
|
+
}
|
|
57
|
+
interface StackedBarSegment {
|
|
58
|
+
readonly seriesId: string;
|
|
59
|
+
readonly value: number | null;
|
|
60
|
+
}
|
|
61
|
+
interface StackedBarBucket {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly label: string;
|
|
64
|
+
/** Pre-aggregated by the caller. Null/nonfinite readings are gaps, not zero. */
|
|
65
|
+
readonly total: number | null;
|
|
66
|
+
/** Stack order is input order. Charts neither groups nor sorts. */
|
|
67
|
+
readonly segments: readonly StackedBarSegment[];
|
|
68
|
+
}
|
|
69
|
+
interface StackedBarChartProps {
|
|
70
|
+
label: string;
|
|
71
|
+
series: readonly StackedBarSeries[];
|
|
72
|
+
buckets: readonly StackedBarBucket[];
|
|
73
|
+
/** Explicit [0, maxValue] domain. Zero is valid only for zero-valued data. */
|
|
74
|
+
maxValue: number;
|
|
75
|
+
formatValue: (value: number) => string;
|
|
76
|
+
selectedBucketId: string | null;
|
|
77
|
+
onSelectionChange: (bucketId: string | null) => void;
|
|
78
|
+
emptyLabel?: string;
|
|
79
|
+
unavailableLabel?: string;
|
|
80
|
+
className?: string;
|
|
81
|
+
}
|
|
82
|
+
export declare function StackedBarChart(props: StackedBarChartProps): import("react").JSX.Element;
|
|
83
|
+
//#endregion
|
|
84
|
+
export type { SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries };
|
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useId, useRef, useState } from "react";
|
|
3
|
+
//#region src/react/sparkline.tsx
|
|
4
|
+
const DEFAULT_SPARKLINE_WIDTH = 96;
|
|
5
|
+
const DEFAULT_SPARKLINE_HEIGHT = 24;
|
|
6
|
+
const DEFAULT_SPARKLINE_LABEL = "Trend";
|
|
7
|
+
const DEFAULT_SPARKLINE_EMPTY_LABEL = "No history yet";
|
|
8
|
+
const DEFAULT_SPARKLINE_UNAVAILABLE_LABEL = "No readings available";
|
|
9
|
+
const DEFAULT_INSET = 2.5;
|
|
10
|
+
const STROKE_WIDTH = 1.5;
|
|
11
|
+
const DOT_RADIUS = 1.75;
|
|
12
|
+
const NUMBER_FORMAT = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
|
|
13
|
+
const hidden = {
|
|
14
|
+
position: "absolute",
|
|
15
|
+
width: 1,
|
|
16
|
+
height: 1,
|
|
17
|
+
padding: 0,
|
|
18
|
+
margin: -1,
|
|
19
|
+
overflow: "hidden",
|
|
20
|
+
clipPath: "inset(50%)",
|
|
21
|
+
whiteSpace: "nowrap",
|
|
22
|
+
border: 0
|
|
23
|
+
};
|
|
24
|
+
function formatSparklineValue(value) {
|
|
25
|
+
return NUMBER_FORMAT.format(value);
|
|
26
|
+
}
|
|
27
|
+
function isReading(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
29
|
+
}
|
|
30
|
+
function sparklineReadings(values) {
|
|
31
|
+
return values.filter(isReading);
|
|
32
|
+
}
|
|
33
|
+
function round(value) {
|
|
34
|
+
return Math.round(value * 100) / 100;
|
|
35
|
+
}
|
|
36
|
+
function sparklineGeometry(values, { width = 96, height = 24, inset = DEFAULT_INSET } = {}) {
|
|
37
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(inset) || width <= 0 || height <= 0 || inset < 0 || inset * 2 > Math.min(width, height)) throw new RangeError("Sparkline requires finite positive dimensions and an inset inside the viewBox");
|
|
38
|
+
const plotted = [];
|
|
39
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
40
|
+
const value = values[index];
|
|
41
|
+
if (isReading(value)) plotted.push({
|
|
42
|
+
index,
|
|
43
|
+
value
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const readings = plotted.map(({ value }) => value);
|
|
47
|
+
const gaps = values.length - readings.length;
|
|
48
|
+
if (readings.length === 0) return {
|
|
49
|
+
readings,
|
|
50
|
+
points: [],
|
|
51
|
+
segments: [],
|
|
52
|
+
gaps,
|
|
53
|
+
min: 0,
|
|
54
|
+
max: 0,
|
|
55
|
+
first: 0,
|
|
56
|
+
last: 0,
|
|
57
|
+
direction: "flat"
|
|
58
|
+
};
|
|
59
|
+
let min = readings[0];
|
|
60
|
+
let max = readings[0];
|
|
61
|
+
for (const value of readings) {
|
|
62
|
+
if (value < min) min = value;
|
|
63
|
+
if (value > max) max = value;
|
|
64
|
+
}
|
|
65
|
+
const first = readings[0];
|
|
66
|
+
const last = readings[readings.length - 1];
|
|
67
|
+
const span = max - min;
|
|
68
|
+
const points = plotted.map(({ index, value }) => {
|
|
69
|
+
const fraction = span === 0 ? .5 : Number.isFinite(span) ? (value - min) / span : (value / 2 - min / 2) / (max / 2 - min / 2);
|
|
70
|
+
return {
|
|
71
|
+
x: round(values.length <= 1 ? width / 2 : inset + (width - 2 * inset) * index / (values.length - 1)),
|
|
72
|
+
y: round(height - inset - (height - 2 * inset) * fraction)
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
const segments = [];
|
|
76
|
+
let run = [];
|
|
77
|
+
let previous = Number.NEGATIVE_INFINITY;
|
|
78
|
+
plotted.forEach(({ index }, position) => {
|
|
79
|
+
if (index !== previous + 1 && run.length > 0) {
|
|
80
|
+
segments.push(run);
|
|
81
|
+
run = [];
|
|
82
|
+
}
|
|
83
|
+
run.push(points[position]);
|
|
84
|
+
previous = index;
|
|
85
|
+
});
|
|
86
|
+
if (run.length > 0) segments.push(run);
|
|
87
|
+
return {
|
|
88
|
+
readings,
|
|
89
|
+
points,
|
|
90
|
+
segments,
|
|
91
|
+
gaps,
|
|
92
|
+
min,
|
|
93
|
+
max,
|
|
94
|
+
first,
|
|
95
|
+
last,
|
|
96
|
+
direction: last > first ? "rising" : last < first ? "falling" : "flat"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function sparklinePointsAttribute(points) {
|
|
100
|
+
return points.map(({ x, y }) => `${x},${y}`).join(" ");
|
|
101
|
+
}
|
|
102
|
+
function sparklineLabel(values, { label = DEFAULT_SPARKLINE_LABEL, format = formatSparklineValue } = {}) {
|
|
103
|
+
const { readings, gaps, min, max, first, last, direction } = sparklineGeometry(values);
|
|
104
|
+
const missing = gaps === 0 ? "" : `, ${gaps} not available`;
|
|
105
|
+
if (readings.length === 0) return gaps === 0 ? `${label}: no readings yet` : `${label}: no readings${missing}`;
|
|
106
|
+
if (readings.length === 1) return `${label}: one reading${missing}, ${format(first)}`;
|
|
107
|
+
if (max === min) return `${label}: ${readings.length} readings${missing}, unchanged at ${format(first)}`;
|
|
108
|
+
const movement = direction === "flat" ? "net unchanged" : direction;
|
|
109
|
+
return `${label}: ${readings.length} readings${missing}, range ${format(min)} to ${format(max)}, ${movement} from ${format(first)} to ${format(last)}`;
|
|
110
|
+
}
|
|
111
|
+
function Sparkline({ values, label = DEFAULT_SPARKLINE_LABEL, format = formatSparklineValue, width = 96, height = 24, emptyLabel = DEFAULT_SPARKLINE_EMPTY_LABEL, unavailableLabel = DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, className }) {
|
|
112
|
+
const geometry = sparklineGeometry(values, {
|
|
113
|
+
width,
|
|
114
|
+
height
|
|
115
|
+
});
|
|
116
|
+
const accessibleName = sparklineLabel(values, {
|
|
117
|
+
label,
|
|
118
|
+
format
|
|
119
|
+
});
|
|
120
|
+
if (geometry.points.length === 0) return /* @__PURE__ */ jsxs("span", {
|
|
121
|
+
"data-sparkline": geometry.gaps > 0 ? "unavailable" : "empty",
|
|
122
|
+
className,
|
|
123
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
124
|
+
style: hidden,
|
|
125
|
+
children: accessibleName
|
|
126
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
127
|
+
"aria-hidden": "true",
|
|
128
|
+
children: geometry.gaps > 0 ? unavailableLabel : emptyLabel
|
|
129
|
+
})]
|
|
130
|
+
});
|
|
131
|
+
const drawsLine = geometry.segments.some((segment) => segment.length > 1);
|
|
132
|
+
const end = geometry.points[geometry.points.length - 1];
|
|
133
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
134
|
+
role: "img",
|
|
135
|
+
"aria-label": accessibleName,
|
|
136
|
+
"data-scale": "extent",
|
|
137
|
+
"data-sparkline": drawsLine ? "line" : "point",
|
|
138
|
+
"data-direction": geometry.direction,
|
|
139
|
+
"data-gaps": geometry.gaps > 0 ? geometry.gaps : void 0,
|
|
140
|
+
width,
|
|
141
|
+
height,
|
|
142
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
143
|
+
className,
|
|
144
|
+
focusable: "false",
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx("desc", { children: Array.from(values, (value, index) => `Sample ${index + 1}: ${isReading(value) ? format(value) : "not available"}`).join("; ") }),
|
|
147
|
+
geometry.segments.map((segment, index) => {
|
|
148
|
+
if (segment.length > 1) return /* @__PURE__ */ jsx("polyline", {
|
|
149
|
+
points: sparklinePointsAttribute(segment),
|
|
150
|
+
fill: "none",
|
|
151
|
+
stroke: "currentColor",
|
|
152
|
+
strokeWidth: STROKE_WIDTH,
|
|
153
|
+
strokeLinecap: "round",
|
|
154
|
+
strokeLinejoin: "round",
|
|
155
|
+
vectorEffect: "non-scaling-stroke"
|
|
156
|
+
}, `segment-${index}`);
|
|
157
|
+
const only = segment[0];
|
|
158
|
+
if (only.x === end.x && only.y === end.y) return null;
|
|
159
|
+
return /* @__PURE__ */ jsx("circle", {
|
|
160
|
+
cx: only.x,
|
|
161
|
+
cy: only.y,
|
|
162
|
+
r: DOT_RADIUS,
|
|
163
|
+
fill: "currentColor"
|
|
164
|
+
}, `segment-${index}`);
|
|
165
|
+
}),
|
|
166
|
+
/* @__PURE__ */ jsx("circle", {
|
|
167
|
+
cx: end.x,
|
|
168
|
+
cy: end.y,
|
|
169
|
+
r: DOT_RADIUS,
|
|
170
|
+
fill: "currentColor"
|
|
171
|
+
})
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/react/stacked-bar-chart.tsx
|
|
177
|
+
/**
|
|
178
|
+
* Renderer extracted from ADC PR #8746, a1c3958fd73a3894d4515f9dc476dae9ace30ce0:
|
|
179
|
+
* products/platform/web/src/client/components/UsageChartSvg.tsx.
|
|
180
|
+
* Keeps its viewBox, hit targets, actual-stack highlight and select-never-toggle
|
|
181
|
+
* interaction. Product/category mapping, aggregation, currency and the portal
|
|
182
|
+
* tooltip formatting remain caller-owned; no React DOM dependency is needed here.
|
|
183
|
+
*/
|
|
184
|
+
function isValue(value) {
|
|
185
|
+
return value !== null && Number.isFinite(value);
|
|
186
|
+
}
|
|
187
|
+
function hasGap(bucket) {
|
|
188
|
+
return !isValue(bucket.total) || bucket.segments.some(({ value }) => !isValue(value));
|
|
189
|
+
}
|
|
190
|
+
function validate({ series, buckets, maxValue }) {
|
|
191
|
+
if (!Number.isFinite(maxValue) || maxValue < 0) throw new RangeError("StackedBarChart maxValue must be finite and nonnegative");
|
|
192
|
+
const seriesIds = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const { id } of series) {
|
|
194
|
+
if (!id || seriesIds.has(id)) throw new RangeError("StackedBarChart requires unique, nonempty series IDs");
|
|
195
|
+
seriesIds.add(id);
|
|
196
|
+
}
|
|
197
|
+
const bucketIds = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const bucket of buckets) {
|
|
199
|
+
if (!bucket.id || bucketIds.has(bucket.id)) throw new RangeError("StackedBarChart requires unique, nonempty bucket IDs");
|
|
200
|
+
bucketIds.add(bucket.id);
|
|
201
|
+
const segments = /* @__PURE__ */ new Set();
|
|
202
|
+
let sum = 0;
|
|
203
|
+
for (const { seriesId, value } of bucket.segments) {
|
|
204
|
+
if (!seriesIds.has(seriesId) || segments.has(seriesId)) throw new RangeError("StackedBarChart segments require distinct, declared series IDs");
|
|
205
|
+
segments.add(seriesId);
|
|
206
|
+
if (isValue(value)) {
|
|
207
|
+
if (value < 0 || value > maxValue) throw new RangeError("StackedBarChart values must lie in [0, maxValue]");
|
|
208
|
+
sum += value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (isValue(bucket.total) && (bucket.total < 0 || bucket.total > maxValue)) throw new RangeError("StackedBarChart totals must lie in [0, maxValue]");
|
|
212
|
+
if (!hasGap(bucket)) {
|
|
213
|
+
const total = bucket.total;
|
|
214
|
+
const tolerance = Number.EPSILON * 8 * Math.max(Math.abs(sum), Math.abs(total), Number.MIN_VALUE);
|
|
215
|
+
if (!Number.isFinite(sum) || Math.abs(sum - total) > tolerance) throw new RangeError("StackedBarChart total must match its supplied segments");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function StackedBarChart(props) {
|
|
220
|
+
const { label, series, buckets, maxValue, formatValue, selectedBucketId, onSelectionChange, emptyLabel = "No data yet", unavailableLabel = "Not available", className } = props;
|
|
221
|
+
const readoutId = useId();
|
|
222
|
+
const touchInteraction = useRef(false);
|
|
223
|
+
const [pointerBucketId, setPointerBucketId] = useState(null);
|
|
224
|
+
const pointerPosition = useRef({
|
|
225
|
+
x: 0,
|
|
226
|
+
y: 0
|
|
227
|
+
});
|
|
228
|
+
const tooltipRef = useRef(null);
|
|
229
|
+
const positionTooltip = (element) => {
|
|
230
|
+
const viewport = element.ownerDocument.defaultView;
|
|
231
|
+
if (!viewport) return;
|
|
232
|
+
const { width, height } = element.getBoundingClientRect();
|
|
233
|
+
const { x, y } = pointerPosition.current;
|
|
234
|
+
const left = x + 14 + width <= viewport.innerWidth - 8 ? x + 14 : x - width - 14;
|
|
235
|
+
const top = y + 14 + height <= viewport.innerHeight - 8 ? y + 14 : y - height - 14;
|
|
236
|
+
element.style.transform = `translate(${Math.max(8, Math.min(left, viewport.innerWidth - width - 8))}px, ${Math.max(8, Math.min(top, viewport.innerHeight - height - 8))}px)`;
|
|
237
|
+
};
|
|
238
|
+
const trackPointer = (event, bucketId) => {
|
|
239
|
+
pointerPosition.current = {
|
|
240
|
+
x: event.clientX,
|
|
241
|
+
y: event.clientY
|
|
242
|
+
};
|
|
243
|
+
if (pointerBucketId !== bucketId) setPointerBucketId(bucketId);
|
|
244
|
+
if (tooltipRef.current) positionTooltip(tooltipRef.current);
|
|
245
|
+
};
|
|
246
|
+
validate(props);
|
|
247
|
+
const bySeries = new Map(series.map((entry) => [entry.id, entry]));
|
|
248
|
+
const formatted = (value) => isValue(value) ? formatValue(value) : unavailableLabel;
|
|
249
|
+
const summary = (bucket) => `${bucket.label}: total ${formatted(bucket.total)}` + (hasGap(bucket) ? ` — ${unavailableLabel}` : "") + (bucket.segments.length ? ` — ${bucket.segments.map((segment) => `${bySeries.get(segment.seriesId).label} ${formatted(segment.value)}`).join(", ")}` : "");
|
|
250
|
+
const activeIndex = selectedBucketId === null ? -1 : buckets.findIndex(({ id }) => id === selectedBucketId);
|
|
251
|
+
const activeBucket = activeIndex >= 0 ? buckets[activeIndex] : null, marginTop = 12, marginRight = 16, marginLeft = 56;
|
|
252
|
+
const chartHeight = 220;
|
|
253
|
+
const innerHeight = 176;
|
|
254
|
+
const barGap = 6;
|
|
255
|
+
const barWidth = Math.max(8, Math.min(36, 648 / Math.max(1, buckets.length) - barGap));
|
|
256
|
+
const viewWidth = Math.max(buckets.length * (barWidth + barGap) + marginLeft + marginRight, 400);
|
|
257
|
+
const denseLabels = buckets.length > 14;
|
|
258
|
+
if (buckets.length === 0) return /* @__PURE__ */ jsxs("p", {
|
|
259
|
+
className,
|
|
260
|
+
children: [
|
|
261
|
+
label,
|
|
262
|
+
": ",
|
|
263
|
+
emptyLabel
|
|
264
|
+
]
|
|
265
|
+
});
|
|
266
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
267
|
+
className,
|
|
268
|
+
"data-stacked-bar-chart": "",
|
|
269
|
+
"data-scale": "zero",
|
|
270
|
+
children: [
|
|
271
|
+
/* @__PURE__ */ jsx("section", {
|
|
272
|
+
tabIndex: 0,
|
|
273
|
+
"aria-label": `${label} chart`,
|
|
274
|
+
style: { overflowX: "auto" },
|
|
275
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
276
|
+
"aria-label": label,
|
|
277
|
+
width: "100%",
|
|
278
|
+
viewBox: `0 0 ${viewWidth} ${chartHeight}`,
|
|
279
|
+
style: {
|
|
280
|
+
display: "block",
|
|
281
|
+
minWidth: 400,
|
|
282
|
+
maxWidth: "100%",
|
|
283
|
+
maxHeight: 300
|
|
284
|
+
},
|
|
285
|
+
preserveAspectRatio: "xMinYMin meet",
|
|
286
|
+
children: [[
|
|
287
|
+
0,
|
|
288
|
+
.25,
|
|
289
|
+
.5,
|
|
290
|
+
.75,
|
|
291
|
+
1
|
|
292
|
+
].map((fraction) => {
|
|
293
|
+
const y = 188 - fraction * innerHeight;
|
|
294
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
295
|
+
"aria-hidden": "true",
|
|
296
|
+
children: [/* @__PURE__ */ jsx("line", {
|
|
297
|
+
x1: marginLeft,
|
|
298
|
+
y1: y,
|
|
299
|
+
x2: viewWidth - marginRight,
|
|
300
|
+
y2: y,
|
|
301
|
+
stroke: "currentColor",
|
|
302
|
+
strokeDasharray: fraction === 0 ? "0" : "3 3",
|
|
303
|
+
opacity: fraction === 0 ? .4 : .2
|
|
304
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
305
|
+
x: 48,
|
|
306
|
+
y,
|
|
307
|
+
dy: "0.32em",
|
|
308
|
+
textAnchor: "end",
|
|
309
|
+
fontSize: "11",
|
|
310
|
+
fill: "currentColor",
|
|
311
|
+
fontFamily: "inherit",
|
|
312
|
+
children: formatValue(maxValue * fraction)
|
|
313
|
+
})]
|
|
314
|
+
}, fraction);
|
|
315
|
+
}), buckets.map((bucket, index) => {
|
|
316
|
+
const x = marginLeft + index * (barWidth + barGap) + barGap / 2;
|
|
317
|
+
const isActive = index === activeIndex;
|
|
318
|
+
const gap = hasGap(bucket);
|
|
319
|
+
const stackHeight = !gap && maxValue > 0 ? bucket.total / maxValue * innerHeight : 0;
|
|
320
|
+
let yOffset = 0;
|
|
321
|
+
const select = () => onSelectionChange(bucket.id);
|
|
322
|
+
const clear = () => {
|
|
323
|
+
if (isActive) onSelectionChange(null);
|
|
324
|
+
};
|
|
325
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
326
|
+
role: "button",
|
|
327
|
+
tabIndex: 0,
|
|
328
|
+
"data-bucket-id": bucket.id,
|
|
329
|
+
"data-gap": gap || void 0,
|
|
330
|
+
"aria-label": summary(bucket),
|
|
331
|
+
"aria-describedby": isActive ? readoutId : void 0,
|
|
332
|
+
onPointerEnter: (event) => {
|
|
333
|
+
touchInteraction.current = event.pointerType === "touch";
|
|
334
|
+
if (!touchInteraction.current) {
|
|
335
|
+
trackPointer(event, bucket.id);
|
|
336
|
+
select();
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
onPointerLeave: () => {
|
|
340
|
+
if (!touchInteraction.current) {
|
|
341
|
+
setPointerBucketId(null);
|
|
342
|
+
clear();
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
onPointerDown: (event) => {
|
|
346
|
+
touchInteraction.current = event.pointerType === "touch";
|
|
347
|
+
if (touchInteraction.current) setPointerBucketId(null);
|
|
348
|
+
},
|
|
349
|
+
onPointerMove: (event) => {
|
|
350
|
+
touchInteraction.current = event.pointerType === "touch";
|
|
351
|
+
if (!touchInteraction.current) {
|
|
352
|
+
trackPointer(event, bucket.id);
|
|
353
|
+
if (selectedBucketId !== bucket.id) select();
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
onMouseEnter: (event) => {
|
|
357
|
+
if (!touchInteraction.current) {
|
|
358
|
+
trackPointer(event, bucket.id);
|
|
359
|
+
select();
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
onMouseMove: (event) => {
|
|
363
|
+
if (!touchInteraction.current) {
|
|
364
|
+
trackPointer(event, bucket.id);
|
|
365
|
+
if (selectedBucketId !== bucket.id) select();
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
onMouseLeave: () => {
|
|
369
|
+
if (!touchInteraction.current) {
|
|
370
|
+
setPointerBucketId(null);
|
|
371
|
+
clear();
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
onFocus: select,
|
|
375
|
+
onBlur: clear,
|
|
376
|
+
onClick: select,
|
|
377
|
+
onKeyDown: (event) => {
|
|
378
|
+
setPointerBucketId(null);
|
|
379
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
select();
|
|
382
|
+
} else if (event.key === "Escape") {
|
|
383
|
+
event.preventDefault();
|
|
384
|
+
clear();
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
style: { cursor: "pointer" },
|
|
388
|
+
children: [
|
|
389
|
+
/* @__PURE__ */ jsx("rect", {
|
|
390
|
+
"data-hit-target": "",
|
|
391
|
+
x,
|
|
392
|
+
y: marginTop,
|
|
393
|
+
width: barWidth,
|
|
394
|
+
height: innerHeight,
|
|
395
|
+
fill: "transparent",
|
|
396
|
+
stroke: "none",
|
|
397
|
+
rx: 3
|
|
398
|
+
}),
|
|
399
|
+
!gap && bucket.segments.map((segment, segmentIndex) => {
|
|
400
|
+
const height = maxValue > 0 ? segment.value / maxValue * innerHeight : 0;
|
|
401
|
+
const y = 188 - yOffset - height;
|
|
402
|
+
yOffset += height;
|
|
403
|
+
return /* @__PURE__ */ jsx("rect", {
|
|
404
|
+
"data-series-id": segment.seriesId,
|
|
405
|
+
x,
|
|
406
|
+
y,
|
|
407
|
+
width: barWidth,
|
|
408
|
+
height: Math.max(height, height > 0 ? 1 : 0),
|
|
409
|
+
rx: segmentIndex === bucket.segments.length - 1 ? 3 : 0,
|
|
410
|
+
fill: bySeries.get(segment.seriesId).color,
|
|
411
|
+
opacity: isActive ? 1 : .92,
|
|
412
|
+
pointerEvents: "none"
|
|
413
|
+
}, segment.seriesId);
|
|
414
|
+
}),
|
|
415
|
+
gap && /* @__PURE__ */ jsx("text", {
|
|
416
|
+
x: x + barWidth / 2,
|
|
417
|
+
y: 184,
|
|
418
|
+
textAnchor: "middle",
|
|
419
|
+
fill: "currentColor",
|
|
420
|
+
"aria-hidden": "true",
|
|
421
|
+
pointerEvents: "none",
|
|
422
|
+
children: "—"
|
|
423
|
+
}),
|
|
424
|
+
isActive && /* @__PURE__ */ jsx("rect", {
|
|
425
|
+
"data-selection": "",
|
|
426
|
+
x,
|
|
427
|
+
y: 188 - Math.max(1, stackHeight),
|
|
428
|
+
width: barWidth,
|
|
429
|
+
height: Math.max(1, stackHeight),
|
|
430
|
+
fill: "none",
|
|
431
|
+
stroke: "currentColor",
|
|
432
|
+
strokeWidth: 1.5,
|
|
433
|
+
rx: 3,
|
|
434
|
+
pointerEvents: "none"
|
|
435
|
+
}),
|
|
436
|
+
/* @__PURE__ */ jsx("text", {
|
|
437
|
+
x: x + barWidth / 2,
|
|
438
|
+
y: 204,
|
|
439
|
+
textAnchor: denseLabels ? "end" : "middle",
|
|
440
|
+
fontSize: "10",
|
|
441
|
+
fill: "currentColor",
|
|
442
|
+
fontFamily: "inherit",
|
|
443
|
+
pointerEvents: "none",
|
|
444
|
+
"aria-hidden": "true",
|
|
445
|
+
transform: denseLabels ? `rotate(-40, ${x + barWidth / 2}, 204)` : void 0,
|
|
446
|
+
children: bucket.label
|
|
447
|
+
})
|
|
448
|
+
]
|
|
449
|
+
}, bucket.id);
|
|
450
|
+
})]
|
|
451
|
+
})
|
|
452
|
+
}),
|
|
453
|
+
activeBucket && pointerBucketId === activeBucket.id && !touchInteraction.current && /* @__PURE__ */ jsxs("div", {
|
|
454
|
+
"data-chart-tooltip": "",
|
|
455
|
+
role: "tooltip",
|
|
456
|
+
"aria-hidden": "true",
|
|
457
|
+
popover: "manual",
|
|
458
|
+
ref: (element) => {
|
|
459
|
+
tooltipRef.current = element;
|
|
460
|
+
if (element) {
|
|
461
|
+
if (typeof element.showPopover === "function") element.showPopover();
|
|
462
|
+
positionTooltip(element);
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
style: {
|
|
466
|
+
position: "fixed",
|
|
467
|
+
inset: "auto",
|
|
468
|
+
margin: 0,
|
|
469
|
+
left: 0,
|
|
470
|
+
top: 0,
|
|
471
|
+
zIndex: 1e3,
|
|
472
|
+
pointerEvents: "none",
|
|
473
|
+
width: "max-content",
|
|
474
|
+
maxWidth: "min(280px, calc(100vw - 16px))",
|
|
475
|
+
padding: "10px 12px",
|
|
476
|
+
borderRadius: "var(--radius-md, 8px)",
|
|
477
|
+
background: "var(--md3-surface-container-highest, #3b3b3b)",
|
|
478
|
+
color: "var(--md3-on-surface, #e6e6e6)",
|
|
479
|
+
border: "1px solid var(--md3-outline-variant, #4c4c4c)",
|
|
480
|
+
fontFamily: "inherit",
|
|
481
|
+
fontSize: "0.75rem",
|
|
482
|
+
lineHeight: 1.5,
|
|
483
|
+
overflowWrap: "anywhere"
|
|
484
|
+
},
|
|
485
|
+
children: [
|
|
486
|
+
/* @__PURE__ */ jsx("strong", { children: activeBucket.label }),
|
|
487
|
+
/* @__PURE__ */ jsxs("div", { children: ["Total ", formatted(activeBucket.total)] }),
|
|
488
|
+
activeBucket.segments.map((segment) => /* @__PURE__ */ jsxs("div", { children: [
|
|
489
|
+
bySeries.get(segment.seriesId).label,
|
|
490
|
+
": ",
|
|
491
|
+
formatted(segment.value)
|
|
492
|
+
] }, segment.seriesId))
|
|
493
|
+
]
|
|
494
|
+
}),
|
|
495
|
+
/* @__PURE__ */ jsx("div", {
|
|
496
|
+
id: readoutId,
|
|
497
|
+
role: "status",
|
|
498
|
+
"aria-live": "polite",
|
|
499
|
+
"aria-atomic": "true",
|
|
500
|
+
children: activeBucket ? summary(activeBucket) : "Hover, tap, or focus a bar to see its breakdown."
|
|
501
|
+
}),
|
|
502
|
+
/* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", { children: "View data" }), /* @__PURE__ */ jsxs("table", { children: [
|
|
503
|
+
/* @__PURE__ */ jsx("caption", { children: label }),
|
|
504
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
505
|
+
/* @__PURE__ */ jsx("th", {
|
|
506
|
+
scope: "col",
|
|
507
|
+
children: "Bucket"
|
|
508
|
+
}),
|
|
509
|
+
/* @__PURE__ */ jsx("th", {
|
|
510
|
+
scope: "col",
|
|
511
|
+
children: "Series"
|
|
512
|
+
}),
|
|
513
|
+
/* @__PURE__ */ jsx("th", {
|
|
514
|
+
scope: "col",
|
|
515
|
+
children: "Value"
|
|
516
|
+
})
|
|
517
|
+
] }) }),
|
|
518
|
+
/* @__PURE__ */ jsx("tbody", { children: buckets.map((bucket) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("tr", { children: [
|
|
519
|
+
/* @__PURE__ */ jsx("th", {
|
|
520
|
+
scope: "row",
|
|
521
|
+
children: bucket.label
|
|
522
|
+
}),
|
|
523
|
+
/* @__PURE__ */ jsx("td", { children: "Total" }),
|
|
524
|
+
/* @__PURE__ */ jsx("td", { children: formatted(bucket.total) })
|
|
525
|
+
] }), bucket.segments.map((segment) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
526
|
+
/* @__PURE__ */ jsx("th", {
|
|
527
|
+
scope: "row",
|
|
528
|
+
children: bucket.label
|
|
529
|
+
}),
|
|
530
|
+
/* @__PURE__ */ jsx("td", { children: bySeries.get(segment.seriesId).label }),
|
|
531
|
+
/* @__PURE__ */ jsx("td", { children: formatted(segment.value) })
|
|
532
|
+
] }, segment.seriesId))] }, bucket.id)) })
|
|
533
|
+
] })] })
|
|
534
|
+
]
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, Sparkline, StackedBarChart, formatSparklineValue, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -26,7 +26,20 @@
|
|
|
26
26
|
"import": "./dist/index.js",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
28
|
},
|
|
29
|
-
"./figure.css": "./dist/figure.css"
|
|
29
|
+
"./figure.css": "./dist/figure.css",
|
|
30
|
+
"./react": {
|
|
31
|
+
"types": "./dist/react/index.d.ts",
|
|
32
|
+
"import": "./dist/react/index.js",
|
|
33
|
+
"default": "./dist/react/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"react": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
30
43
|
},
|
|
31
44
|
"devDependencies": {
|
|
32
45
|
"tsdown": "^0.23.0",
|