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