@gntik-ai/charts 0.0.0-stage → 0.5.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/dist/index.js ADDED
@@ -0,0 +1,2317 @@
1
+ 'use client';
2
+ import { Area, AreaChart as AreaChart$1, Bar, BarChart as BarChart$1, CartesianGrid, ComposedChart, Funnel, FunnelChart as FunnelChart$1, Line, LineChart as LineChart$1, Pie, PieChart, PolarAngleAxis, PolarGrid, PolarRadiusAxis, Radar, RadarChart as RadarChart$1, ReferenceLine, ResponsiveContainer, Sankey, Tooltip, XAxis, YAxis } from "recharts";
3
+ import { useCallback, useId, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { currentTheme, observeTheme, tokenColor } from "@gntik-ai/tokens/runtime";
6
+ //#region src/format.ts
7
+ const nf = (opts) => new Intl.NumberFormat("en-US", opts);
8
+ /** Ready-made value formatters (en-US). */
9
+ const chartFmt = {
10
+ usd: (n) => "$" + nf({ maximumFractionDigits: 0 }).format(n),
11
+ usd2: (n) => "$" + nf({
12
+ minimumFractionDigits: 2,
13
+ maximumFractionDigits: 2
14
+ }).format(n),
15
+ usdCompact: (n) => "$" + nf({
16
+ notation: "compact",
17
+ maximumFractionDigits: 1
18
+ }).format(n),
19
+ num: (n) => nf().format(n),
20
+ compact: (n) => nf({
21
+ notation: "compact",
22
+ maximumFractionDigits: 1
23
+ }).format(n),
24
+ ms: (n) => nf().format(n) + " ms",
25
+ pct: (n) => `${n}%`,
26
+ /** Ratio (0–1) → percent with up to one decimal: `0.125` → `12.5%`. */
27
+ percent: (n) => nf({
28
+ style: "percent",
29
+ maximumFractionDigits: 1
30
+ }).format(n),
31
+ /** Signed ratio for deltas: `0.042` → `+4.2%`, `-0.1` → `−10%`. */
32
+ percentDelta: (n) => nf({
33
+ style: "percent",
34
+ maximumFractionDigits: 1,
35
+ signDisplay: "exceptZero"
36
+ }).format(n).replace("-", "−"),
37
+ /** Milliseconds → the two largest units: `850` → `850 ms`, `2400` → `2.4 s`, `3_723_000` → `1h 2m`. */
38
+ duration: (n) => formatDuration(n),
39
+ /** Seconds → like `duration`: `90` → `1m 30s`. */
40
+ durationS: (n) => formatDuration(n * 1e3),
41
+ /** Bytes → decimal SI units: `1536` → `1.5 kB`, `2_000_000_000` → `2 GB`. */
42
+ bytes: (n) => formatBytes(n)
43
+ };
44
+ function formatDuration(ms) {
45
+ if (!Number.isFinite(ms)) return "—";
46
+ const sign = ms < 0 ? "−" : "";
47
+ const a = Math.abs(ms);
48
+ if (a < 1e3) return `${sign}${nf({ maximumFractionDigits: a < 10 ? 2 : 0 }).format(a)} ms`;
49
+ if (a < 6e4) return `${sign}${nf({ maximumFractionDigits: 1 }).format(a / 1e3)} s`;
50
+ const parts = [
51
+ [864e5, "d"],
52
+ [36e5, "h"],
53
+ [6e4, "m"],
54
+ [1e3, "s"]
55
+ ];
56
+ const total = Math.round(a / 1e3) * 1e3;
57
+ const first = parts.findIndex(([size]) => total >= size);
58
+ const out = [];
59
+ let rest = total;
60
+ for (const [size, unit] of parts.slice(first, first + 2)) {
61
+ const q = Math.floor(rest / size);
62
+ rest -= q * size;
63
+ if (q > 0) out.push(`${q}${unit}`);
64
+ }
65
+ return sign + out.join(" ");
66
+ }
67
+ const BYTE_UNITS = [
68
+ "B",
69
+ "kB",
70
+ "MB",
71
+ "GB",
72
+ "TB",
73
+ "PB"
74
+ ];
75
+ function formatBytes(bytes) {
76
+ if (!Number.isFinite(bytes)) return "—";
77
+ const sign = bytes < 0 ? "−" : "";
78
+ let v = Math.abs(bytes);
79
+ let u = 0;
80
+ while (v >= 1e3 && u < BYTE_UNITS.length - 1) {
81
+ v /= 1e3;
82
+ u += 1;
83
+ }
84
+ return `${sign}${nf({ maximumFractionDigits: u === 0 ? 0 : 1 }).format(v)} ${BYTE_UNITS[u]}`;
85
+ }
86
+ const identity = (n) => String(n);
87
+ /** Applies a formatter to whatever Recharts hands over (number, string or range). */
88
+ function formatAny(value, fmt) {
89
+ if (typeof value === "number") return fmt(value);
90
+ if (Array.isArray(value)) return value.map((v) => formatAny(v, fmt)).join(" – ");
91
+ return value == null ? "" : String(value);
92
+ }
93
+ function cx(...parts) {
94
+ return parts.filter(Boolean).join(" ");
95
+ }
96
+ //#endregion
97
+ //#region src/ChartLegend.tsx
98
+ const legendFocus = "rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring";
99
+ /** Interactive legend: each entry is a toggle button that shows/hides its series. */
100
+ function ChartLegend({ items, hidden, onToggle, className }) {
101
+ return /* @__PURE__ */ jsx("div", {
102
+ className: cx("flex flex-wrap items-center gap-x-4 gap-y-1.5", className),
103
+ children: items.map((it) => {
104
+ const off = hidden.has(it.key);
105
+ return /* @__PURE__ */ jsxs("button", {
106
+ type: "button",
107
+ "aria-pressed": !off,
108
+ onClick: () => onToggle(it.key),
109
+ className: cx("group flex items-center gap-1.5 transition-opacity", off && "opacity-40", legendFocus),
110
+ children: [/* @__PURE__ */ jsx("span", {
111
+ "aria-hidden": true,
112
+ className: cx("h-2.5 w-2.5 shrink-0 rounded-[3px]", off && "grayscale"),
113
+ style: { background: it.color }
114
+ }), /* @__PURE__ */ jsx("span", {
115
+ className: cx("text-[12.5px] text-muted-foreground transition-colors", off ? "line-through" : "group-hover:text-foreground"),
116
+ children: it.label
117
+ })]
118
+ }, it.key);
119
+ })
120
+ });
121
+ }
122
+ /** Legend state: the set of hidden series keys plus a toggle. */
123
+ function useHiddenSeries() {
124
+ const [hidden, setHidden] = useState(() => /* @__PURE__ */ new Set());
125
+ return [hidden, useCallback((key) => {
126
+ setHidden((s) => {
127
+ const next = new Set(s);
128
+ if (next.has(key)) next.delete(key);
129
+ else next.add(key);
130
+ return next;
131
+ });
132
+ }, [])];
133
+ }
134
+ //#endregion
135
+ //#region src/ChartTooltip.tsx
136
+ function swatchOf(entry) {
137
+ if (entry.color) return entry.color;
138
+ if (entry.fill) return entry.fill;
139
+ const inner = entry.payload;
140
+ if (inner && typeof inner === "object" && "fill" in inner) {
141
+ const fill = inner.fill;
142
+ return typeof fill === "string" ? fill : void 0;
143
+ }
144
+ }
145
+ /** Brand tooltip card: label header + one row per series, values in mono. */
146
+ function ChartTooltip({ active, payload, label, valueFormatter = identity, seriesFormatter, labelFormatter }) {
147
+ if (!active || !payload || payload.length === 0) return null;
148
+ const rows = payload.filter((p) => p.value != null);
149
+ return /* @__PURE__ */ jsxs("div", {
150
+ className: "min-w-[150px] rounded-md border border-border bg-popover px-3 py-2.5 font-sans shadow-md",
151
+ children: [label != null && label !== "" && /* @__PURE__ */ jsx("div", {
152
+ className: "mb-2 border-b border-border pb-2 text-[11px] font-medium text-muted-foreground",
153
+ children: labelFormatter ? labelFormatter(label) : label
154
+ }), /* @__PURE__ */ jsx("div", {
155
+ className: "flex flex-col gap-1.5",
156
+ children: rows.map((p, i) => {
157
+ const name = p.name == null ? "" : String(p.name);
158
+ return /* @__PURE__ */ jsxs("div", {
159
+ className: "flex items-center justify-between gap-5",
160
+ children: [/* @__PURE__ */ jsxs("span", {
161
+ className: "flex min-w-0 items-center gap-2",
162
+ children: [/* @__PURE__ */ jsx("span", {
163
+ "aria-hidden": true,
164
+ className: "h-2.5 w-2.5 shrink-0 rounded-[3px]",
165
+ style: { background: swatchOf(p) }
166
+ }), /* @__PURE__ */ jsx("span", {
167
+ className: "truncate text-[12px] text-muted-foreground",
168
+ children: name
169
+ })]
170
+ }), /* @__PURE__ */ jsx("span", {
171
+ className: "font-mono text-[12px] font-semibold tabular-nums text-foreground",
172
+ children: seriesFormatter ? seriesFormatter(p.value, name) : formatAny(p.value, valueFormatter)
173
+ })]
174
+ }, `${name}-${i}`);
175
+ })
176
+ })]
177
+ });
178
+ }
179
+ /** Adapts Recharts 3 `<Tooltip content>` to the brand tooltip: `content={tooltipContent({ valueFormatter })}`. */
180
+ function tooltipContent(options) {
181
+ return function BrandTooltipContent(p) {
182
+ return /* @__PURE__ */ jsx(ChartTooltip, {
183
+ active: p.active,
184
+ payload: p.payload,
185
+ label: p.label,
186
+ ...options
187
+ });
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/states.tsx
192
+ const stateBox = "flex w-full flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border px-4 text-center";
193
+ /** Placeholder for a chart without data. */
194
+ function ChartEmpty({ height = 240, children = "No data for this period", className }) {
195
+ return /* @__PURE__ */ jsxs("div", {
196
+ "data-chart-state": "empty",
197
+ className: cx(stateBox, className),
198
+ style: { height },
199
+ children: [/* @__PURE__ */ jsxs("svg", {
200
+ "aria-hidden": true,
201
+ viewBox: "0 0 24 24",
202
+ className: "h-5 w-5 fill-none stroke-muted-foreground",
203
+ strokeWidth: 1.75,
204
+ children: [/* @__PURE__ */ jsx("path", { d: "M3 3v18h18" }), /* @__PURE__ */ jsx("path", {
205
+ d: "M7 15h2M12 11h2M17 7h2",
206
+ strokeLinecap: "round"
207
+ })]
208
+ }), /* @__PURE__ */ jsx("p", {
209
+ className: "text-[13px] text-muted-foreground",
210
+ children
211
+ })]
212
+ });
213
+ }
214
+ const SKELETON_BARS = [
215
+ 42,
216
+ 64,
217
+ 50,
218
+ 78,
219
+ 58,
220
+ 88,
221
+ 70,
222
+ 54
223
+ ];
224
+ /** Skeleton plot (bars over a baseline); the pulse stops under prefers-reduced-motion. */
225
+ function ChartLoading({ height = 240, label = "Loading chart", className }) {
226
+ return /* @__PURE__ */ jsxs("div", {
227
+ role: "status",
228
+ "aria-label": label,
229
+ "data-chart-state": "loading",
230
+ className: cx("flex w-full items-end gap-2 border-b border-border px-2", className),
231
+ style: { height },
232
+ children: [SKELETON_BARS.map((h, i) => /* @__PURE__ */ jsx("div", {
233
+ "aria-hidden": true,
234
+ className: "flex-1 animate-pulse rounded-t-sm bg-muted motion-reduce:animate-none",
235
+ style: { height: `${h}%` }
236
+ }, i)), /* @__PURE__ */ jsxs("span", {
237
+ className: "sr-only",
238
+ children: [label, "…"]
239
+ })]
240
+ });
241
+ }
242
+ /** Error placeholder with an optional retry action. */
243
+ function ChartError({ height = 240, children = "The chart could not be loaded.", onRetry, className }) {
244
+ return /* @__PURE__ */ jsxs("div", {
245
+ role: "alert",
246
+ "data-chart-state": "error",
247
+ className: cx(stateBox, "border-destructive/40", className),
248
+ style: { height },
249
+ children: [/* @__PURE__ */ jsx("p", {
250
+ className: "text-[13px] text-destructive-text",
251
+ children
252
+ }), onRetry && /* @__PURE__ */ jsx("button", {
253
+ type: "button",
254
+ onClick: onRetry,
255
+ className: "h-8 rounded-md border border-border bg-card px-3 text-[12.5px] font-medium text-foreground transition-colors hover:bg-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring motion-reduce:transition-none",
256
+ children: "Retry"
257
+ })]
258
+ });
259
+ }
260
+ /** Resolves the effective state: `ready` with no rows is `empty`. */
261
+ function resolveState(state, rows) {
262
+ const s = state ?? "ready";
263
+ return s === "ready" && rows === 0 ? "empty" : s;
264
+ }
265
+ /** The placeholder for a non-ready state, or `null` when the chart should render. */
266
+ function chartStateView(state, { height, emptyMessage, errorMessage, onRetry }) {
267
+ if (state === "loading") return /* @__PURE__ */ jsx(ChartLoading, { height });
268
+ if (state === "empty") return /* @__PURE__ */ jsx(ChartEmpty, {
269
+ height,
270
+ children: emptyMessage
271
+ });
272
+ if (state === "error") return /* @__PURE__ */ jsx(ChartError, {
273
+ height,
274
+ onRetry,
275
+ children: errorMessage
276
+ });
277
+ return null;
278
+ }
279
+ /** Plain data table: a screen-reader fallback for a chart (visually hidden by default). */
280
+ function ChartDataTable({ caption, columns, rows, visible = false }) {
281
+ return /* @__PURE__ */ jsxs("table", {
282
+ "data-chart-table": "",
283
+ className: cx(visible ? "mt-3 w-full text-[12px]" : "sr-only"),
284
+ children: [
285
+ /* @__PURE__ */ jsx("caption", {
286
+ className: visible ? "mb-1 text-left text-muted-foreground" : void 0,
287
+ children: caption
288
+ }),
289
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((c, i) => /* @__PURE__ */ jsx("th", {
290
+ scope: "col",
291
+ className: visible ? "text-left font-medium text-muted-foreground" : void 0,
292
+ children: c
293
+ }, i)) }) }),
294
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((r, i) => /* @__PURE__ */ jsx("tr", { children: r.map((cell, j) => j === 0 ? /* @__PURE__ */ jsx("th", {
295
+ scope: "row",
296
+ className: visible ? "text-left font-normal text-foreground" : void 0,
297
+ children: cell
298
+ }, j) : /* @__PURE__ */ jsx("td", {
299
+ className: visible ? "font-mono tabular-nums text-foreground" : void 0,
300
+ children: cell
301
+ }, j)) }, i)) })
302
+ ]
303
+ });
304
+ }
305
+ //#endregion
306
+ //#region src/shared.tsx
307
+ /** Reads a field from a datum without widening the public generic. */
308
+ function field(d, key) {
309
+ return d[key];
310
+ }
311
+ function numField(d, key) {
312
+ const v = field(d, key);
313
+ return typeof v === "number" ? v : 0;
314
+ }
315
+ function accessibleName(kind, explicit, title, series) {
316
+ return explicit ?? title ?? (series.length ? `${kind}: ${series.join(", ")}` : kind);
317
+ }
318
+ /** Shared axis styling: no tick lines, no axis line, muted labels. */
319
+ function axisProps(t) {
320
+ return {
321
+ stroke: t.axis,
322
+ tickLine: false,
323
+ axisLine: false,
324
+ tick: {
325
+ fill: t.text,
326
+ fontSize: 11
327
+ }
328
+ };
329
+ }
330
+ const CHART_MARGIN = {
331
+ top: 6,
332
+ right: 10,
333
+ left: 0,
334
+ bottom: 0
335
+ };
336
+ /** Figure wrapper: accessible name, optional legend above, responsive plot area, state placeholders. */
337
+ function ChartFrame({ label, height, legend, className, state = "ready", table, footer, figureProps, emptyMessage, errorMessage, onRetry, children }) {
338
+ const placeholder = chartStateView(state, {
339
+ height,
340
+ emptyMessage,
341
+ errorMessage,
342
+ onRetry
343
+ });
344
+ return /* @__PURE__ */ jsx("div", {
345
+ ...figureProps,
346
+ role: "figure",
347
+ "aria-label": label,
348
+ "aria-busy": state === "loading" || void 0,
349
+ className: cx("w-full font-sans", className),
350
+ children: placeholder ?? /* @__PURE__ */ jsxs(Fragment, { children: [
351
+ legend,
352
+ /* @__PURE__ */ jsx("div", {
353
+ className: "w-full",
354
+ style: { height },
355
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
356
+ width: "100%",
357
+ height: "100%",
358
+ children
359
+ })
360
+ }),
361
+ footer,
362
+ table
363
+ ] })
364
+ });
365
+ }
366
+ /** Data table fallback for an index × categories dataset. */
367
+ function cartesianTable(caption, data, index, categories, format) {
368
+ return /* @__PURE__ */ jsx(ChartDataTable, {
369
+ caption,
370
+ columns: [index, ...categories],
371
+ rows: data.map((d) => [String(field(d, index)), ...categories.map((c) => format(field(d, c), c))])
372
+ });
373
+ }
374
+ //#endregion
375
+ //#region src/ChartBrush.tsx
376
+ /** Clamps a range to `[0, length - 1]`, ordered, at least `minSpan` points wide when possible. */
377
+ function clampRange(range, length, minSpan = 1) {
378
+ const last = Math.max(0, length - 1);
379
+ if (!range) return [0, last];
380
+ let start = Math.round(Math.min(range[0], range[1]));
381
+ let end = Math.round(Math.max(range[0], range[1]));
382
+ start = Math.min(Math.max(0, start), last);
383
+ end = Math.min(Math.max(0, end), last);
384
+ const span = Math.min(minSpan, last);
385
+ if (end - start < span) {
386
+ if (start + span <= last) end = start + span;
387
+ else start = Math.max(0, end - span);
388
+ }
389
+ return [start, end];
390
+ }
391
+ /** Controlled/uncontrolled brush range; `null` inner state means "everything". */
392
+ function useChartRange({ length, range, defaultRange, onRangeChange }) {
393
+ const [inner, setInner] = useState(defaultRange ?? null);
394
+ const current = clampRange(range ?? inner, length);
395
+ const set = (next) => {
396
+ const clamped = clampRange(next, length);
397
+ setInner(clamped);
398
+ onRangeChange?.(clamped);
399
+ };
400
+ return [current, set];
401
+ }
402
+ const thumbClass = cx("absolute top-1/2 z-10 h-7 w-3 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize touch-none rounded-sm border border-primary bg-card shadow-sm", "after:absolute after:inset-y-2 after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-primary", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring");
403
+ function previewPath(values) {
404
+ if (values.length < 2) return "";
405
+ const min = Math.min(...values);
406
+ const span = Math.max(...values) - min || 1;
407
+ return values.map((val, i) => `${i === 0 ? "M" : "L"}${(i / (values.length - 1) * 100).toFixed(2)},${(36 - (val - min) / span * 32).toFixed(2)}`).join(" ");
408
+ }
409
+ /**
410
+ * Range brush under a time-series chart: two keyboard-accessible handles (`role="slider"`,
411
+ * Arrow keys ±1, PageUp/PageDown ±10%, Home/End) and a draggable window to pan.
412
+ */
413
+ function ChartBrush({ length, range, onRangeChange, labelOf, preview, label = "Zoom range", className }) {
414
+ const track = useRef(null);
415
+ const drag = useRef(null);
416
+ const last = Math.max(1, length - 1);
417
+ const [start, end] = range;
418
+ const pct = (i) => `${i / last * 100}%`;
419
+ const full = start === 0 && end === length - 1;
420
+ const step = Math.max(1, Math.round(length / 10));
421
+ const indexAt = (clientX) => {
422
+ const rect = track.current?.getBoundingClientRect();
423
+ if (!rect || rect.width === 0) return 0;
424
+ return Math.round((clientX - rect.left) / rect.width * last);
425
+ };
426
+ const begin = (e) => {
427
+ const part = e.currentTarget.dataset.brushHandle ?? "window";
428
+ const kind = part === "start" || part === "end" ? part : "window";
429
+ e.preventDefault();
430
+ e.stopPropagation();
431
+ e.currentTarget.setPointerCapture?.(e.pointerId);
432
+ drag.current = {
433
+ kind,
434
+ originIndex: indexAt(e.clientX),
435
+ originRange: range
436
+ };
437
+ };
438
+ const move = (e) => {
439
+ const d = drag.current;
440
+ if (!d) return;
441
+ const i = indexAt(e.clientX);
442
+ const [s0, e0] = d.originRange;
443
+ if (d.kind === "start") onRangeChange([Math.min(i, e0 - 1), e0]);
444
+ else if (d.kind === "end") onRangeChange([s0, Math.max(i, s0 + 1)]);
445
+ else {
446
+ const delta = Math.min(Math.max(i - d.originIndex, -s0), length - 1 - e0);
447
+ onRangeChange([s0 + delta, e0 + delta]);
448
+ }
449
+ };
450
+ const stop = () => {
451
+ drag.current = null;
452
+ };
453
+ const onKey = (which) => (e) => {
454
+ const value = which === "start" ? start : end;
455
+ const min = which === "start" ? 0 : start + 1;
456
+ const max = which === "start" ? end - 1 : length - 1;
457
+ let next = null;
458
+ if (e.key === "ArrowLeft" || e.key === "ArrowDown") next = value - 1;
459
+ else if (e.key === "ArrowRight" || e.key === "ArrowUp") next = value + 1;
460
+ else if (e.key === "PageDown") next = value - step;
461
+ else if (e.key === "PageUp") next = value + step;
462
+ else if (e.key === "Home") next = min;
463
+ else if (e.key === "End") next = max;
464
+ if (next == null) return;
465
+ e.preventDefault();
466
+ const clamped = Math.min(Math.max(next, min), max);
467
+ onRangeChange(which === "start" ? [clamped, end] : [start, clamped]);
468
+ };
469
+ const path = preview ? previewPath(preview) : "";
470
+ const thumb = (which) => {
471
+ const value = which === "start" ? start : end;
472
+ return /* @__PURE__ */ jsx("div", {
473
+ role: "slider",
474
+ tabIndex: 0,
475
+ "aria-label": which === "start" ? "Range start" : "Range end",
476
+ "aria-orientation": "horizontal",
477
+ "aria-valuemin": which === "start" ? 0 : start + 1,
478
+ "aria-valuemax": which === "start" ? end - 1 : length - 1,
479
+ "aria-valuenow": value,
480
+ "aria-valuetext": labelOf(value),
481
+ "data-brush-handle": which,
482
+ className: thumbClass,
483
+ style: { left: pct(value) },
484
+ onKeyDown: onKey(which),
485
+ onPointerDown: begin,
486
+ onPointerMove: move,
487
+ onPointerUp: stop,
488
+ onPointerCancel: stop
489
+ });
490
+ };
491
+ if (length < 2) return null;
492
+ return /* @__PURE__ */ jsxs("div", {
493
+ role: "group",
494
+ "aria-label": label,
495
+ "data-chart-brush": "",
496
+ className: cx("mt-3 px-1", className),
497
+ children: [/* @__PURE__ */ jsxs("div", {
498
+ ref: track,
499
+ className: "relative mx-1.5 h-10 rounded-md border border-border bg-secondary/30",
500
+ children: [
501
+ path && /* @__PURE__ */ jsx("svg", {
502
+ "aria-hidden": true,
503
+ viewBox: "0 0 100 40",
504
+ preserveAspectRatio: "none",
505
+ className: "absolute inset-0 h-full w-full",
506
+ children: /* @__PURE__ */ jsx("path", {
507
+ d: path,
508
+ fill: "none",
509
+ stroke: "hsl(var(--muted-foreground))",
510
+ strokeOpacity: .6,
511
+ strokeWidth: 1,
512
+ vectorEffect: "non-scaling-stroke"
513
+ })
514
+ }),
515
+ /* @__PURE__ */ jsx("div", {
516
+ "aria-hidden": true,
517
+ "data-brush-window": "",
518
+ className: "absolute inset-y-0 cursor-grab touch-none border-y border-primary/60 bg-primary/14 active:cursor-grabbing",
519
+ style: {
520
+ left: pct(start),
521
+ width: `${(end - start) / last * 100}%`
522
+ },
523
+ onPointerDown: begin,
524
+ onPointerMove: move,
525
+ onPointerUp: stop,
526
+ onPointerCancel: stop
527
+ }),
528
+ thumb("start"),
529
+ thumb("end")
530
+ ]
531
+ }), /* @__PURE__ */ jsxs("div", {
532
+ className: "mt-1.5 flex items-center justify-between gap-2 text-[11.5px] text-muted-foreground",
533
+ children: [/* @__PURE__ */ jsxs("span", {
534
+ className: "font-mono tabular-nums",
535
+ children: [
536
+ labelOf(start),
537
+ " – ",
538
+ labelOf(end)
539
+ ]
540
+ }), !full && /* @__PURE__ */ jsx("button", {
541
+ type: "button",
542
+ onClick: () => onRangeChange([0, length - 1]),
543
+ className: cx("px-1 font-medium text-foreground underline-offset-2 hover:underline", "rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"),
544
+ children: "Reset zoom"
545
+ })]
546
+ })]
547
+ });
548
+ }
549
+ //#endregion
550
+ //#region src/live.ts
551
+ const REDUCED_QUERY = "(prefers-reduced-motion: reduce)";
552
+ function subscribeReduced(onChange) {
553
+ if (typeof window === "undefined" || !window.matchMedia) return () => {};
554
+ const mq = window.matchMedia(REDUCED_QUERY);
555
+ mq.addEventListener?.("change", onChange);
556
+ return () => mq.removeEventListener?.("change", onChange);
557
+ }
558
+ const getReduced = () => typeof window !== "undefined" && window.matchMedia ? window.matchMedia(REDUCED_QUERY).matches : false;
559
+ /** `true` when the user asks for reduced motion (false on the server). */
560
+ function usePrefersReducedMotion() {
561
+ return useSyncExternalStore(subscribeReduced, getReduced, () => false);
562
+ }
563
+ function reducer(state, action) {
564
+ const paused = state.hover || state.focus || state.manual;
565
+ switch (action.type) {
566
+ case "append": {
567
+ if (action.points.length === 0) return state;
568
+ const live = [...state.live, ...action.points].slice(-action.size);
569
+ return {
570
+ ...state,
571
+ live,
572
+ pending: paused ? Math.min(action.size, state.pending + action.points.length) : 0
573
+ };
574
+ }
575
+ case "reset": {
576
+ const live = action.points.slice(-action.size);
577
+ return {
578
+ ...state,
579
+ live,
580
+ frozen: paused ? live : null,
581
+ pending: 0
582
+ };
583
+ }
584
+ case "flag": {
585
+ if (state[action.flag] === action.on) return state;
586
+ const next = {
587
+ ...state,
588
+ [action.flag]: action.on
589
+ };
590
+ const nowPaused = next.hover || next.focus || next.manual;
591
+ if (nowPaused && !paused) return {
592
+ ...next,
593
+ frozen: state.live,
594
+ pending: 0
595
+ };
596
+ if (!nowPaused && paused) return {
597
+ ...next,
598
+ frozen: null,
599
+ pending: 0
600
+ };
601
+ return next;
602
+ }
603
+ }
604
+ }
605
+ /**
606
+ * State for a live-updating chart: a sliding window of points, with updates frozen while the
607
+ * user hovers or focuses the chart (so a tooltip or a value does not move under them) and a
608
+ * manual pause. Render with `animate={!reducedMotion}` for smooth transitions only when the
609
+ * user has not asked for reduced motion.
610
+ */
611
+ function useLiveSeries({ initial = [], windowSize = 60, pauseOnInteraction = true } = {}) {
612
+ const size = Math.max(1, Math.floor(windowSize));
613
+ const [state, dispatch] = useReducer(reducer, initial, (init) => ({
614
+ live: init.slice(-size),
615
+ frozen: null,
616
+ pending: 0,
617
+ hover: false,
618
+ focus: false,
619
+ manual: false
620
+ }));
621
+ const reducedMotion = usePrefersReducedMotion();
622
+ const append = useCallback((points) => dispatch({
623
+ type: "append",
624
+ points: Array.isArray(points) ? points : [points],
625
+ size
626
+ }), [size]);
627
+ const reset = useCallback((points = []) => dispatch({
628
+ type: "reset",
629
+ points,
630
+ size
631
+ }), [size]);
632
+ const pause = useCallback(() => dispatch({
633
+ type: "flag",
634
+ flag: "manual",
635
+ on: true
636
+ }), []);
637
+ const resume = useCallback(() => dispatch({
638
+ type: "flag",
639
+ flag: "manual",
640
+ on: false
641
+ }), []);
642
+ const pauseProps = useMemo(() => ({
643
+ onPointerEnter: () => pauseOnInteraction && dispatch({
644
+ type: "flag",
645
+ flag: "hover",
646
+ on: true
647
+ }),
648
+ onPointerLeave: () => dispatch({
649
+ type: "flag",
650
+ flag: "hover",
651
+ on: false
652
+ }),
653
+ onFocus: () => pauseOnInteraction && dispatch({
654
+ type: "flag",
655
+ flag: "focus",
656
+ on: true
657
+ }),
658
+ onBlur: (e) => {
659
+ if (e.currentTarget.contains(e.relatedTarget)) return;
660
+ dispatch({
661
+ type: "flag",
662
+ flag: "focus",
663
+ on: false
664
+ });
665
+ }
666
+ }), [pauseOnInteraction]);
667
+ const paused = state.hover || state.focus || state.manual;
668
+ return {
669
+ data: paused && state.frozen ? state.frozen : state.live,
670
+ append,
671
+ reset,
672
+ paused,
673
+ manuallyPaused: state.manual,
674
+ pause,
675
+ resume,
676
+ togglePaused: state.manual ? resume : pause,
677
+ pending: state.pending,
678
+ reducedMotion,
679
+ pauseProps
680
+ };
681
+ }
682
+ //#endregion
683
+ //#region src/overlays.tsx
684
+ /** Stroke token and contrast-safe text token of each tone. */
685
+ const TONE_TOKENS = {
686
+ neutral: {
687
+ stroke: "--muted-foreground",
688
+ text: "--muted-foreground"
689
+ },
690
+ primary: {
691
+ stroke: "--primary",
692
+ text: "--primary-text"
693
+ },
694
+ success: {
695
+ stroke: "--success",
696
+ text: "--success-text"
697
+ },
698
+ warning: {
699
+ stroke: "--warning",
700
+ text: "--warning-text"
701
+ },
702
+ destructive: {
703
+ stroke: "--destructive",
704
+ text: "--destructive-text"
705
+ },
706
+ info: {
707
+ stroke: "--info",
708
+ text: "--foreground"
709
+ }
710
+ };
711
+ const v = (token) => `hsl(var(${token}))`;
712
+ /** A stable key for an annotation (x + label). */
713
+ const annotationKey = (a) => `${String(a.x)}::${a.label}`;
714
+ function boxOf(props, key = "viewBox") {
715
+ const vb = props?.[key];
716
+ if (!vb || typeof vb.x !== "number" || typeof vb.y !== "number") return null;
717
+ return {
718
+ x: vb.x,
719
+ y: vb.y,
720
+ width: vb.width ?? 0,
721
+ height: vb.height ?? 0
722
+ };
723
+ }
724
+ function plotRightOf(props) {
725
+ const parent = boxOf(props, "parentViewBox");
726
+ return parent ? parent.x + parent.width : void 0;
727
+ }
728
+ function ThresholdLabel(props) {
729
+ const { label, tone, vertical } = props;
730
+ const box = boxOf(props);
731
+ if (!box) return null;
732
+ const fill = v(TONE_TOKENS[tone].text);
733
+ if (vertical) return /* @__PURE__ */ jsx("text", {
734
+ x: box.x + 4,
735
+ y: box.y + 12,
736
+ fill,
737
+ fontSize: 11,
738
+ fontWeight: 500,
739
+ "data-chart-threshold-label": "",
740
+ children: label
741
+ });
742
+ return /* @__PURE__ */ jsx("text", {
743
+ x: box.x + box.width - 4,
744
+ y: box.y - 5,
745
+ textAnchor: "end",
746
+ fill,
747
+ fontSize: 11,
748
+ fontWeight: 500,
749
+ "data-chart-threshold-label": "",
750
+ children: label
751
+ });
752
+ }
753
+ function AnnotationMarker({ box, annotation, active, onActive, plotRight }) {
754
+ const tone = annotation.tone ?? "neutral";
755
+ const key = annotationKey(annotation);
756
+ const tip = annotation.description ? `${annotation.label}: ${annotation.description}` : annotation.label;
757
+ const tipWidth = Math.min(240, Math.max(90, tip.length * 6.2 + 16));
758
+ const tipX = plotRight != null && box.x + tipWidth + 12 > plotRight ? box.x - tipWidth - 8 : box.x + 8;
759
+ return /* @__PURE__ */ jsxs("g", {
760
+ "aria-hidden": true,
761
+ "data-chart-annotation": key,
762
+ "data-active": active || void 0,
763
+ onPointerEnter: () => onActive(key),
764
+ onPointerLeave: () => onActive(null),
765
+ style: { cursor: "default" },
766
+ children: [
767
+ /* @__PURE__ */ jsx("title", { children: tip }),
768
+ /* @__PURE__ */ jsx("path", {
769
+ d: `M${box.x},${box.y} l7,4 l-7,4 z`,
770
+ fill: v(TONE_TOKENS[tone].stroke)
771
+ }),
772
+ /* @__PURE__ */ jsx("text", {
773
+ x: box.x + 10,
774
+ y: box.y + 8,
775
+ fill: v(TONE_TOKENS[tone].text),
776
+ fontSize: 11,
777
+ fontWeight: active ? 600 : 500,
778
+ children: annotation.label
779
+ }),
780
+ /* @__PURE__ */ jsx("rect", {
781
+ x: box.x - 6,
782
+ y: box.y,
783
+ width: 12,
784
+ height: Math.max(box.height, 1),
785
+ fill: "transparent"
786
+ }),
787
+ active && /* @__PURE__ */ jsxs("g", {
788
+ "data-chart-annotation-tooltip": "",
789
+ children: [/* @__PURE__ */ jsx("rect", {
790
+ x: tipX,
791
+ y: box.y + 14,
792
+ width: tipWidth,
793
+ height: 24,
794
+ rx: 6,
795
+ fill: v("--popover"),
796
+ stroke: v("--border")
797
+ }), /* @__PURE__ */ jsx("text", {
798
+ x: tipX + 8,
799
+ y: box.y + 30,
800
+ fill: v("--popover-foreground"),
801
+ fontSize: 11.5,
802
+ children: tip
803
+ })]
804
+ })
805
+ ]
806
+ });
807
+ }
808
+ /** Recharts clones a label element with the line's `viewBox` (and the plot's `parentViewBox`). */
809
+ function AnnotationLabel(props) {
810
+ const box = boxOf(props);
811
+ if (!box) return null;
812
+ return /* @__PURE__ */ jsx(AnnotationMarker, {
813
+ box,
814
+ plotRight: plotRightOf(props),
815
+ annotation: props.annotation,
816
+ active: props.active,
817
+ onActive: props.onActive
818
+ });
819
+ }
820
+ /**
821
+ * Recharts `<ReferenceLine>` elements for thresholds and annotations. Returned as an array so
822
+ * they are direct children of the chart.
823
+ */
824
+ function referenceElements({ thresholds = [], annotations = [], vertical = false, categories, activeAnnotation, onActiveAnnotation, yAxisIdOf }) {
825
+ const out = [];
826
+ thresholds.forEach((th, i) => {
827
+ const tone = th.tone ?? "neutral";
828
+ const pos = vertical ? { x: th.value } : { y: th.value };
829
+ out.push(/* @__PURE__ */ jsx(ReferenceLine, {
830
+ ...pos,
831
+ ...yAxisIdOf ? { yAxisId: yAxisIdOf(th.axis ?? "left") } : {},
832
+ stroke: v(TONE_TOKENS[tone].stroke),
833
+ strokeWidth: 1.5,
834
+ strokeDasharray: "6 4",
835
+ ifOverflow: "extendDomain",
836
+ label: /* @__PURE__ */ jsx(ThresholdLabel, {
837
+ label: th.label,
838
+ tone,
839
+ vertical
840
+ })
841
+ }, `threshold-${i}`));
842
+ });
843
+ annotations.forEach((a) => {
844
+ if (!categories.has(String(a.x))) return;
845
+ const key = annotationKey(a);
846
+ const active = activeAnnotation === key;
847
+ const pos = vertical ? { y: a.x } : { x: a.x };
848
+ out.push(/* @__PURE__ */ jsx(ReferenceLine, {
849
+ ...pos,
850
+ ...yAxisIdOf ? { yAxisId: yAxisIdOf("left") } : {},
851
+ stroke: v(TONE_TOKENS[a.tone ?? "neutral"].stroke),
852
+ strokeWidth: active ? 2 : 1.5,
853
+ strokeDasharray: "2 3",
854
+ label: /* @__PURE__ */ jsx(AnnotationLabel, {
855
+ annotation: a,
856
+ active,
857
+ onActive: onActiveAnnotation
858
+ })
859
+ }, `annotation-${key}`));
860
+ });
861
+ return out;
862
+ }
863
+ /**
864
+ * Text list of the thresholds and annotations under a chart: the keyboard and screen-reader
865
+ * route to the same information as the in-plot markers. Focusing or hovering an annotation
866
+ * highlights its marker and opens its tooltip in the plot.
867
+ */
868
+ function ChartMarkers({ thresholds = [], annotations = [], valueFormatter, active = null, onActiveChange, className }) {
869
+ const id = useId();
870
+ if (thresholds.length === 0 && annotations.length === 0) return null;
871
+ const fmt = (n) => valueFormatter ? valueFormatter(n) : String(n);
872
+ return /* @__PURE__ */ jsxs("ul", {
873
+ "aria-label": "Chart markers",
874
+ className: cx("mt-2 flex flex-wrap items-center gap-x-4 gap-y-1.5 px-1 text-[12px] text-muted-foreground", className),
875
+ children: [thresholds.map((th, i) => /* @__PURE__ */ jsxs("li", {
876
+ className: "flex items-center gap-1.5",
877
+ "data-chart-marker": "threshold",
878
+ children: [/* @__PURE__ */ jsx("svg", {
879
+ "aria-hidden": true,
880
+ width: "18",
881
+ height: "4",
882
+ className: "shrink-0",
883
+ children: /* @__PURE__ */ jsx("line", {
884
+ x1: "0",
885
+ y1: "2",
886
+ x2: "18",
887
+ y2: "2",
888
+ stroke: v(TONE_TOKENS[th.tone ?? "neutral"].stroke),
889
+ strokeWidth: 1.5,
890
+ strokeDasharray: "6 4"
891
+ })
892
+ }), /* @__PURE__ */ jsxs("span", { children: [
893
+ /* @__PURE__ */ jsx("span", {
894
+ className: "sr-only",
895
+ children: "Threshold:"
896
+ }),
897
+ " ",
898
+ th.label,
899
+ " ",
900
+ /* @__PURE__ */ jsxs("span", {
901
+ className: "font-mono tabular-nums",
902
+ children: [
903
+ "(",
904
+ fmt(th.value),
905
+ ")"
906
+ ]
907
+ })
908
+ ] })]
909
+ }, `t-${i}`)), annotations.map((a) => {
910
+ const key = annotationKey(a);
911
+ const descId = `${id}-${key}`;
912
+ return /* @__PURE__ */ jsxs("li", {
913
+ "data-chart-marker": "annotation",
914
+ children: [/* @__PURE__ */ jsxs("button", {
915
+ type: "button",
916
+ "aria-describedby": a.description ? descId : void 0,
917
+ onFocus: () => onActiveChange?.(key),
918
+ onBlur: () => onActiveChange?.(null),
919
+ onPointerEnter: () => onActiveChange?.(key),
920
+ onPointerLeave: () => onActiveChange?.(null),
921
+ onKeyDown: (e) => {
922
+ if (e.key === "Escape") onActiveChange?.(null);
923
+ },
924
+ className: cx("flex items-center gap-1.5 hover:text-foreground", active === key && "text-foreground", legendFocus),
925
+ children: [/* @__PURE__ */ jsx("svg", {
926
+ "aria-hidden": true,
927
+ width: "10",
928
+ height: "10",
929
+ viewBox: "0 0 10 10",
930
+ className: "shrink-0",
931
+ children: /* @__PURE__ */ jsx("path", {
932
+ d: "M1,1 l8,4 l-8,4 z",
933
+ fill: v(TONE_TOKENS[a.tone ?? "neutral"].stroke)
934
+ })
935
+ }), /* @__PURE__ */ jsxs("span", { children: [
936
+ /* @__PURE__ */ jsx("span", {
937
+ className: "sr-only",
938
+ children: `Marker at ${String(a.x)}:`
939
+ }),
940
+ " ",
941
+ a.label
942
+ ] })]
943
+ }), a.description && /* @__PURE__ */ jsx("span", {
944
+ id: descId,
945
+ className: "sr-only",
946
+ children: a.description
947
+ })]
948
+ }, key);
949
+ })]
950
+ });
951
+ }
952
+ /** State of the highlighted annotation (shared by the plot markers and the marker list). */
953
+ function useActiveAnnotation() {
954
+ return useState(null);
955
+ }
956
+ //#endregion
957
+ //#region src/cartesian-extras.tsx
958
+ function useCartesianExtras(data, index, { thresholds = [], annotations = [], brush = false, range, defaultRange, onRangeChange, animate = false }, { valueFormatter, previewKey, vertical = false, yAxisIdOf } = {}) {
959
+ const [current, setRange] = useChartRange({
960
+ length: data.length,
961
+ range,
962
+ defaultRange,
963
+ onRangeChange
964
+ });
965
+ const [active, setActive] = useActiveAnnotation();
966
+ const reduced = usePrefersReducedMotion();
967
+ const view = brush ? data.slice(current[0], current[1] + 1) : data;
968
+ const categories = new Set(view.map((d) => String(field(d, index))));
969
+ const shown = annotations.filter((a) => categories.has(String(a.x)));
970
+ const references = referenceElements({
971
+ thresholds,
972
+ annotations: shown,
973
+ vertical,
974
+ categories,
975
+ activeAnnotation: active,
976
+ onActiveAnnotation: setActive,
977
+ yAxisIdOf
978
+ });
979
+ const labelOf = (i) => {
980
+ const d = data[i];
981
+ return d ? String(field(d, index)) : "";
982
+ };
983
+ return {
984
+ view,
985
+ references,
986
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ChartMarkers, {
987
+ thresholds,
988
+ annotations: shown,
989
+ valueFormatter,
990
+ active: active && shown.some((a) => annotationKey(a) === active) ? active : null,
991
+ onActiveChange: setActive
992
+ }), brush && /* @__PURE__ */ jsx(ChartBrush, {
993
+ length: data.length,
994
+ range: current,
995
+ onRangeChange: setRange,
996
+ labelOf,
997
+ preview: previewKey ? data.map((d) => numField(d, previewKey)) : void 0
998
+ })] }),
999
+ animation: {
1000
+ isAnimationActive: animate && !reduced,
1001
+ animationDuration: 300
1002
+ }
1003
+ };
1004
+ }
1005
+ //#endregion
1006
+ //#region src/theme.ts
1007
+ /** Series colour name → brand token. Green primary is the hero; the rest are categorical accents. */
1008
+ const SERIES_TOKENS = {
1009
+ primary: "--primary",
1010
+ emerald: "--brand-accent",
1011
+ violet: "--category-violet",
1012
+ cyan: "--category-cyan",
1013
+ amber: "--category-amber",
1014
+ rose: "--category-rose",
1015
+ info: "--info"
1016
+ };
1017
+ /**
1018
+ * Default series order: primary first, then the categorical accents ordered so every adjacent
1019
+ * pair differs in luminance by ≥ 1.5:1 in all three themes (see `palette.ts`).
1020
+ */
1021
+ const CHART_COLORS = [
1022
+ "primary",
1023
+ "violet",
1024
+ "cyan",
1025
+ "rose",
1026
+ "amber"
1027
+ ];
1028
+ /** `hsl(var(--token))`: resolved by the browser (SVG fill/stroke and inline styles), so it is SSR-safe and follows theme switches. */
1029
+ const cssVarColor = (token, alpha) => alpha == null ? `hsl(var(${token}))` : `hsl(var(${token}) / ${alpha})`;
1030
+ function buildTheme(theme, c) {
1031
+ return {
1032
+ theme,
1033
+ grid: c("--border"),
1034
+ axis: c("--muted-foreground", .5),
1035
+ text: c("--muted-foreground"),
1036
+ cursor: c("--muted-foreground", .32),
1037
+ barCursor: c("--secondary", .55),
1038
+ surface: c("--card"),
1039
+ color: (name, alpha) => c(SERIES_TOKENS[name] ?? SERIES_TOKENS.primary, alpha)
1040
+ };
1041
+ }
1042
+ const varThemes = /* @__PURE__ */ new Map();
1043
+ let resolvedSnapshot = null;
1044
+ function varTheme(theme) {
1045
+ let t = varThemes.get(theme);
1046
+ if (!t) {
1047
+ t = buildTheme(theme, cssVarColor);
1048
+ varThemes.set(theme, t);
1049
+ }
1050
+ return t;
1051
+ }
1052
+ function subscribe(onChange) {
1053
+ return observeTheme(() => {
1054
+ resolvedSnapshot = null;
1055
+ onChange();
1056
+ });
1057
+ }
1058
+ const SERVER_THEME = varTheme("dark");
1059
+ const getServerSnapshot = () => SERVER_THEME;
1060
+ const getVarSnapshot = () => varTheme(currentTheme());
1061
+ function getResolvedSnapshot() {
1062
+ const theme = currentTheme();
1063
+ if (!resolvedSnapshot || resolvedSnapshot.theme !== theme) resolvedSnapshot = buildTheme(theme, (token, alpha) => tokenColor(token, alpha));
1064
+ return resolvedSnapshot;
1065
+ }
1066
+ /**
1067
+ * Brand colours for Recharts. By default every colour is a `hsl(var(--token))` string: SVG
1068
+ * fill/stroke attributes and inline styles resolve it, so server and client render the same
1069
+ * markup and a theme switch re-skins the chart without re-rendering. `theme` follows the
1070
+ * <html> class after mount. With `{ resolved: true }` colours are read from the live CSS
1071
+ * variables once mounted (the server and hydration render use the `var()` strings).
1072
+ */
1073
+ function useChartTheme({ resolved = false } = {}) {
1074
+ return useSyncExternalStore(subscribe, resolved ? getResolvedSnapshot : getVarSnapshot, getServerSnapshot);
1075
+ }
1076
+ /** Colour for the i-th series, cycling through `colors`. */
1077
+ function seriesColor(t, colors, i) {
1078
+ const name = colors.length ? colors[i % colors.length] : void 0;
1079
+ return t.color(name ?? "primary");
1080
+ }
1081
+ //#endregion
1082
+ //#region src/AreaChart.tsx
1083
+ /** Area chart: flat fills (no gradient), no entry animation, interactive legend. */
1084
+ function AreaChart({ data, index, categories, colors = CHART_COLORS, valueFormatter = identity, stacked = false, showLegend = true, showGrid = true, showYAxis = true, curve = "monotone", height = 288, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false, thresholds, annotations, brush, range, defaultRange, onRangeChange, animate }) {
1085
+ const t = useChartTheme();
1086
+ const [hidden, toggle] = useHiddenSeries();
1087
+ const x = useCartesianExtras(data, index, {
1088
+ thresholds,
1089
+ annotations,
1090
+ brush,
1091
+ range,
1092
+ defaultRange,
1093
+ onRangeChange,
1094
+ animate
1095
+ }, {
1096
+ valueFormatter,
1097
+ previewKey: categories[0]
1098
+ });
1099
+ const col = (i) => seriesColor(t, colors, i);
1100
+ const legend = categories.map((c, i) => ({
1101
+ key: c,
1102
+ label: c,
1103
+ color: col(i)
1104
+ }));
1105
+ const name = accessibleName("Area chart", ariaLabel, title, categories);
1106
+ return /* @__PURE__ */ jsx(ChartFrame, {
1107
+ label: name,
1108
+ state: resolveState(state, data.length),
1109
+ emptyMessage,
1110
+ errorMessage,
1111
+ onRetry,
1112
+ table: dataTable && cartesianTable(name, x.view, index, categories, (v) => formatAny(v, valueFormatter)),
1113
+ height,
1114
+ className,
1115
+ footer: x.footer,
1116
+ legend: showLegend && /* @__PURE__ */ jsx(ChartLegend, {
1117
+ className: "mb-3 px-1",
1118
+ items: legend,
1119
+ hidden,
1120
+ onToggle: toggle
1121
+ }),
1122
+ children: /* @__PURE__ */ jsxs(AreaChart$1, {
1123
+ data: x.view,
1124
+ margin: CHART_MARGIN,
1125
+ accessibilityLayer: true,
1126
+ title,
1127
+ desc: description,
1128
+ children: [
1129
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1130
+ strokeDasharray: "3 3",
1131
+ stroke: t.grid,
1132
+ vertical: false
1133
+ }),
1134
+ /* @__PURE__ */ jsx(XAxis, {
1135
+ dataKey: index,
1136
+ ...axisProps(t),
1137
+ dy: 8,
1138
+ minTickGap: 20
1139
+ }),
1140
+ showYAxis && /* @__PURE__ */ jsx(YAxis, {
1141
+ ...axisProps(t),
1142
+ width: 50,
1143
+ tickFormatter: valueFormatter
1144
+ }),
1145
+ x.references,
1146
+ /* @__PURE__ */ jsx(Tooltip, {
1147
+ isAnimationActive: false,
1148
+ cursor: {
1149
+ stroke: t.cursor,
1150
+ strokeWidth: 1
1151
+ },
1152
+ content: tooltipContent({ valueFormatter })
1153
+ }),
1154
+ categories.map((c, i) => /* @__PURE__ */ jsx(Area, {
1155
+ type: curve,
1156
+ dataKey: c,
1157
+ name: c,
1158
+ hide: hidden.has(c),
1159
+ stackId: stacked ? "1" : void 0,
1160
+ stroke: col(i),
1161
+ strokeWidth: 2,
1162
+ fill: col(i),
1163
+ fillOpacity: stacked ? .82 : .14,
1164
+ dot: false,
1165
+ activeDot: {
1166
+ r: 3.5,
1167
+ strokeWidth: 0
1168
+ },
1169
+ ...x.animation
1170
+ }, c))
1171
+ ]
1172
+ })
1173
+ });
1174
+ }
1175
+ //#endregion
1176
+ //#region src/BarChart.tsx
1177
+ /** Bar chart: vertical, horizontal or stacked; flat fills, no animation. */
1178
+ function BarChart({ data, index, categories, colors = CHART_COLORS, valueFormatter = identity, stacked = false, layout = "horizontal", showLegend = true, showGrid = true, showYAxis = true, height = 288, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false, thresholds, annotations, brush, range, defaultRange, onRangeChange, animate }) {
1179
+ const t = useChartTheme();
1180
+ const [hidden, toggle] = useHiddenSeries();
1181
+ const col = (i) => seriesColor(t, colors, i);
1182
+ const legend = categories.map((c, i) => ({
1183
+ key: c,
1184
+ label: c,
1185
+ color: col(i)
1186
+ }));
1187
+ const vertical = layout === "vertical";
1188
+ const x = useCartesianExtras(data, index, {
1189
+ thresholds,
1190
+ annotations,
1191
+ brush,
1192
+ range,
1193
+ defaultRange,
1194
+ onRangeChange,
1195
+ animate
1196
+ }, {
1197
+ valueFormatter,
1198
+ previewKey: categories[0],
1199
+ vertical
1200
+ });
1201
+ const radius = stacked ? 0 : vertical ? [
1202
+ 0,
1203
+ 3,
1204
+ 3,
1205
+ 0
1206
+ ] : [
1207
+ 3,
1208
+ 3,
1209
+ 0,
1210
+ 0
1211
+ ];
1212
+ const name = accessibleName("Bar chart", ariaLabel, title, categories);
1213
+ return /* @__PURE__ */ jsx(ChartFrame, {
1214
+ label: name,
1215
+ state: resolveState(state, data.length),
1216
+ emptyMessage,
1217
+ errorMessage,
1218
+ onRetry,
1219
+ table: dataTable && cartesianTable(name, x.view, index, categories, (v) => formatAny(v, valueFormatter)),
1220
+ height,
1221
+ className,
1222
+ footer: x.footer,
1223
+ legend: showLegend && categories.length > 1 && /* @__PURE__ */ jsx(ChartLegend, {
1224
+ className: "mb-3 px-1",
1225
+ items: legend,
1226
+ hidden,
1227
+ onToggle: toggle
1228
+ }),
1229
+ children: /* @__PURE__ */ jsxs(BarChart$1, {
1230
+ data: x.view,
1231
+ layout,
1232
+ margin: CHART_MARGIN,
1233
+ barCategoryGap: vertical ? "22%" : "28%",
1234
+ accessibilityLayer: true,
1235
+ title,
1236
+ desc: description,
1237
+ children: [
1238
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1239
+ strokeDasharray: "3 3",
1240
+ stroke: t.grid,
1241
+ vertical,
1242
+ horizontal: !vertical
1243
+ }),
1244
+ vertical ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
1245
+ type: "number",
1246
+ ...axisProps(t),
1247
+ tickFormatter: valueFormatter
1248
+ }), /* @__PURE__ */ jsx(YAxis, {
1249
+ type: "category",
1250
+ dataKey: index,
1251
+ ...axisProps(t),
1252
+ width: 92
1253
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
1254
+ dataKey: index,
1255
+ ...axisProps(t),
1256
+ dy: 8,
1257
+ minTickGap: 16
1258
+ }), showYAxis && /* @__PURE__ */ jsx(YAxis, {
1259
+ ...axisProps(t),
1260
+ width: 50,
1261
+ tickFormatter: valueFormatter
1262
+ })] }),
1263
+ x.references,
1264
+ /* @__PURE__ */ jsx(Tooltip, {
1265
+ isAnimationActive: false,
1266
+ cursor: { fill: t.barCursor },
1267
+ content: tooltipContent({ valueFormatter })
1268
+ }),
1269
+ categories.map((c, i) => /* @__PURE__ */ jsx(Bar, {
1270
+ dataKey: c,
1271
+ name: c,
1272
+ hide: hidden.has(c),
1273
+ stackId: stacked ? "1" : void 0,
1274
+ fill: col(i),
1275
+ radius,
1276
+ maxBarSize: vertical ? 22 : 46,
1277
+ ...x.animation
1278
+ }, c))
1279
+ ]
1280
+ })
1281
+ });
1282
+ }
1283
+ //#endregion
1284
+ //#region src/LineChart.tsx
1285
+ const asTick = (v) => typeof v === "number" ? v : String(v);
1286
+ /** Line chart: 2px strokes, no dots until hover, no animation. */
1287
+ function LineChart({ data, index, categories, colors = CHART_COLORS, valueFormatter = identity, showLegend = true, showGrid = true, showYAxis = true, startEndOnly = false, curve = "monotone", height = 288, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false, thresholds, annotations, brush, range, defaultRange, onRangeChange, animate }) {
1288
+ const t = useChartTheme();
1289
+ const [hidden, toggle] = useHiddenSeries();
1290
+ const x = useCartesianExtras(data, index, {
1291
+ thresholds,
1292
+ annotations,
1293
+ brush,
1294
+ range,
1295
+ defaultRange,
1296
+ onRangeChange,
1297
+ animate
1298
+ }, {
1299
+ valueFormatter,
1300
+ previewKey: categories[0]
1301
+ });
1302
+ const col = (i) => seriesColor(t, colors, i);
1303
+ const legend = categories.map((c, i) => ({
1304
+ key: c,
1305
+ label: c,
1306
+ color: col(i)
1307
+ }));
1308
+ const first = x.view[0];
1309
+ const last = x.view[x.view.length - 1];
1310
+ const xTicks = startEndOnly && first && last ? [asTick(field(first, index)), asTick(field(last, index))] : void 0;
1311
+ const name = accessibleName("Line chart", ariaLabel, title, categories);
1312
+ return /* @__PURE__ */ jsx(ChartFrame, {
1313
+ label: name,
1314
+ state: resolveState(state, data.length),
1315
+ emptyMessage,
1316
+ errorMessage,
1317
+ onRetry,
1318
+ table: dataTable && cartesianTable(name, x.view, index, categories, (v) => formatAny(v, valueFormatter)),
1319
+ height,
1320
+ className,
1321
+ footer: x.footer,
1322
+ legend: showLegend && /* @__PURE__ */ jsx(ChartLegend, {
1323
+ className: "mb-3 px-1",
1324
+ items: legend,
1325
+ hidden,
1326
+ onToggle: toggle
1327
+ }),
1328
+ children: /* @__PURE__ */ jsxs(LineChart$1, {
1329
+ data: x.view,
1330
+ margin: CHART_MARGIN,
1331
+ accessibilityLayer: true,
1332
+ title,
1333
+ desc: description,
1334
+ children: [
1335
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1336
+ strokeDasharray: "3 3",
1337
+ stroke: t.grid,
1338
+ vertical: false
1339
+ }),
1340
+ /* @__PURE__ */ jsx(XAxis, {
1341
+ dataKey: index,
1342
+ ...axisProps(t),
1343
+ dy: 8,
1344
+ minTickGap: startEndOnly ? 0 : 20,
1345
+ ticks: xTicks,
1346
+ interval: startEndOnly ? "preserveStartEnd" : void 0
1347
+ }),
1348
+ showYAxis && /* @__PURE__ */ jsx(YAxis, {
1349
+ ...axisProps(t),
1350
+ width: 50,
1351
+ tickFormatter: valueFormatter
1352
+ }),
1353
+ x.references,
1354
+ /* @__PURE__ */ jsx(Tooltip, {
1355
+ isAnimationActive: false,
1356
+ cursor: {
1357
+ stroke: t.cursor,
1358
+ strokeWidth: 1
1359
+ },
1360
+ content: tooltipContent({ valueFormatter })
1361
+ }),
1362
+ categories.map((c, i) => /* @__PURE__ */ jsx(Line, {
1363
+ type: curve,
1364
+ dataKey: c,
1365
+ name: c,
1366
+ hide: hidden.has(c),
1367
+ stroke: col(i),
1368
+ strokeWidth: 2,
1369
+ dot: false,
1370
+ activeDot: {
1371
+ r: 3.5,
1372
+ strokeWidth: 0
1373
+ },
1374
+ ...x.animation
1375
+ }, c))
1376
+ ]
1377
+ })
1378
+ });
1379
+ }
1380
+ //#endregion
1381
+ //#region src/ComboChart.tsx
1382
+ /** Combo chart: bars on the left axis, a line on the right axis. */
1383
+ function ComboChart({ data, index, barSeries, lineSeries, barColor = "primary", lineColor = "amber", barFormatter = identity, lineFormatter = identity, showLegend = true, showGrid = true, height = 300, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false, thresholds, annotations, brush, range, defaultRange, onRangeChange, animate }) {
1384
+ const t = useChartTheme();
1385
+ const [hidden, toggle] = useHiddenSeries();
1386
+ const x = useCartesianExtras(data, index, {
1387
+ thresholds,
1388
+ annotations,
1389
+ brush,
1390
+ range,
1391
+ defaultRange,
1392
+ onRangeChange,
1393
+ animate
1394
+ }, {
1395
+ valueFormatter: barFormatter,
1396
+ previewKey: barSeries,
1397
+ yAxisIdOf: (axis) => axis
1398
+ });
1399
+ const bc = t.color(barColor);
1400
+ const lc = t.color(lineColor);
1401
+ const legend = [{
1402
+ key: barSeries,
1403
+ label: barSeries,
1404
+ color: bc
1405
+ }, {
1406
+ key: lineSeries,
1407
+ label: lineSeries,
1408
+ color: lc
1409
+ }];
1410
+ const fmtSeries = (v, name) => formatAny(v, name === lineSeries ? lineFormatter : barFormatter);
1411
+ const name = accessibleName("Combo chart", ariaLabel, title, [barSeries, lineSeries]);
1412
+ return /* @__PURE__ */ jsx(ChartFrame, {
1413
+ label: name,
1414
+ state: resolveState(state, data.length),
1415
+ emptyMessage,
1416
+ errorMessage,
1417
+ onRetry,
1418
+ table: dataTable && cartesianTable(name, x.view, index, [barSeries, lineSeries], fmtSeries),
1419
+ height,
1420
+ className,
1421
+ footer: x.footer,
1422
+ legend: showLegend && /* @__PURE__ */ jsx(ChartLegend, {
1423
+ className: "mb-3 px-1",
1424
+ items: legend,
1425
+ hidden,
1426
+ onToggle: toggle
1427
+ }),
1428
+ children: /* @__PURE__ */ jsxs(ComposedChart, {
1429
+ data: x.view,
1430
+ margin: {
1431
+ top: 6,
1432
+ right: 6,
1433
+ left: 0,
1434
+ bottom: 0
1435
+ },
1436
+ barCategoryGap: "30%",
1437
+ accessibilityLayer: true,
1438
+ title,
1439
+ desc: description,
1440
+ children: [
1441
+ showGrid && /* @__PURE__ */ jsx(CartesianGrid, {
1442
+ strokeDasharray: "3 3",
1443
+ stroke: t.grid,
1444
+ vertical: false
1445
+ }),
1446
+ /* @__PURE__ */ jsx(XAxis, {
1447
+ dataKey: index,
1448
+ ...axisProps(t),
1449
+ dy: 8,
1450
+ minTickGap: 16
1451
+ }),
1452
+ /* @__PURE__ */ jsx(YAxis, {
1453
+ yAxisId: "left",
1454
+ ...axisProps(t),
1455
+ width: 48,
1456
+ tickFormatter: barFormatter
1457
+ }),
1458
+ /* @__PURE__ */ jsx(YAxis, {
1459
+ yAxisId: "right",
1460
+ orientation: "right",
1461
+ ...axisProps(t),
1462
+ width: 48,
1463
+ tickFormatter: lineFormatter
1464
+ }),
1465
+ x.references,
1466
+ /* @__PURE__ */ jsx(Tooltip, {
1467
+ isAnimationActive: false,
1468
+ cursor: { fill: t.barCursor },
1469
+ content: tooltipContent({ seriesFormatter: fmtSeries })
1470
+ }),
1471
+ /* @__PURE__ */ jsx(Bar, {
1472
+ yAxisId: "left",
1473
+ dataKey: barSeries,
1474
+ name: barSeries,
1475
+ hide: hidden.has(barSeries),
1476
+ fill: bc,
1477
+ radius: [
1478
+ 3,
1479
+ 3,
1480
+ 0,
1481
+ 0
1482
+ ],
1483
+ maxBarSize: 42,
1484
+ ...x.animation
1485
+ }),
1486
+ /* @__PURE__ */ jsx(Line, {
1487
+ yAxisId: "right",
1488
+ type: "monotone",
1489
+ dataKey: lineSeries,
1490
+ name: lineSeries,
1491
+ hide: hidden.has(lineSeries),
1492
+ stroke: lc,
1493
+ strokeWidth: 2,
1494
+ dot: false,
1495
+ activeDot: {
1496
+ r: 3.5,
1497
+ strokeWidth: 0
1498
+ },
1499
+ ...x.animation
1500
+ })
1501
+ ]
1502
+ })
1503
+ });
1504
+ }
1505
+ //#endregion
1506
+ //#region src/DonutChart.tsx
1507
+ /** Donut / pie with a clickable value legend; hidden slices drop out of the total. */
1508
+ function DonutChart({ data, index, category, colors = CHART_COLORS, valueFormatter = identity, variant = "donut", showLegend = true, centerLabel, height = 264, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false }) {
1509
+ const t = useChartTheme();
1510
+ const [hidden, toggle] = useHiddenSeries();
1511
+ const slices = data.map((d, i) => ({
1512
+ name: String(field(d, index)),
1513
+ value: numField(d, category),
1514
+ fill: seriesColor(t, colors, i)
1515
+ }));
1516
+ const shown = slices.filter((s) => !hidden.has(s.name));
1517
+ const total = shown.reduce((sum, s) => sum + s.value, 0);
1518
+ const isDonut = variant === "donut";
1519
+ const name = accessibleName(isDonut ? "Donut chart" : "Pie chart", ariaLabel, title, slices.map((s) => s.name));
1520
+ const effective = resolveState(state, slices.length);
1521
+ const placeholder = chartStateView(effective, {
1522
+ height,
1523
+ emptyMessage,
1524
+ errorMessage,
1525
+ onRetry
1526
+ });
1527
+ if (placeholder) return /* @__PURE__ */ jsx("div", {
1528
+ role: "figure",
1529
+ "aria-label": name,
1530
+ "aria-busy": effective === "loading" || void 0,
1531
+ className: cx("w-full font-sans", className),
1532
+ children: placeholder
1533
+ });
1534
+ return /* @__PURE__ */ jsxs("div", {
1535
+ role: "figure",
1536
+ "aria-label": name,
1537
+ className: cx("flex flex-col items-center gap-6 font-sans sm:flex-row", className),
1538
+ children: [
1539
+ /* @__PURE__ */ jsxs("div", {
1540
+ className: "relative shrink-0",
1541
+ style: {
1542
+ width: height,
1543
+ height
1544
+ },
1545
+ children: [/* @__PURE__ */ jsx(ResponsiveContainer, {
1546
+ width: "100%",
1547
+ height: "100%",
1548
+ children: /* @__PURE__ */ jsxs(PieChart, {
1549
+ accessibilityLayer: true,
1550
+ title,
1551
+ desc: description,
1552
+ children: [/* @__PURE__ */ jsx(Pie, {
1553
+ data: shown,
1554
+ dataKey: "value",
1555
+ nameKey: "name",
1556
+ cx: "50%",
1557
+ cy: "50%",
1558
+ innerRadius: isDonut ? "64%" : 0,
1559
+ outerRadius: "94%",
1560
+ paddingAngle: shown.length > 1 ? 1.5 : 0,
1561
+ stroke: t.surface,
1562
+ strokeWidth: 2,
1563
+ isAnimationActive: false
1564
+ }), /* @__PURE__ */ jsx(Tooltip, {
1565
+ isAnimationActive: false,
1566
+ content: tooltipContent({ valueFormatter })
1567
+ })]
1568
+ })
1569
+ }), isDonut && centerLabel && /* @__PURE__ */ jsxs("div", {
1570
+ className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center",
1571
+ children: [/* @__PURE__ */ jsx("div", {
1572
+ className: "font-mono text-[20px] font-semibold tabular-nums text-foreground",
1573
+ children: valueFormatter(total)
1574
+ }), /* @__PURE__ */ jsx("div", {
1575
+ className: "mt-0.5 text-[11px] text-muted-foreground",
1576
+ children: centerLabel
1577
+ })]
1578
+ })]
1579
+ }),
1580
+ showLegend && /* @__PURE__ */ jsx("div", {
1581
+ className: "flex w-full min-w-0 flex-1 flex-col gap-2",
1582
+ children: slices.map((s) => {
1583
+ const off = hidden.has(s.name);
1584
+ return /* @__PURE__ */ jsxs("button", {
1585
+ type: "button",
1586
+ "aria-pressed": !off,
1587
+ onClick: () => toggle(s.name),
1588
+ className: cx("group flex items-center justify-between gap-3", off && "opacity-40", "rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring"),
1589
+ children: [/* @__PURE__ */ jsxs("span", {
1590
+ className: "flex min-w-0 items-center gap-2",
1591
+ children: [/* @__PURE__ */ jsx("span", {
1592
+ "aria-hidden": true,
1593
+ className: cx("h-2.5 w-2.5 shrink-0 rounded-[3px]", off && "grayscale"),
1594
+ style: { background: s.fill }
1595
+ }), /* @__PURE__ */ jsx("span", {
1596
+ className: cx("truncate text-[12.5px] text-muted-foreground", off ? "line-through" : "transition-colors group-hover:text-foreground"),
1597
+ children: s.name
1598
+ })]
1599
+ }), /* @__PURE__ */ jsx("span", {
1600
+ className: "shrink-0 font-mono text-[12px] font-medium tabular-nums text-foreground",
1601
+ children: valueFormatter(s.value)
1602
+ })]
1603
+ }, s.name);
1604
+ })
1605
+ }),
1606
+ dataTable && /* @__PURE__ */ jsx(ChartDataTable, {
1607
+ caption: name,
1608
+ columns: [index, category],
1609
+ rows: slices.map((sl) => [sl.name, valueFormatter(sl.value)])
1610
+ })
1611
+ ]
1612
+ });
1613
+ }
1614
+ //#endregion
1615
+ //#region src/BarList.tsx
1616
+ const BAR_LIST_BG = {
1617
+ primary: "bg-primary/20",
1618
+ violet: "bg-category-violet/20",
1619
+ cyan: "bg-category-cyan/20",
1620
+ amber: "bg-category-amber/20",
1621
+ rose: "bg-category-rose/20"
1622
+ };
1623
+ /** Axis-less ranked list: label inside a token-tinted bar, value on the right. */
1624
+ function BarList({ data, index, category, valueFormatter = identity, color = "primary", sortOrder = "desc", className, "aria-label": ariaLabel = "Bar list", state, emptyMessage, errorMessage, onRetry }) {
1625
+ const effective = resolveState(state, data.length);
1626
+ const placeholder = chartStateView(effective, {
1627
+ height: 160,
1628
+ emptyMessage,
1629
+ errorMessage,
1630
+ onRetry
1631
+ });
1632
+ if (placeholder) return /* @__PURE__ */ jsx("div", {
1633
+ role: "figure",
1634
+ "aria-label": ariaLabel,
1635
+ "aria-busy": effective === "loading" || void 0,
1636
+ className: cx("font-sans", className),
1637
+ children: placeholder
1638
+ });
1639
+ const rows = data.slice();
1640
+ if (sortOrder === "desc") rows.sort((a, b) => numField(b, category) - numField(a, category));
1641
+ else if (sortOrder === "asc") rows.sort((a, b) => numField(a, category) - numField(b, category));
1642
+ const max = Math.max(...rows.map((d) => numField(d, category)), 1);
1643
+ return /* @__PURE__ */ jsx("ul", {
1644
+ "aria-label": ariaLabel,
1645
+ className: cx("flex flex-col gap-2 font-sans", className),
1646
+ children: rows.map((r, i) => {
1647
+ const value = numField(r, category);
1648
+ const pct = Math.max(value / max * 100, 1.5);
1649
+ return /* @__PURE__ */ jsxs("li", {
1650
+ className: "flex items-center gap-3",
1651
+ children: [/* @__PURE__ */ jsxs("div", {
1652
+ className: "relative h-9 min-w-0 flex-1",
1653
+ children: [/* @__PURE__ */ jsx("div", {
1654
+ "aria-hidden": true,
1655
+ className: cx("absolute inset-y-0 left-0 rounded-md", BAR_LIST_BG[color]),
1656
+ style: { width: `${pct}%` }
1657
+ }), /* @__PURE__ */ jsx("div", {
1658
+ className: "relative z-10 flex h-full items-center px-2.5",
1659
+ children: /* @__PURE__ */ jsx("span", {
1660
+ className: "truncate text-[13px] text-foreground",
1661
+ children: String(field(r, index))
1662
+ })
1663
+ })]
1664
+ }), /* @__PURE__ */ jsx("span", {
1665
+ className: "w-16 shrink-0 text-right font-mono text-[13px] tabular-nums text-foreground",
1666
+ children: valueFormatter(value)
1667
+ })]
1668
+ }, i);
1669
+ })
1670
+ });
1671
+ }
1672
+ //#endregion
1673
+ //#region src/palette.ts
1674
+ /**
1675
+ * Data-viz palette built ONLY from the frozen brand tokens.
1676
+ *
1677
+ * Categorical: `SAFE_CHART_COLORS` orders the five accents so that every adjacent pair differs
1678
+ * in relative luminance by at least 1.5:1 in dark, light and high_contrast (asserted by
1679
+ * `palette.test.ts` against `@gntik-ai/tokens/brand.css`). It is the default `CHART_COLORS`
1680
+ * order of every chart; this name is kept as an alias.
1681
+ *
1682
+ * Colour-blind safety: hue alone is never the only channel. Charts pair colour with position,
1683
+ * labels or the legend text; use at most five categorical series, prefer a sequential ramp
1684
+ * (one hue, varying lightness) for ordered data, and a diverging ramp (destructive ↔ muted ↔
1685
+ * primary, i.e. red ↔ green, which deutan/protan viewers confuse) only together with value
1686
+ * labels, a signed legend or the `dataTable` fallback — lightness still separates the ends.
1687
+ */
1688
+ const SAFE_CHART_COLORS = CHART_COLORS;
1689
+ const round = (n) => Math.round(n * 1e3) / 1e3;
1690
+ const pct = (n) => `${Math.round(n * 1e3) / 10}%`;
1691
+ const solid = (token) => `hsl(var(${token}))`;
1692
+ /** Mixes `token` into `base` at `weight` (0–1); `weight` 1 is the plain token. */
1693
+ function mix(token, base, weight, mode) {
1694
+ if (weight >= 1) return solid(token);
1695
+ if (mode === "alpha") return `hsl(var(${token}) / ${round(weight)})`;
1696
+ return `color-mix(in oklab, ${solid(token)} ${pct(weight)}, ${solid(base)})`;
1697
+ }
1698
+ function clamp01(n) {
1699
+ return Math.min(1, Math.max(0, n));
1700
+ }
1701
+ /**
1702
+ * `n` colours from faint to the full token, for ordered magnitudes (heatmaps, choropleths,
1703
+ * funnel stages). Every value is a CSS colour string usable as SVG fill/stroke or inline style.
1704
+ */
1705
+ function sequentialRamp(n, options = {}) {
1706
+ const { color = "primary", surface = "--card", mode = "mix", min = .14 } = options;
1707
+ const token = SERIES_TOKENS[color];
1708
+ const count = Math.max(0, Math.floor(n));
1709
+ const lo = clamp01(min);
1710
+ return Array.from({ length: count }, (_, i) => {
1711
+ const t = count === 1 ? 1 : lo + (1 - lo) * i / (count - 1);
1712
+ return mix(token, surface, t, mode);
1713
+ });
1714
+ }
1715
+ /**
1716
+ * `n` colours from `negative` (index 0) through the neutral `mid` to `positive` (last index),
1717
+ * for signed values around a meaningful centre (deltas, budgets, sentiment). With an odd `n`
1718
+ * the middle step is the plain neutral token.
1719
+ */
1720
+ function divergingRamp(n, options = {}) {
1721
+ const { negative = "--destructive", positive = "--primary", mid = "--muted", mode = "mix", min = .18 } = options;
1722
+ const count = Math.max(0, Math.floor(n));
1723
+ const lo = clamp01(min);
1724
+ const dMin = count % 2 ? 2 / (count - 1) : 1 / (count - 1);
1725
+ return Array.from({ length: count }, (_, i) => {
1726
+ if (count === 1) return solid(mid);
1727
+ const d = i / (count - 1) * 2 - 1;
1728
+ const a = Math.abs(d);
1729
+ if (a < 1e-9) return solid(mid);
1730
+ const strength = a >= 1 - 1e-9 ? 1 : lo + (1 - lo) * (a - dMin) / (1 - dMin);
1731
+ return mix(d < 0 ? negative : positive, mid, strength, mode);
1732
+ });
1733
+ }
1734
+ /**
1735
+ * Index of the ramp step for `value`. `domain` is `[min, max]` (sequential) or
1736
+ * `[min, mid, max]` (diverging: `mid` maps to the centre of the ramp).
1737
+ */
1738
+ function rampIndex(value, domain, steps) {
1739
+ if (steps <= 1 || !Number.isFinite(value)) return 0;
1740
+ let t;
1741
+ if (domain.length === 3) {
1742
+ const [lo, mid, hi] = domain;
1743
+ t = value <= mid ? .5 * (mid === lo ? 1 : (value - lo) / (mid - lo)) : .5 + .5 * (hi === mid ? 1 : (value - mid) / (hi - mid));
1744
+ } else {
1745
+ const [lo, hi] = domain;
1746
+ t = hi === lo ? 1 : (value - lo) / (hi - lo);
1747
+ }
1748
+ return Math.min(steps - 1, Math.floor(clamp01(t) * steps));
1749
+ }
1750
+ /** Colour of `value` on `ramp` (see `rampIndex`). */
1751
+ function rampColor(value, domain, ramp) {
1752
+ return ramp[rampIndex(value, domain, ramp.length)] ?? ramp[0] ?? solid("--muted");
1753
+ }
1754
+ //#endregion
1755
+ //#region src/FunnelChart.tsx
1756
+ /** Stage maths: conversion against the first stage and drop-off against the previous one. */
1757
+ function funnelStages(values, fills = []) {
1758
+ const first = values[0]?.value ?? 0;
1759
+ return values.map((s, i) => {
1760
+ const prev = i === 0 ? s.value : values[i - 1]?.value ?? 0;
1761
+ return {
1762
+ name: s.name,
1763
+ value: s.value,
1764
+ conversion: first > 0 ? s.value / first : 0,
1765
+ dropOff: i === 0 || prev <= 0 ? 0 : Math.max(0, (prev - s.value) / prev),
1766
+ fill: fills[i] ?? fills[fills.length - 1] ?? "hsl(var(--primary))"
1767
+ };
1768
+ });
1769
+ }
1770
+ /** Staged conversion funnel: trapezoids in a one-hue ramp plus a stage list with drop-off. */
1771
+ function FunnelChart({ data, index, category, color = "primary", valueFormatter = identity, showStages = true, height = 260, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false }) {
1772
+ const t = useChartTheme();
1773
+ const ramp = sequentialRamp(Math.max(data.length, 1), {
1774
+ color,
1775
+ min: .35
1776
+ }).reverse();
1777
+ const stages = funnelStages(data.map((d) => ({
1778
+ name: String(field(d, index)),
1779
+ value: numField(d, category)
1780
+ })), ramp);
1781
+ const name = accessibleName("Funnel chart", ariaLabel, title, stages.map((s) => s.name));
1782
+ const effective = resolveState(state, data.length);
1783
+ const placeholder = chartStateView(effective, {
1784
+ height,
1785
+ emptyMessage,
1786
+ errorMessage,
1787
+ onRetry
1788
+ });
1789
+ const byName = new Map(stages.map((s) => [s.name, s]));
1790
+ const fmtStage = (v, stage) => {
1791
+ const s = byName.get(stage);
1792
+ const base = typeof v === "number" ? valueFormatter(v) : String(v ?? "");
1793
+ return s ? `${base} · ${chartFmt.percent(s.conversion)}` : base;
1794
+ };
1795
+ return /* @__PURE__ */ jsx("div", {
1796
+ role: "figure",
1797
+ "aria-label": name,
1798
+ "aria-busy": effective === "loading" || void 0,
1799
+ className: cx("w-full font-sans", className),
1800
+ children: placeholder ?? /* @__PURE__ */ jsxs("div", {
1801
+ className: "flex flex-col gap-5 sm:flex-row sm:items-center",
1802
+ children: [
1803
+ /* @__PURE__ */ jsx("div", {
1804
+ className: "min-w-0 flex-1",
1805
+ style: { height },
1806
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
1807
+ width: "100%",
1808
+ height: "100%",
1809
+ children: /* @__PURE__ */ jsxs(FunnelChart$1, {
1810
+ margin: {
1811
+ top: 4,
1812
+ right: 4,
1813
+ bottom: 4,
1814
+ left: 4
1815
+ },
1816
+ accessibilityLayer: true,
1817
+ title,
1818
+ desc: description,
1819
+ children: [/* @__PURE__ */ jsx(Funnel, {
1820
+ data: stages,
1821
+ dataKey: "value",
1822
+ nameKey: "name",
1823
+ stroke: t.surface,
1824
+ strokeWidth: 2,
1825
+ lastShapeType: "rectangle",
1826
+ isAnimationActive: false
1827
+ }), /* @__PURE__ */ jsx(Tooltip, {
1828
+ isAnimationActive: false,
1829
+ content: tooltipContent({ seriesFormatter: fmtStage })
1830
+ })]
1831
+ })
1832
+ })
1833
+ }),
1834
+ showStages && /* @__PURE__ */ jsx("ol", {
1835
+ "aria-label": "Stages",
1836
+ className: "flex w-full flex-col gap-2 sm:w-64",
1837
+ children: stages.map((s, i) => /* @__PURE__ */ jsxs("li", {
1838
+ className: "flex items-center justify-between gap-3 border-b border-border pb-2 last:border-b-0",
1839
+ children: [/* @__PURE__ */ jsxs("span", {
1840
+ className: "flex min-w-0 items-center gap-2",
1841
+ children: [/* @__PURE__ */ jsx("span", {
1842
+ "aria-hidden": true,
1843
+ className: "h-2.5 w-2.5 shrink-0 rounded-[3px]",
1844
+ style: { background: s.fill }
1845
+ }), /* @__PURE__ */ jsx("span", {
1846
+ className: "truncate text-[12.5px] text-foreground",
1847
+ children: s.name
1848
+ })]
1849
+ }), /* @__PURE__ */ jsxs("span", {
1850
+ className: "flex shrink-0 items-baseline gap-2 font-mono text-[12px] tabular-nums",
1851
+ children: [
1852
+ /* @__PURE__ */ jsx("span", {
1853
+ className: "font-medium text-foreground",
1854
+ children: valueFormatter(s.value)
1855
+ }),
1856
+ /* @__PURE__ */ jsx("span", {
1857
+ className: "text-muted-foreground",
1858
+ children: chartFmt.percent(s.conversion)
1859
+ }),
1860
+ i > 0 && /* @__PURE__ */ jsxs("span", {
1861
+ className: "text-destructive-text",
1862
+ children: [
1863
+ /* @__PURE__ */ jsx("span", {
1864
+ className: "sr-only",
1865
+ children: "drop-off "
1866
+ }),
1867
+ "−",
1868
+ chartFmt.percent(s.dropOff)
1869
+ ]
1870
+ })
1871
+ ]
1872
+ })]
1873
+ }, s.name))
1874
+ }),
1875
+ dataTable && /* @__PURE__ */ jsx(ChartDataTable, {
1876
+ caption: name,
1877
+ columns: [
1878
+ "Stage",
1879
+ category,
1880
+ "Conversion",
1881
+ "Drop-off"
1882
+ ],
1883
+ rows: stages.map((s) => [
1884
+ s.name,
1885
+ valueFormatter(s.value),
1886
+ chartFmt.percent(s.conversion),
1887
+ chartFmt.percent(s.dropOff)
1888
+ ])
1889
+ })
1890
+ ]
1891
+ })
1892
+ });
1893
+ }
1894
+ //#endregion
1895
+ //#region src/SankeyChart.tsx
1896
+ /** Turns name-or-index links into the index links Recharts expects; drops unknown endpoints and non-positive values. */
1897
+ function resolveSankeyLinks(nodes, links) {
1898
+ const byName = new Map(nodes.map((n, i) => [n.name, i]));
1899
+ const idx = (ref) => typeof ref === "number" ? ref >= 0 && ref < nodes.length ? ref : void 0 : byName.get(ref);
1900
+ const out = [];
1901
+ for (const l of links) {
1902
+ const source = idx(l.source);
1903
+ const target = idx(l.target);
1904
+ if (source === void 0 || target === void 0 || source === target || !(l.value > 0)) continue;
1905
+ out.push({
1906
+ source,
1907
+ target,
1908
+ value: l.value
1909
+ });
1910
+ }
1911
+ return out;
1912
+ }
1913
+ /** Flow diagram: nodes as token-coloured bars, links tinted with their source colour. */
1914
+ function SankeyChart({ nodes, links, colors = SAFE_CHART_COLORS, valueFormatter = identity, height = 320, nodeWidth = 10, nodePadding = 18, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false }) {
1915
+ const t = useChartTheme();
1916
+ const resolved = resolveSankeyLinks(nodes, links);
1917
+ const fills = nodes.map((n, i) => n.color ? t.color(n.color) : seriesColor(t, colors, i));
1918
+ const sources = new Set(resolved.map((l) => l.source));
1919
+ const name = accessibleName("Sankey chart", ariaLabel, title, nodes.map((n) => n.name));
1920
+ const data = {
1921
+ nodes: nodes.map((n, i) => ({
1922
+ name: n.name,
1923
+ fill: fills[i]
1924
+ })),
1925
+ links: resolved
1926
+ };
1927
+ const renderNode = ({ x, y, width, height: h, index }) => {
1928
+ const sink = !sources.has(index);
1929
+ return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("rect", {
1930
+ x,
1931
+ y,
1932
+ width,
1933
+ height: Math.max(h, 1),
1934
+ rx: 2,
1935
+ fill: fills[index]
1936
+ }), /* @__PURE__ */ jsx("text", {
1937
+ x: sink ? x - 6 : x + width + 6,
1938
+ y: y + h / 2,
1939
+ dy: "0.35em",
1940
+ textAnchor: sink ? "end" : "start",
1941
+ fontSize: 11,
1942
+ fill: t.text,
1943
+ children: nodes[index]?.name
1944
+ })] });
1945
+ };
1946
+ const renderLink = (p) => {
1947
+ const source = resolved[p.index]?.source ?? 0;
1948
+ return /* @__PURE__ */ jsx("path", {
1949
+ d: `M${p.sourceX},${p.sourceY} C${p.sourceControlX},${p.sourceY} ${p.targetControlX},${p.targetY} ${p.targetX},${p.targetY}`,
1950
+ fill: "none",
1951
+ stroke: fills[source] ?? t.color("primary"),
1952
+ strokeOpacity: .28,
1953
+ strokeWidth: Math.max(p.linkWidth, 1)
1954
+ });
1955
+ };
1956
+ return /* @__PURE__ */ jsx(ChartFrame, {
1957
+ label: name,
1958
+ state: resolveState(state, resolved.length),
1959
+ emptyMessage,
1960
+ errorMessage,
1961
+ onRetry,
1962
+ height,
1963
+ className,
1964
+ table: dataTable && /* @__PURE__ */ jsx(ChartDataTable, {
1965
+ caption: name,
1966
+ columns: [
1967
+ "From",
1968
+ "To",
1969
+ "Value"
1970
+ ],
1971
+ rows: resolved.map((l) => [
1972
+ nodes[l.source]?.name ?? "",
1973
+ nodes[l.target]?.name ?? "",
1974
+ valueFormatter(l.value)
1975
+ ])
1976
+ }),
1977
+ children: /* @__PURE__ */ jsx(Sankey, {
1978
+ data,
1979
+ nameKey: "name",
1980
+ nodeWidth,
1981
+ nodePadding,
1982
+ margin: {
1983
+ top: 6,
1984
+ right: 96,
1985
+ bottom: 6,
1986
+ left: 6
1987
+ },
1988
+ node: renderNode,
1989
+ link: renderLink,
1990
+ accessibilityLayer: true,
1991
+ title,
1992
+ desc: description,
1993
+ children: /* @__PURE__ */ jsx(Tooltip, {
1994
+ isAnimationActive: false,
1995
+ content: tooltipContent({ valueFormatter })
1996
+ })
1997
+ })
1998
+ });
1999
+ }
2000
+ //#endregion
2001
+ //#region src/RadarChart.tsx
2002
+ /** Multi-axis scores: one translucent polygon per category over a polygonal grid. */
2003
+ function RadarChart({ data, index, categories, colors = SAFE_CHART_COLORS, valueFormatter = identity, max, showLegend = true, showGrid = true, showRadiusAxis = false, height = 300, title, description, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = false }) {
2004
+ const t = useChartTheme();
2005
+ const [hidden, toggle] = useHiddenSeries();
2006
+ const col = (i) => seriesColor(t, colors, i);
2007
+ const legend = categories.map((c, i) => ({
2008
+ key: c,
2009
+ label: c,
2010
+ color: col(i)
2011
+ }));
2012
+ const name = accessibleName("Radar chart", ariaLabel, title, categories);
2013
+ return /* @__PURE__ */ jsx(ChartFrame, {
2014
+ label: name,
2015
+ state: resolveState(state, data.length),
2016
+ emptyMessage,
2017
+ errorMessage,
2018
+ onRetry,
2019
+ table: dataTable && cartesianTable(name, data, index, categories, (v) => formatAny(v, valueFormatter)),
2020
+ height,
2021
+ className,
2022
+ legend: showLegend && /* @__PURE__ */ jsx(ChartLegend, {
2023
+ className: "mb-3 px-1",
2024
+ items: legend,
2025
+ hidden,
2026
+ onToggle: toggle
2027
+ }),
2028
+ children: /* @__PURE__ */ jsxs(RadarChart$1, {
2029
+ data,
2030
+ outerRadius: "76%",
2031
+ accessibilityLayer: true,
2032
+ title,
2033
+ desc: description,
2034
+ children: [
2035
+ showGrid && /* @__PURE__ */ jsx(PolarGrid, { stroke: t.grid }),
2036
+ /* @__PURE__ */ jsx(PolarAngleAxis, {
2037
+ dataKey: index,
2038
+ tick: {
2039
+ fill: t.text,
2040
+ fontSize: 11
2041
+ }
2042
+ }),
2043
+ /* @__PURE__ */ jsx(PolarRadiusAxis, {
2044
+ domain: [0, max ?? "auto"],
2045
+ angle: 90,
2046
+ axisLine: false,
2047
+ tick: showRadiusAxis ? {
2048
+ fill: t.text,
2049
+ fontSize: 10
2050
+ } : false,
2051
+ tickFormatter: valueFormatter
2052
+ }),
2053
+ /* @__PURE__ */ jsx(Tooltip, {
2054
+ isAnimationActive: false,
2055
+ content: tooltipContent({ valueFormatter })
2056
+ }),
2057
+ categories.map((c, i) => /* @__PURE__ */ jsx(Radar, {
2058
+ dataKey: c,
2059
+ name: c,
2060
+ hide: hidden.has(c),
2061
+ stroke: col(i),
2062
+ strokeWidth: 2,
2063
+ fill: col(i),
2064
+ fillOpacity: .14,
2065
+ dot: {
2066
+ r: 2.5,
2067
+ strokeWidth: 0,
2068
+ fill: col(i)
2069
+ },
2070
+ isAnimationActive: false
2071
+ }, c))
2072
+ ]
2073
+ })
2074
+ });
2075
+ }
2076
+ //#endregion
2077
+ //#region src/Heatmap.tsx
2078
+ function extent(values) {
2079
+ if (!values.length) return [0, 1];
2080
+ return [Math.min(...values), Math.max(...values)];
2081
+ }
2082
+ /** Column-label row height + inter-row gap, used to fit rows into a fixed `height`. */
2083
+ const LABEL_ROW = 20;
2084
+ const GAP = 3;
2085
+ /** Cell height that fits `rows` rows in `height` px (clamped to 16–48), or undefined without a height. */
2086
+ function cellHeightFor(height, rows) {
2087
+ if (height === void 0) return void 0;
2088
+ const n = Math.max(rows, 1);
2089
+ return Math.min(48, Math.max(16, Math.floor((height - LABEL_ROW - GAP * n) / n)));
2090
+ }
2091
+ const cellFocus = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring";
2092
+ /**
2093
+ * Cohort / time grid coloured with a sequential or diverging token ramp. The colour grid is
2094
+ * a single keyboard stop (arrow keys move the active cell, Home/End jump within the row) with a
2095
+ * live readout; screen readers get the full data table (`dataTable`, on by default).
2096
+ */
2097
+ function Heatmap({ data, index, categories, scale = "sequential", domain, steps, color = "primary", valueFormatter = identity, showValues = false, showScale = true, cellHeight: cellHeightProp = 28, height, rowHeader, title, className, "aria-label": ariaLabel, state, emptyMessage, errorMessage, onRetry, dataTable = true }) {
2098
+ const id = useId();
2099
+ const cellHeight = cellHeightFor(height, data.length) ?? cellHeightProp;
2100
+ const [active, setActive] = useState(null);
2101
+ const name = accessibleName("Heatmap", ariaLabel, title, []);
2102
+ const diverging = scale === "diverging";
2103
+ const count = steps ?? (diverging ? 7 : 6);
2104
+ const ramp = diverging ? divergingRamp(count) : sequentialRamp(count, { color });
2105
+ const value = (r, c) => {
2106
+ const row = data[r];
2107
+ const key = categories[c];
2108
+ if (!row || key === void 0) return null;
2109
+ const v = field(row, key);
2110
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
2111
+ };
2112
+ const all = data.flatMap((_, r) => categories.map((__, c) => value(r, c))).filter((v) => v !== null);
2113
+ const [lo, hi] = extent(all);
2114
+ const dom = domain ?? (diverging ? [
2115
+ Math.min(lo, 0),
2116
+ 0,
2117
+ Math.max(hi, 0)
2118
+ ] : [lo, hi]);
2119
+ const effective = resolveState(state, all.length);
2120
+ const placeholder = chartStateView(effective, {
2121
+ height: height ?? Math.max(data.length, 4) * (cellHeight + 3),
2122
+ emptyMessage,
2123
+ errorMessage,
2124
+ onRetry
2125
+ });
2126
+ const cellStyle = (v) => ({ fill: ramp[rampIndex(v, dom, count)] ?? ramp[0] });
2127
+ const rowLabel = (r) => {
2128
+ const row = data[r];
2129
+ return row ? String(field(row, index)) : "";
2130
+ };
2131
+ const describe = ([r, c]) => {
2132
+ const v = value(r, c);
2133
+ return `${rowLabel(r)} · ${categories[c] ?? ""}: ${v === null ? "no data" : valueFormatter(v)}`;
2134
+ };
2135
+ const onKeyDown = (e) => {
2136
+ const [r, c] = active ?? [0, 0];
2137
+ const lastR = data.length - 1;
2138
+ const lastC = categories.length - 1;
2139
+ const to = {
2140
+ ArrowRight: [r, Math.min(lastC, c + 1)],
2141
+ ArrowLeft: [r, Math.max(0, c - 1)],
2142
+ ArrowDown: [Math.min(lastR, r + 1), c],
2143
+ ArrowUp: [Math.max(0, r - 1), c],
2144
+ Home: [r, 0],
2145
+ End: [r, lastC]
2146
+ }[e.key];
2147
+ if (to) {
2148
+ e.preventDefault();
2149
+ setActive(active ? to : [0, 0]);
2150
+ } else if (e.key === "Escape") setActive(null);
2151
+ };
2152
+ if (placeholder) return /* @__PURE__ */ jsx("div", {
2153
+ role: "figure",
2154
+ "aria-label": name,
2155
+ "aria-busy": effective === "loading" || void 0,
2156
+ className: cx("w-full font-sans", className),
2157
+ children: placeholder
2158
+ });
2159
+ const columns = `minmax(4.5rem, max-content) repeat(${categories.length}, minmax(1.75rem, 1fr))`;
2160
+ return /* @__PURE__ */ jsxs("div", {
2161
+ role: "figure",
2162
+ "aria-label": name,
2163
+ className: cx("w-full font-sans", className),
2164
+ children: [
2165
+ /* @__PURE__ */ jsx("div", {
2166
+ role: "group",
2167
+ tabIndex: 0,
2168
+ "aria-label": `${name}: colour grid, use the arrow keys to read cells`,
2169
+ onKeyDown,
2170
+ onFocus: () => setActive((a) => a ?? [0, 0]),
2171
+ onBlur: () => setActive(null),
2172
+ onMouseLeave: () => setActive(null),
2173
+ className: cx("overflow-x-auto rounded-md", cellFocus),
2174
+ children: /* @__PURE__ */ jsxs("div", {
2175
+ "aria-hidden": true,
2176
+ className: "grid gap-[3px]",
2177
+ style: { gridTemplateColumns: columns },
2178
+ children: [
2179
+ /* @__PURE__ */ jsx("div", {}),
2180
+ categories.map((c) => /* @__PURE__ */ jsx("div", {
2181
+ className: "truncate pb-1 text-center text-[11px] text-muted-foreground",
2182
+ children: c
2183
+ }, c)),
2184
+ data.map((_, r) => [/* @__PURE__ */ jsx("div", {
2185
+ className: "flex items-center truncate pr-2 text-[11.5px] text-muted-foreground",
2186
+ style: { height: cellHeight },
2187
+ children: rowLabel(r)
2188
+ }, `h${r}`), ...categories.map((c, ci) => {
2189
+ const v = value(r, ci);
2190
+ const s = v === null ? null : cellStyle(v);
2191
+ const on = active?.[0] === r && active[1] === ci;
2192
+ return /* @__PURE__ */ jsx("div", {
2193
+ "data-cell": `${r}-${ci}`,
2194
+ "data-fill": s?.fill,
2195
+ onMouseEnter: () => setActive([r, ci]),
2196
+ className: cx("flex items-center justify-center rounded-[3px] font-mono text-[10.5px] tabular-nums", !s && "border border-dashed border-border", on && "outline-2 outline-offset-1 outline-foreground"),
2197
+ style: {
2198
+ height: cellHeight,
2199
+ background: s?.fill
2200
+ },
2201
+ children: showValues && v !== null ? /* @__PURE__ */ jsx("span", {
2202
+ className: "rounded-[2px] bg-card px-1 leading-4 text-foreground",
2203
+ children: valueFormatter(v)
2204
+ }) : null
2205
+ }, `${r}-${c}`);
2206
+ })])
2207
+ ]
2208
+ })
2209
+ }),
2210
+ /* @__PURE__ */ jsxs("div", {
2211
+ className: "mt-2 flex flex-wrap items-center justify-between gap-3",
2212
+ children: [/* @__PURE__ */ jsx("p", {
2213
+ id: `${id}-readout`,
2214
+ "data-heatmap-readout": "",
2215
+ "aria-live": "polite",
2216
+ className: "min-h-[1.25rem] text-[12px] text-muted-foreground",
2217
+ children: active ? describe(active) : ""
2218
+ }), showScale && /* @__PURE__ */ jsxs("div", {
2219
+ "aria-hidden": true,
2220
+ className: "flex items-center gap-1.5 text-[11px] text-muted-foreground",
2221
+ children: [
2222
+ /* @__PURE__ */ jsx("span", {
2223
+ className: "font-mono tabular-nums",
2224
+ children: valueFormatter(dom[0])
2225
+ }),
2226
+ /* @__PURE__ */ jsx("span", {
2227
+ className: "flex",
2228
+ children: ramp.map((f, i) => /* @__PURE__ */ jsx("span", {
2229
+ className: "h-2.5 w-4 first:rounded-l-sm last:rounded-r-sm",
2230
+ style: { background: f }
2231
+ }, i))
2232
+ }),
2233
+ /* @__PURE__ */ jsx("span", {
2234
+ className: "font-mono tabular-nums",
2235
+ children: valueFormatter(dom[dom.length - 1] ?? dom[0])
2236
+ })
2237
+ ]
2238
+ })]
2239
+ }),
2240
+ dataTable && /* @__PURE__ */ jsx(ChartDataTable, {
2241
+ caption: name,
2242
+ columns: [rowHeader ?? index, ...categories],
2243
+ rows: data.map((_, r) => [rowLabel(r), ...categories.map((__, c) => {
2244
+ const v = value(r, c);
2245
+ return v === null ? "—" : valueFormatter(v);
2246
+ })])
2247
+ })
2248
+ ]
2249
+ });
2250
+ }
2251
+ //#endregion
2252
+ //#region src/MiniBar.tsx
2253
+ const TONE_TOKEN = {
2254
+ primary: "--primary",
2255
+ success: "--success",
2256
+ warning: "--warning",
2257
+ destructive: "--destructive",
2258
+ info: "--info",
2259
+ neutral: "--muted-foreground"
2260
+ };
2261
+ const fmtDefault = (v) => new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(v);
2262
+ /** "Deployments: 12 bars, latest 9, low 2, high 14, total 84". */
2263
+ function describeBars(data, label = "Bars", format = fmtDefault) {
2264
+ const last = data[data.length - 1];
2265
+ if (last === void 0) return `${label}: no data`;
2266
+ const total = data.reduce((s, v) => s + v, 0);
2267
+ return `${label}: ${data.length} bars, latest ${format(last)}, low ${format(Math.min(...data))}, high ${format(Math.max(...data))}, total ${format(total)}`;
2268
+ }
2269
+ function highlightIndex(data, h) {
2270
+ if (typeof h === "number") return h;
2271
+ if (h === "last") return data.length - 1;
2272
+ if (h === "max") return data.indexOf(Math.max(...data));
2273
+ if (h === "min") return data.indexOf(Math.min(...data));
2274
+ return -1;
2275
+ }
2276
+ /**
2277
+ * Sparkline-sized bar chart (pure SVG, no axes): drop-in next to `Sparkline variant="bar"`,
2278
+ * with a highlighted bar, per-bar hover titles and a generated `role="img"` summary.
2279
+ */
2280
+ function MiniBar({ data, labels, tone = "primary", highlight = "last", width = 120, height = 32, label, formatValue = fmtDefault, className, ...props }) {
2281
+ const name = props["aria-label"] ?? describeBars(data, label, formatValue);
2282
+ const token = TONE_TOKEN[tone];
2283
+ const n = data.length;
2284
+ const lo = n ? Math.min(0, ...data) : 0;
2285
+ const span = (n ? Math.max(0, ...data) : 0) - lo || 1;
2286
+ const zero = height - (0 - lo) / span * height;
2287
+ const slot = n ? width / n : width;
2288
+ const gap = Math.min(2, slot * .25);
2289
+ const hl = highlightIndex(data, highlight);
2290
+ return /* @__PURE__ */ jsx("svg", {
2291
+ role: "img",
2292
+ viewBox: `0 0 ${width} ${height}`,
2293
+ width,
2294
+ height,
2295
+ className: cx("inline-block shrink-0 overflow-visible align-middle", className),
2296
+ ...props,
2297
+ "aria-label": name,
2298
+ children: data.map((v, i) => {
2299
+ const h = Math.max(1, Math.abs(v) / span * height);
2300
+ const y = v >= 0 ? zero - h : zero;
2301
+ return /* @__PURE__ */ jsx("rect", {
2302
+ "data-highlight": i === hl || void 0,
2303
+ x: i * slot + gap / 2,
2304
+ y,
2305
+ width: Math.max(1, slot - gap),
2306
+ height: h,
2307
+ rx: 1,
2308
+ fill: i === hl || hl < 0 ? `hsl(var(${token}))` : `hsl(var(${token}) / 0.4)`,
2309
+ children: /* @__PURE__ */ jsx("title", { children: `${labels?.[i] ?? `#${i + 1}`}: ${formatValue(v)}` })
2310
+ }, i);
2311
+ })
2312
+ });
2313
+ }
2314
+ //#endregion
2315
+ export { AreaChart, BarChart, BarList, CHART_COLORS, ChartBrush, ChartDataTable, ChartEmpty, ChartError, ChartLegend, ChartLoading, ChartMarkers, ChartTooltip, ComboChart, DonutChart, FunnelChart, Heatmap, LineChart, MiniBar, RadarChart, SAFE_CHART_COLORS, SERIES_TOKENS, SankeyChart, TONE_TOKENS, cellHeightFor, chartFmt, clampRange, describeBars, divergingRamp, funnelStages, rampColor, rampIndex, resolveSankeyLinks, sequentialRamp, tooltipContent, useChartRange, useChartTheme, useHiddenSeries, useLiveSeries, usePrefersReducedMotion };
2316
+
2317
+ //# sourceMappingURL=index.js.map