@invana/charts 0.0.32

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,1701 @@
1
+ import * as React2 from 'react';
2
+ import uPlot2 from 'uplot';
3
+ import { Card, Eyebrow, Legend, LegendItem, Button, cn, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, useExpandedKeys, ExpandToggle } from '@invana/ui';
4
+ import { DataTable } from '@invana/tables';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+
7
+ // src/line-chart.tsx
8
+
9
+ // src/base/theme.ts
10
+ var DATA_SLOTS = 8;
11
+ function resolveColor(host, css) {
12
+ const probe = document.createElement("span");
13
+ probe.style.color = css;
14
+ probe.style.display = "none";
15
+ host.appendChild(probe);
16
+ const value = getComputedStyle(probe).color;
17
+ probe.remove();
18
+ return toRgba(value);
19
+ }
20
+ var pixel;
21
+ function toRgba(css) {
22
+ if (/^rgba?\(/.test(css)) return css;
23
+ if (pixel === void 0) {
24
+ const canvas = document.createElement("canvas");
25
+ canvas.width = canvas.height = 1;
26
+ pixel = canvas.getContext("2d", { willReadFrequently: true });
27
+ }
28
+ if (!pixel) return css;
29
+ pixel.clearRect(0, 0, 1, 1);
30
+ pixel.fillStyle = css;
31
+ pixel.fillRect(0, 0, 1, 1);
32
+ const [r, g, b, a] = pixel.getImageData(0, 0, 1, 1).data;
33
+ return `rgba(${r}, ${g}, ${b}, ${Math.round(a / 255 * 1e3) / 1e3})`;
34
+ }
35
+ function resolveSize(host, css) {
36
+ const probe = document.createElement("span");
37
+ probe.style.fontSize = css;
38
+ probe.style.display = "none";
39
+ host.appendChild(probe);
40
+ const value = parseFloat(getComputedStyle(probe).fontSize);
41
+ probe.remove();
42
+ return value;
43
+ }
44
+ function readChartTheme(host) {
45
+ const color = (name) => resolveColor(host, `var(${name})`);
46
+ const family = getComputedStyle(host).fontFamily;
47
+ const size = {
48
+ base: resolveSize(host, "var(--text-base, 1rem)"),
49
+ sm: resolveSize(host, "var(--text-sm, 0.923rem)"),
50
+ xs: resolveSize(host, "var(--text-xs, 0.846rem)")
51
+ };
52
+ return {
53
+ data: Array.from(
54
+ { length: DATA_SLOTS },
55
+ (_, i) => resolveColor(host, `var(--color-data-${i + 1}, var(--color-muted-foreground))`)
56
+ ),
57
+ success: color("--color-success"),
58
+ warning: color("--color-warning"),
59
+ destructive: color("--color-destructive"),
60
+ foreground: color("--color-foreground"),
61
+ mutedForeground: color("--color-muted-foreground"),
62
+ border: color("--color-border"),
63
+ card: color("--color-card"),
64
+ // Whole pixels: uPlot rescales a font for the device by matching
65
+ // `(\d+)px`, which reads `10.998px` as `998px`.
66
+ font: {
67
+ base: `${Math.round(size.base)}px ${family}`,
68
+ sm: `${Math.round(size.sm)}px ${family}`,
69
+ xs: `${Math.round(size.xs)}px ${family}`
70
+ },
71
+ size,
72
+ family
73
+ };
74
+ }
75
+ function withAlpha(rgb, alpha) {
76
+ const parts = rgb.match(/[\d.]+/g);
77
+ if (!parts || parts.length < 3) return rgb;
78
+ return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})`;
79
+ }
80
+
81
+ // src/base/hooks.ts
82
+ var THEME_ATTRIBUTES = ["class", "style", "data-theme", "data-density"];
83
+ function useChartTheme(ref) {
84
+ const [theme, setTheme] = React2.useState(null);
85
+ React2.useLayoutEffect(() => {
86
+ const el = ref.current;
87
+ if (!el) return;
88
+ const read = () => setTheme(readChartTheme(el));
89
+ read();
90
+ const observer = new MutationObserver(read);
91
+ observer.observe(document.documentElement, {
92
+ attributes: true,
93
+ attributeFilter: THEME_ATTRIBUTES
94
+ });
95
+ const scheme = window.matchMedia?.("(prefers-color-scheme: dark)");
96
+ scheme?.addEventListener?.("change", read);
97
+ document.fonts?.ready.then(read).catch(() => {
98
+ });
99
+ return () => {
100
+ observer.disconnect();
101
+ scheme?.removeEventListener?.("change", read);
102
+ };
103
+ }, [ref]);
104
+ return theme;
105
+ }
106
+ function useWidth(ref) {
107
+ const [width, setWidth] = React2.useState(0);
108
+ React2.useLayoutEffect(() => {
109
+ const el = ref.current;
110
+ if (!el) return;
111
+ setWidth(Math.floor(el.getBoundingClientRect().width));
112
+ const observer = new ResizeObserver(
113
+ ([entry]) => setWidth(Math.floor(entry.contentRect.width))
114
+ );
115
+ observer.observe(el);
116
+ return () => observer.disconnect();
117
+ }, [ref]);
118
+ return width;
119
+ }
120
+
121
+ // src/base/uplot-styles.ts
122
+ var CSS = `
123
+ .invana-uplot, .invana-uplot * { box-sizing: border-box; }
124
+ .invana-uplot { width: min-content; line-height: 1; }
125
+ .invana-uplot .u-wrap { position: relative; user-select: none; }
126
+ .invana-uplot .u-over, .invana-uplot .u-under { position: absolute; }
127
+ .invana-uplot .u-under { overflow: hidden; }
128
+ .invana-uplot canvas { display: block; position: relative; width: 100%; height: 100%; }
129
+ .invana-uplot .u-axis { position: absolute; }
130
+ .invana-uplot .u-select { position: absolute; pointer-events: none; }
131
+ .invana-uplot .u-cursor-x, .invana-uplot .u-cursor-y {
132
+ position: absolute; left: 0; top: 0; pointer-events: none; will-change: transform;
133
+ }
134
+ .invana-uplot .u-hz .u-cursor-x { height: 100%; border-right: 1px solid var(--color-border); }
135
+ .invana-uplot .u-cursor-pt {
136
+ position: absolute; top: 0; left: 0; border-radius: 50%; border: 0 solid;
137
+ pointer-events: none; will-change: transform; background-clip: padding-box !important;
138
+ }
139
+ .invana-uplot .u-off { display: none; }
140
+ `;
141
+ var ID = "invana-charts-uplot";
142
+ function installUPlotStyles() {
143
+ if (typeof document === "undefined" || document.getElementById(ID)) return;
144
+ const style = document.createElement("style");
145
+ style.id = ID;
146
+ style.textContent = CSS;
147
+ document.head.appendChild(style);
148
+ }
149
+ var measure = /* @__PURE__ */ (() => {
150
+ let ctx;
151
+ return (font, text) => {
152
+ if (ctx === void 0) ctx = document.createElement("canvas").getContext("2d");
153
+ if (!ctx) return text.length * 7;
154
+ ctx.font = font;
155
+ return ctx.measureText(text).width;
156
+ };
157
+ })();
158
+ var ChartFrame = React2.forwardRef(
159
+ ({
160
+ summary,
161
+ labels,
162
+ height,
163
+ ticks,
164
+ marks = [],
165
+ reference,
166
+ legend = [],
167
+ empty,
168
+ build,
169
+ tooltip,
170
+ table,
171
+ className,
172
+ "aria-label": ariaLabel,
173
+ ...props
174
+ }, ref) => {
175
+ const root = React2.useRef(null);
176
+ React2.useImperativeHandle(ref, () => root.current);
177
+ const mount = React2.useRef(null);
178
+ const plot = React2.useRef(null);
179
+ const theme = useChartTheme(root);
180
+ const width = useWidth(root);
181
+ const widthRef = React2.useRef(width);
182
+ React2.useLayoutEffect(() => {
183
+ widthRef.current = width;
184
+ });
185
+ const [asTable, setAsTable] = React2.useState(false);
186
+ const [hover, setHover] = React2.useState(null);
187
+ const [box, setBox] = React2.useState(null);
188
+ const [crosshair, setCrosshair] = React2.useState(true);
189
+ const n = labels.length;
190
+ const blank = empty != null || n === 0;
191
+ const marksKey = JSON.stringify(marks);
192
+ const referenceKey = JSON.stringify(reference ?? null);
193
+ const hasWidth = width > 0;
194
+ React2.useEffect(() => {
195
+ const el = mount.current;
196
+ const host = root.current;
197
+ if (!el || !host || !theme || !hasWidth || blank || asTable) return;
198
+ installUPlotStyles();
199
+ const built = build(theme, host);
200
+ const marksNow = JSON.parse(marksKey);
201
+ const referenceNow = JSON.parse(referenceKey);
202
+ setCrosshair(built.crosshair);
203
+ const labelBand = marksNow.some((m) => m.label) ? Math.ceil(theme.size.xs) + 8 : 0;
204
+ const half = Math.ceil(theme.size.xs / 2) + 1;
205
+ const axisWidth = Math.ceil(Math.max(0, ...built.splits.map((s) => measure(theme.font.xs, built.format(s))))) + 10;
206
+ const referenceBand = referenceNow ? Math.ceil(measure(theme.font.xs, referenceNow.label)) + 10 : 0;
207
+ const readBox = (u2) => setBox({
208
+ left: u2.over.offsetLeft,
209
+ top: u2.over.offsetTop,
210
+ width: u2.over.clientWidth,
211
+ height: u2.over.clientHeight
212
+ });
213
+ const drawMarks = (u2) => {
214
+ const ctx = u2.ctx;
215
+ const pr = uPlot2.pxRatio;
216
+ const { left, top, width: w, height: h } = u2.bbox;
217
+ ctx.save();
218
+ ctx.font = `${theme.size.xs * pr}px ${theme.family}`;
219
+ ctx.lineWidth = pr;
220
+ ctx.setLineDash([3 * pr, 3 * pr]);
221
+ ctx.strokeStyle = theme.mutedForeground;
222
+ ctx.fillStyle = theme.mutedForeground;
223
+ ctx.textBaseline = "bottom";
224
+ let lastRight = -Infinity;
225
+ for (const m of marksNow) {
226
+ if (m.index < 0 || m.index >= n) continue;
227
+ const x = Math.round(u2.valToPos(m.index, "x", true)) + 0.5;
228
+ ctx.beginPath();
229
+ ctx.moveTo(x, top - 2 * pr);
230
+ ctx.lineTo(x, top + h);
231
+ ctx.stroke();
232
+ if (!m.label) continue;
233
+ const tw = ctx.measureText(m.label).width;
234
+ const flip = x + 4 * pr + tw > left + w;
235
+ const start = flip ? x - 4 * pr - tw : x + 4 * pr;
236
+ if (start < lastRight + 6 * pr) continue;
237
+ ctx.textAlign = "left";
238
+ ctx.fillText(m.label, start, top - 2 * pr);
239
+ lastRight = start + tw;
240
+ }
241
+ if (referenceNow) {
242
+ const y = Math.round(u2.valToPos(referenceNow.value, "y", true)) + 0.5;
243
+ ctx.setLineDash([2 * pr, 4 * pr]);
244
+ ctx.beginPath();
245
+ ctx.moveTo(left, y);
246
+ ctx.lineTo(left + w, y);
247
+ ctx.stroke();
248
+ ctx.textAlign = "left";
249
+ ctx.textBaseline = "middle";
250
+ ctx.fillText(referenceNow.label, left + w + 6 * pr, y);
251
+ }
252
+ ctx.restore();
253
+ };
254
+ const color = (u2, i) => {
255
+ const stroke = u2.series[i].stroke;
256
+ return String(typeof stroke === "function" ? stroke(u2, i) : stroke);
257
+ };
258
+ const opts = {
259
+ class: "invana-uplot",
260
+ width: widthRef.current,
261
+ height,
262
+ padding: [labelBand || half, Math.max(built.padRight ?? 4, referenceBand), half, 0],
263
+ legend: { show: false },
264
+ scales: {
265
+ x: { time: false, range: [-0.5, n - 0.5] },
266
+ y: { range: [built.floor ?? 0, built.ceiling] }
267
+ },
268
+ axes: [
269
+ { show: false },
270
+ {
271
+ scale: "y",
272
+ side: 3,
273
+ size: axisWidth,
274
+ gap: 6,
275
+ font: theme.font.xs,
276
+ stroke: theme.mutedForeground,
277
+ ticks: { show: false },
278
+ grid: { stroke: theme.border, width: 1 },
279
+ splits: () => built.splits,
280
+ values: (_u, splits) => splits.map((s) => built.format(s))
281
+ }
282
+ ],
283
+ series: [{}, ...built.series],
284
+ bands: built.bands,
285
+ cursor: {
286
+ x: built.crosshair,
287
+ y: false,
288
+ drag: { x: false, y: false, setScale: false },
289
+ points: {
290
+ show: built.cursorPoints ?? built.crosshair,
291
+ size: 8,
292
+ width: 2,
293
+ stroke: () => theme.card,
294
+ fill: color
295
+ }
296
+ },
297
+ hooks: {
298
+ ready: [readBox],
299
+ setSize: [readBox],
300
+ setCursor: [(u2) => setHover(u2.cursor.idx ?? null)],
301
+ drawAxes: built.drawUnder ? [built.drawUnder] : [],
302
+ draw: [...built.draw ? [built.draw] : [], drawMarks]
303
+ }
304
+ };
305
+ const xs = Array.from({ length: n }, (_, i) => i);
306
+ const u = new uPlot2(opts, [xs, ...built.data], el);
307
+ plot.current = u;
308
+ return () => {
309
+ u.destroy();
310
+ plot.current = null;
311
+ setHover(null);
312
+ };
313
+ }, [theme, build, hasWidth, blank, asTable, n, height, marksKey, referenceKey]);
314
+ React2.useEffect(() => {
315
+ if (hasWidth) plot.current?.setSize({ width, height });
316
+ }, [width, height, hasWidth]);
317
+ const slot = box ? box.width / Math.max(n, 1) : 0;
318
+ const xAt = (i) => box ? box.left + (i + 0.5) * slot : 0;
319
+ const tickStyle = (i) => n > 1 && i === 0 ? { left: xAt(i) - slot / 2 } : n > 1 && i === n - 1 ? { right: (width || 0) - (xAt(i) + slot / 2) } : { left: xAt(i), transform: "translateX(-50%)" };
320
+ const tickSpan = (i) => {
321
+ const w = theme ? measure(theme.font.xs, labels[i] ?? "") : 0;
322
+ if (n > 1 && i === 0) return [xAt(i) - slot / 2, xAt(i) - slot / 2 + w];
323
+ if (n > 1 && i === n - 1) return [xAt(i) + slot / 2 - w, xAt(i) + slot / 2];
324
+ return [xAt(i) - w / 2, xAt(i) + w / 2];
325
+ };
326
+ const wanted = [...new Set(ticks ?? (n > 1 ? [0, n - 1] : [0]))].filter((i) => i >= 0 && i < n);
327
+ const placed = [];
328
+ const named = [
329
+ ...wanted.filter((i) => i === 0 || i === n - 1),
330
+ ...wanted.filter((i) => i !== 0 && i !== n - 1)
331
+ ].filter((i) => {
332
+ const [a, b] = tickSpan(i);
333
+ if (placed.some(([c, d]) => a < d + 8 && b > c - 8)) return false;
334
+ placed.push([a, b]);
335
+ return true;
336
+ });
337
+ const tip = hover != null && hover < n ? tooltip(hover) : null;
338
+ const markHere = hover != null ? marks.flatMap((m) => m.index === hover && m.label ? [m.label] : []) : [];
339
+ const tipLeft = hover != null ? xAt(hover) : 0;
340
+ const tipFlip = box ? tipLeft > box.left + box.width / 2 : false;
341
+ const onKeyDown = (e) => {
342
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
343
+ e.preventDefault();
344
+ const step = e.key === "ArrowLeft" ? -1 : 1;
345
+ setHover((at) => Math.min(n - 1, Math.max(0, (at ?? (step > 0 ? -1 : n)) + step)));
346
+ };
347
+ return /* @__PURE__ */ jsxs("div", { ref: root, className: cn("flex min-w-0 flex-col gap-1", className), ...props, children: [
348
+ asTable ? /* @__PURE__ */ jsx(
349
+ DataTable,
350
+ {
351
+ columns: table.columns,
352
+ data: table.rows,
353
+ enableSorting: false
354
+ }
355
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
356
+ /* @__PURE__ */ jsx(
357
+ "div",
358
+ {
359
+ role: "img",
360
+ "aria-label": ariaLabel ?? summary,
361
+ tabIndex: blank ? void 0 : 0,
362
+ onKeyDown,
363
+ onBlur: () => setHover(null),
364
+ className: "relative w-full rounded-sm outline-none focus-visible:ring-1 focus-visible:ring-ring",
365
+ style: { height },
366
+ children: blank ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground", children: empty ?? "Nothing in this period" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
367
+ !crosshair && hover != null && box ? (
368
+ // The hover band sits behind the transparent canvas, so a
369
+ // column lifts without its colour being tinted.
370
+ /* @__PURE__ */ jsx(
371
+ "div",
372
+ {
373
+ "aria-hidden": true,
374
+ className: "absolute rounded-sm bg-muted",
375
+ style: { left: xAt(hover) - slot / 2, width: slot, top: box.top, height: box.height }
376
+ }
377
+ )
378
+ ) : null,
379
+ /* @__PURE__ */ jsx("div", { ref: mount, "aria-hidden": true, className: "relative" }),
380
+ tip && box ? /* @__PURE__ */ jsxs(
381
+ Card,
382
+ {
383
+ "aria-hidden": true,
384
+ className: "pointer-events-none absolute z-10 flex min-w-32 max-w-64 flex-col gap-1 px-2 py-1.5 shadow-sm",
385
+ style: {
386
+ top: box.top,
387
+ left: tipFlip ? void 0 : tipLeft + 10,
388
+ right: tipFlip ? (width || 0) - tipLeft + 10 : void 0
389
+ },
390
+ children: [
391
+ /* @__PURE__ */ jsx(Eyebrow, { children: tip.title }),
392
+ tip.rows.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
393
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-0.5 w-3 shrink-0 rounded-full", style: { background: r.color } }),
394
+ /* @__PURE__ */ jsx("span", { className: "font-semibold tabular-nums", children: r.value }),
395
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate text-muted-foreground", children: r.label })
396
+ ] }, r.key)),
397
+ [...markHere, ...tip.note ? [tip.note] : []].map((note) => /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: note }, note))
398
+ ]
399
+ }
400
+ ) : null
401
+ ] })
402
+ }
403
+ ),
404
+ !blank && box ? /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "relative h-4 text-xs text-muted-foreground", children: named.map((i) => /* @__PURE__ */ jsx(
405
+ "span",
406
+ {
407
+ className: "absolute top-0 whitespace-nowrap tabular-nums",
408
+ style: tickStyle(i),
409
+ children: labels[i]
410
+ },
411
+ i
412
+ )) }) : null
413
+ ] }),
414
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [
415
+ legend.length > 1 && !blank ? /* @__PURE__ */ jsx(Legend, { children: legend.map((s) => /* @__PURE__ */ jsx(LegendItem, { color: s.color, label: s.label, kind: s.kind }, s.key)) }) : null,
416
+ blank ? null : /* @__PURE__ */ jsx(
417
+ Button,
418
+ {
419
+ variant: "link",
420
+ size: "xs",
421
+ className: "ml-auto px-0 text-muted-foreground",
422
+ "aria-pressed": asTable,
423
+ onClick: () => setAsTable((v) => !v),
424
+ children: asTable ? "View as chart" : "View as table"
425
+ }
426
+ )
427
+ ] })
428
+ ] });
429
+ }
430
+ );
431
+ ChartFrame.displayName = "ChartFrame";
432
+
433
+ // src/base/floors.ts
434
+ var COUNT_FLOOR = 5;
435
+ var MAX_BAR_WIDTH = 24;
436
+ var SEGMENT_GAP = 2;
437
+ var BAR_RADIUS = 4;
438
+ function niceScale(max, { integers = false } = {}) {
439
+ const top = max > 0 ? max : 1;
440
+ let power = 10 ** Math.floor(Math.log10(top / 4));
441
+ if (integers) power = Math.max(1, power);
442
+ for (; ; ) {
443
+ for (const m of [1, 2, 5]) {
444
+ const step = m * power;
445
+ const k = Math.max(1, Math.ceil(top / step - 1e-9));
446
+ if (k <= 4) return { ceiling: step * k, splits: Array.from({ length: k + 1 }, (_, i) => i * step) };
447
+ }
448
+ power *= 10;
449
+ }
450
+ }
451
+ function niceRange(min, max) {
452
+ const span = max > min ? max - min : Math.abs(max) || 1;
453
+ let power = 10 ** Math.floor(Math.log10(span / 4));
454
+ for (; ; ) {
455
+ for (const m of [1, 2, 5]) {
456
+ const step = m * power;
457
+ const floor = Math.floor(min / step + 1e-9) * step;
458
+ const k = Math.max(1, Math.ceil((max - floor) / step - 1e-9));
459
+ if (k <= 4) {
460
+ return {
461
+ floor,
462
+ ceiling: floor + step * k,
463
+ splits: Array.from({ length: k + 1 }, (_, i) => floor + i * step)
464
+ };
465
+ }
466
+ }
467
+ power *= 10;
468
+ }
469
+ }
470
+ function countScale(max) {
471
+ return niceScale(Math.max(max, COUNT_FLOOR), { integers: true });
472
+ }
473
+ function fixedSplits(ceiling) {
474
+ return [0, ceiling / 2, ceiling];
475
+ }
476
+ var compact = new Intl.NumberFormat(void 0, { notation: "compact", maximumFractionDigits: 1 });
477
+ var whole = new Intl.NumberFormat();
478
+ function formatCount(value) {
479
+ return Math.abs(value) < 1e4 ? whole.format(value) : compact.format(value);
480
+ }
481
+ function useLatest(value) {
482
+ const ref = React2.useRef(value);
483
+ React2.useLayoutEffect(() => {
484
+ ref.current = value;
485
+ });
486
+ return ref;
487
+ }
488
+ var NONE = [];
489
+ var LineChart = React2.forwardRef(
490
+ ({
491
+ values,
492
+ name,
493
+ compare = NONE,
494
+ labels,
495
+ format = String,
496
+ max,
497
+ gridlines = NONE,
498
+ marks = NONE,
499
+ reference,
500
+ band,
501
+ highlights = NONE,
502
+ forecastFrom,
503
+ forecastLabel,
504
+ zero = true,
505
+ ticks,
506
+ height = 132,
507
+ color = "var(--color-primary)",
508
+ empty,
509
+ ...props
510
+ }, ref) => {
511
+ const fmt = useLatest(format);
512
+ const referenceValue = reference?.value;
513
+ const n = values.length;
514
+ const [lower, upper] = React2.useMemo(() => {
515
+ const spread = (edge) => edge == null ? null : Array.from({ length: n }, (_, i) => typeof edge === "number" ? edge : edge[i] ?? null);
516
+ return [spread(band?.lower), spread(band?.upper)];
517
+ }, [band?.lower, band?.upper, n]);
518
+ const forecast = forecastFrom != null && forecastFrom >= 0 && forecastFrom < n ? forecastFrom : null;
519
+ const highlightsKey = JSON.stringify(highlights);
520
+ const compareKey = JSON.stringify(compare);
521
+ const named = compare.length > 0;
522
+ const build = React2.useCallback(
523
+ (theme, host) => {
524
+ const stroke = resolveColor(host, color);
525
+ const rings = JSON.parse(highlightsKey).filter((h) => h.index >= 0 && h.index < n).map((h) => ({ index: h.index, color: h.color ? resolveColor(host, h.color) : stroke }));
526
+ const ringed = new Set(rings.map((r) => r.index));
527
+ const others = JSON.parse(compareKey).map((c) => ({
528
+ ...c,
529
+ stroke: resolveColor(host, c.color ?? "var(--color-muted-foreground)")
530
+ }));
531
+ const measured2 = [...values, ...others.flatMap((c) => c.values)].filter((v) => v != null);
532
+ const edges = [...lower ?? [], ...upper ?? []].filter((v) => v != null);
533
+ const top = Math.max(...measured2, ...edges, referenceValue ?? -Infinity);
534
+ const bottom = Math.min(...measured2, ...edges, referenceValue ?? Infinity);
535
+ const scale = zero ? { floor: 0, ...niceScale(Math.max(0, top) * 1.05) } : niceRange(bottom, top + (top - bottom) * 0.05);
536
+ const ceiling = max ?? Math.max(scale.ceiling, ...gridlines);
537
+ const last = forecast ?? values.reduce((at, v, i) => v != null ? i : at, -1);
538
+ const points = values.map((v, i) => v != null && (i === last || values[i - 1] == null && values[i + 1] == null) ? i : -1).filter((i) => i >= 0 && !ringed.has(i));
539
+ const line = (dash) => ({
540
+ stroke,
541
+ width: 2,
542
+ dash,
543
+ spanGaps: false,
544
+ points: {
545
+ show: !dash,
546
+ size: 10,
547
+ width: 2,
548
+ stroke: theme.card,
549
+ fill: stroke,
550
+ filter: () => points
551
+ }
552
+ });
553
+ const actual = forecast == null ? values : values.map((v, i) => i <= forecast ? v : null);
554
+ const ahead = forecast == null ? null : values.map((v, i) => i >= forecast ? v : null);
555
+ const bandFill = withAlpha(band?.color ? resolveColor(host, band.color) : stroke, 0.1);
556
+ const bandLabel = band?.label;
557
+ const drawUnder = (u) => {
558
+ if (!lower || !upper) return;
559
+ const ctx = u.ctx;
560
+ const pr = uPlot2.pxRatio;
561
+ const { left, width: w, top: t, height: h } = u.bbox;
562
+ ctx.save();
563
+ ctx.beginPath();
564
+ ctx.rect(left, t, w, h);
565
+ ctx.clip();
566
+ ctx.fillStyle = bandFill;
567
+ const y = (v) => u.valToPos(v, "y", true);
568
+ if (typeof band?.lower === "number" && typeof band?.upper === "number") {
569
+ ctx.fillRect(left, y(band.upper), w, y(band.lower) - y(band.upper));
570
+ } else {
571
+ let run = [];
572
+ const flush = () => {
573
+ if (run.length) {
574
+ const x = (i) => u.valToPos(i, "x", true);
575
+ ctx.beginPath();
576
+ run.forEach((i, k) => k ? ctx.lineTo(x(i), y(upper[i])) : ctx.moveTo(x(i), y(upper[i])));
577
+ for (const i of [...run].reverse()) ctx.lineTo(x(i), y(lower[i]));
578
+ ctx.closePath();
579
+ ctx.fill();
580
+ }
581
+ run = [];
582
+ };
583
+ for (let i = 0; i < n; i++) {
584
+ if (lower[i] != null && upper[i] != null) run.push(i);
585
+ else flush();
586
+ }
587
+ flush();
588
+ }
589
+ ctx.restore();
590
+ if (bandLabel) {
591
+ const at = n - 1 - [...lower].reverse().findIndex((v, k) => v != null && upper[n - 1 - k] != null);
592
+ if (at < n) {
593
+ ctx.save();
594
+ ctx.font = `${theme.size.xs * pr}px ${theme.family}`;
595
+ ctx.fillStyle = theme.mutedForeground;
596
+ ctx.textAlign = "right";
597
+ const below = y(lower[at]) + 3 * pr;
598
+ const room = below + theme.size.xs * pr <= t + h;
599
+ ctx.textBaseline = room ? "top" : "bottom";
600
+ ctx.fillText(bandLabel, left + w - 4 * pr, room ? below : y(upper[at]) - 3 * pr);
601
+ ctx.restore();
602
+ }
603
+ }
604
+ };
605
+ const lastOf = (vs) => vs.reduce((at, v, i) => v != null ? i : at, -1);
606
+ const endLabels = named ? [{ name: name ?? "", values, stroke, main: true }, ...others.map((c) => ({ ...c, main: false }))] : [];
607
+ const font = `600 ${theme.size.xs}px ${theme.family}`;
608
+ const padRight = named ? Math.ceil(
609
+ Math.max(
610
+ 0,
611
+ ...endLabels.map((l) => {
612
+ const m = document.createElement("canvas").getContext("2d");
613
+ if (!m) return l.name.length * 7;
614
+ m.font = font;
615
+ return m.measureText(l.name).width;
616
+ })
617
+ )
618
+ ) + 12 : void 0;
619
+ const drawLabels = (u) => {
620
+ if (!named) return;
621
+ const ctx = u.ctx;
622
+ const pr = uPlot2.pxRatio;
623
+ const x = u.bbox.left + u.bbox.width + 6 * pr;
624
+ const step = theme.size.xs * pr + 2 * pr;
625
+ const placed = endLabels.map((l) => {
626
+ const at = lastOf(l.values);
627
+ return at < 0 ? null : { ...l, y: u.valToPos(l.values[at], "y", true) };
628
+ }).filter((l) => l != null).sort((a, b) => a.y - b.y);
629
+ for (let i = 1; i < placed.length; i++) placed[i].y = Math.max(placed[i].y, placed[i - 1].y + step);
630
+ ctx.save();
631
+ ctx.font = `600 ${theme.size.xs * pr}px ${theme.family}`;
632
+ ctx.textBaseline = "middle";
633
+ ctx.textAlign = "left";
634
+ for (const l of placed) {
635
+ ctx.fillStyle = l.main ? theme.foreground : l.stroke;
636
+ ctx.fillText(l.name, x, l.y);
637
+ }
638
+ ctx.restore();
639
+ };
640
+ const draw = (u) => {
641
+ drawLabels(u);
642
+ if (!rings.length) return;
643
+ const ctx = u.ctx;
644
+ const pr = uPlot2.pxRatio;
645
+ ctx.save();
646
+ ctx.lineWidth = 1.5 * pr;
647
+ for (const r of rings) {
648
+ const v = values[r.index];
649
+ if (v == null) continue;
650
+ ctx.strokeStyle = r.color;
651
+ ctx.beginPath();
652
+ ctx.arc(u.valToPos(r.index, "x", true), u.valToPos(v, "y", true), 4.5 * pr, 0, Math.PI * 2);
653
+ ctx.stroke();
654
+ }
655
+ ctx.restore();
656
+ };
657
+ const behind = others.map(
658
+ (c) => ({ stroke: c.stroke, width: 2, spanGaps: false, points: { show: false } })
659
+ );
660
+ return {
661
+ series: [...behind, ...ahead ? [line(), line([6, 4])] : [line()]],
662
+ data: [...others.map((c) => c.values), ...ahead ? [actual, ahead] : [actual]],
663
+ padRight,
664
+ floor: scale.floor,
665
+ ceiling,
666
+ splits: gridlines.length ? gridlines : max != null ? fixedSplits(max) : scale.splits,
667
+ format: (v) => fmt.current(v),
668
+ crosshair: true,
669
+ cursorPoints: true,
670
+ drawUnder,
671
+ draw
672
+ };
673
+ },
674
+ [values, name, compareKey, named, color, max, gridlines, referenceValue, fmt, zero, lower, upper, band?.lower, band?.upper, band?.color, band?.label, forecast, highlightsKey, n]
675
+ );
676
+ const measured = values.filter((v) => v != null);
677
+ const blank = measured.length === 0;
678
+ const summary = React2.useMemo(() => {
679
+ if (blank) return "Line chart with no measured periods.";
680
+ const f = format;
681
+ let top = 0;
682
+ values.forEach((v, i) => {
683
+ if (v != null && v > (values[top] ?? -Infinity)) top = i;
684
+ });
685
+ const lastIdx = forecast ?? values.reduce((at, v, i) => v != null ? i : at, -1);
686
+ const parts = [
687
+ `Line chart over ${values.length} periods, ${labels[0]} to ${labels[labels.length - 1]}.`,
688
+ `Latest ${f(values[lastIdx])} on ${labels[lastIdx]}; highest ${f(values[top])} on ${labels[top]}.`
689
+ ];
690
+ const gaps = values.length - measured.length;
691
+ if (gaps) parts.push(`${gaps} periods with nothing measured.`);
692
+ if (forecast != null) parts.push(`Forecast from ${labels[forecast]}.`);
693
+ if (band && typeof band.lower === "number" && typeof band.upper === "number")
694
+ parts.push(`${band.label ?? "Band"} ${f(band.lower)} to ${f(band.upper)}.`);
695
+ else if (band) parts.push(`${band.label ?? "A band"} shaded.`);
696
+ if (highlights.length) parts.push(`Highlighted: ${highlights.map((h) => labels[h.index]).join(", ")}.`);
697
+ const named2 = marks.filter((m) => m.label);
698
+ if (named2.length) parts.push(`Marked: ${named2.map((m) => `${m.label} (${labels[m.index]})`).join(", ")}.`);
699
+ if (reference) parts.push(`Reference ${reference.label} at ${f(reference.value)}.`);
700
+ for (const c of compare) {
701
+ const at = c.values.reduce((last, v, i) => v != null ? i : last, -1);
702
+ if (at >= 0) parts.push(`Against ${c.name}: ${f(c.values[at])} on ${labels[at]}.`);
703
+ }
704
+ return parts.join(" ");
705
+ }, [blank, values, labels, marks, reference, measured.length, format, forecast, band, highlights, compare]);
706
+ const allMarks = React2.useMemo(
707
+ () => forecast == null ? marks : [...marks, { index: forecast, label: forecastLabel }],
708
+ [marks, forecast, forecastLabel]
709
+ );
710
+ const bandText = (i) => lower?.[i] != null && upper?.[i] != null ? `${format(lower[i])} \u2013 ${format(upper[i])}` : "";
711
+ return /* @__PURE__ */ jsx(
712
+ ChartFrame,
713
+ {
714
+ ref,
715
+ summary,
716
+ labels,
717
+ height,
718
+ ticks,
719
+ marks: allMarks,
720
+ reference,
721
+ empty: blank ? empty ?? "Nothing measured in this period" : void 0,
722
+ build,
723
+ tooltip: (i) => ({
724
+ title: labels[i],
725
+ rows: [
726
+ ...values[i] != null ? [{ key: "value", label: named ? name ?? "" : "", color, value: format(values[i]) }] : [],
727
+ ...compare.flatMap(
728
+ (c, k) => c.values[i] != null ? [{ key: `compare-${k}`, label: c.name, color: c.color ?? "var(--color-muted-foreground)", value: format(c.values[i]) }] : []
729
+ )
730
+ ],
731
+ note: values[i] == null ? "nothing ran" : [
732
+ forecast != null && i > forecast ? "forecast" : "",
733
+ bandText(i) ? `${band?.label ?? "band"} ${bandText(i)}` : ""
734
+ ].filter(Boolean).join(" \xB7 ") || void 0
735
+ }),
736
+ table: {
737
+ columns: [
738
+ { accessorKey: "period", header: "Period" },
739
+ { accessorKey: "value", header: name ?? "Value" },
740
+ ...compare.map((c, k) => ({ accessorKey: `compare${k}`, header: c.name })),
741
+ ...band ? [{ accessorKey: "band", header: band.label ?? "Band" }] : []
742
+ ],
743
+ rows: values.map((v, i) => ({
744
+ period: labels[i],
745
+ value: v != null ? `${format(v)}${forecast != null && i > forecast ? " (forecast)" : ""}` : "\u2014",
746
+ ...Object.fromEntries(compare.map((c, k) => [`compare${k}`, c.values[i] != null ? format(c.values[i]) : "\u2014"])),
747
+ band: bandText(i) || "\u2014"
748
+ }))
749
+ },
750
+ ...props
751
+ }
752
+ );
753
+ }
754
+ );
755
+ LineChart.displayName = "LineChart";
756
+
757
+ // src/base/draw.ts
758
+ function topRoundedRect(ctx, x, y, w, h, r) {
759
+ const radius = Math.max(0, Math.min(r, w / 2, h));
760
+ ctx.beginPath();
761
+ ctx.moveTo(x, y + h);
762
+ ctx.lineTo(x, y + radius);
763
+ ctx.arcTo(x, y, x + radius, y, radius);
764
+ ctx.lineTo(x + w - radius, y);
765
+ ctx.arcTo(x + w, y, x + w, y + radius, radius);
766
+ ctx.lineTo(x + w, y + h);
767
+ ctx.closePath();
768
+ ctx.fill();
769
+ }
770
+ var NONE2 = [];
771
+ var StackedBarChartV = React2.forwardRef(
772
+ ({ data, series, max, gridlines = NONE2, height = 132, ticks, empty, ...props }, ref) => {
773
+ const totals = React2.useMemo(
774
+ () => data.map((d) => series.reduce((sum, s) => sum + (d.values[s.key] ?? 0), 0)),
775
+ [data, series]
776
+ );
777
+ const blank = totals.every((t) => t === 0);
778
+ const build = React2.useCallback(
779
+ (_theme, host) => {
780
+ const colors = series.map((s) => resolveColor(host, s.color));
781
+ const scale = countScale(Math.max(0, ...totals));
782
+ const ceiling = max ?? Math.max(scale.ceiling, ...gridlines);
783
+ const draw = (u) => {
784
+ const ctx = u.ctx;
785
+ const pr = uPlot2.pxRatio;
786
+ const slot = u.bbox.width / Math.max(data.length, 1);
787
+ const width = Math.max(pr, Math.min(MAX_BAR_WIDTH * pr, slot * 0.72));
788
+ const gap = SEGMENT_GAP * pr;
789
+ const baseline = u.valToPos(0, "y", true);
790
+ ctx.save();
791
+ data.forEach((d, i) => {
792
+ const x = Math.round(u.valToPos(i, "x", true) - width / 2);
793
+ const parts = series.map((s, k) => ({ value: d.values[s.key] ?? 0, color: colors[k] })).filter((p) => p.value > 0);
794
+ let sum = 0;
795
+ let bottom = baseline;
796
+ parts.forEach((p, j) => {
797
+ sum += p.value;
798
+ const top = u.valToPos(Math.min(sum, ceiling), "y", true);
799
+ const last = j === parts.length - 1;
800
+ const y = last ? top : Math.min(top + gap, bottom - pr);
801
+ const h = Math.max(pr, bottom - y);
802
+ ctx.fillStyle = p.color;
803
+ if (last) topRoundedRect(ctx, x, y, width, h, BAR_RADIUS * pr);
804
+ else ctx.fillRect(x, y, width, h);
805
+ bottom = top;
806
+ });
807
+ });
808
+ ctx.restore();
809
+ };
810
+ return {
811
+ // One invisible series carries the totals, so the cursor has a value
812
+ // to snap to on every day; the columns themselves are drawn above.
813
+ series: [{ stroke: "transparent", paths: () => null, points: { show: false } }],
814
+ data: [totals],
815
+ ceiling,
816
+ splits: gridlines.length ? gridlines : max != null ? fixedSplits(max) : scale.splits,
817
+ format: formatCount,
818
+ crosshair: false,
819
+ draw
820
+ };
821
+ },
822
+ [data, series, totals, max, gridlines]
823
+ );
824
+ const summary = React2.useMemo(() => {
825
+ if (blank) return "Stacked column chart with nothing counted in this period.";
826
+ const sums = series.map((s) => data.reduce((sum, d) => sum + (d.values[s.key] ?? 0), 0));
827
+ const busiest = totals.indexOf(Math.max(...totals));
828
+ return [
829
+ `Stacked column chart over ${data.length} periods, ${data[0].label} to ${data[data.length - 1].label}.`,
830
+ `Totals: ${series.map((s, k) => `${s.label} ${formatCount(sums[k])}`).join(", ")}.`,
831
+ `Busiest: ${data[busiest].label} with ${formatCount(totals[busiest])}.`
832
+ ].join(" ");
833
+ }, [blank, data, series, totals]);
834
+ return /* @__PURE__ */ jsx(
835
+ ChartFrame,
836
+ {
837
+ ref,
838
+ summary,
839
+ labels: data.map((d) => d.label),
840
+ height,
841
+ ticks,
842
+ legend: series,
843
+ empty: blank ? empty ?? "Nothing counted in this period" : void 0,
844
+ build,
845
+ tooltip: (i) => ({
846
+ title: data[i].label,
847
+ rows: series.map((s) => ({
848
+ key: s.key,
849
+ label: s.label,
850
+ color: s.color,
851
+ value: formatCount(data[i].values[s.key] ?? 0)
852
+ }))
853
+ }),
854
+ table: {
855
+ columns: [
856
+ { accessorKey: "period", header: "Period" },
857
+ ...series.map((s) => ({ accessorKey: s.key, header: s.label })),
858
+ { accessorKey: "__total", header: "Total" }
859
+ ],
860
+ rows: data.map((d, i) => ({
861
+ period: d.label,
862
+ ...Object.fromEntries(series.map((s) => [s.key, formatCount(d.values[s.key] ?? 0)])),
863
+ __total: formatCount(totals[i])
864
+ }))
865
+ },
866
+ ...props
867
+ }
868
+ );
869
+ }
870
+ );
871
+ StackedBarChartV.displayName = "StackedBarChartV";
872
+ var NONE3 = [];
873
+ var FILL_ALPHA = 0.78;
874
+ var StackedAreaChart = React2.forwardRef(
875
+ ({
876
+ data,
877
+ series,
878
+ marks = NONE3,
879
+ markLegend,
880
+ format = formatCount,
881
+ max,
882
+ gridlines = NONE3,
883
+ ticks,
884
+ height = 200,
885
+ directLabels,
886
+ empty,
887
+ ...props
888
+ }, ref) => {
889
+ const fmt = useLatest(format);
890
+ const labelled = directLabels ?? series.length <= 4;
891
+ const stack = React2.useMemo(() => {
892
+ const sums = data.map(() => 0);
893
+ return series.map(
894
+ (s) => data.map((d, i) => {
895
+ sums[i] += d.values[s.key] ?? 0;
896
+ return sums[i];
897
+ })
898
+ );
899
+ }, [data, series]);
900
+ const totals = stack.length ? stack[stack.length - 1] : data.map(() => 0);
901
+ const blank = totals.every((t) => t === 0);
902
+ const build = React2.useCallback(
903
+ (theme, host) => {
904
+ const colors = series.map((s) => resolveColor(host, s.color));
905
+ const scale = niceScale(Math.max(0, ...totals));
906
+ const ceiling = max ?? Math.max(scale.ceiling, ...gridlines);
907
+ const stepped = uPlot2.paths.stepped({ align: 1 });
908
+ const last = data.length - 1;
909
+ const uSeries = series.map((_, k) => ({
910
+ stroke: theme.card,
911
+ width: 2,
912
+ paths: stepped,
913
+ points: { show: false },
914
+ // The bottom band fills to the baseline; every other band is a
915
+ // `bands` entry filling down to the band beneath it.
916
+ fill: k === 0 ? withAlpha(colors[0], FILL_ALPHA) : void 0
917
+ }));
918
+ const bands = series.slice(1).map((_, j) => ({
919
+ series: [j + 2, j + 1],
920
+ fill: withAlpha(colors[j + 1], FILL_ALPHA)
921
+ }));
922
+ const measure2 = document.createElement("canvas").getContext("2d");
923
+ let padRight = 4;
924
+ if (labelled && measure2) {
925
+ measure2.font = `600 ${theme.font.xs}`;
926
+ padRight = Math.ceil(Math.max(0, ...series.map((s) => measure2.measureText(s.label).width))) + 20;
927
+ }
928
+ const draw = (u) => {
929
+ if (!labelled || last < 0) return;
930
+ const ctx = u.ctx;
931
+ const pr = uPlot2.pxRatio;
932
+ const x = u.bbox.left + u.bbox.width + 6 * pr;
933
+ const line = theme.size.xs * pr + 2 * pr;
934
+ const placed = series.map((s, k) => {
935
+ const top = stack[k][last];
936
+ const bottom = k ? stack[k - 1][last] : 0;
937
+ return { s, k, empty: top === bottom, y: u.valToPos((top + bottom) / 2, "y", true) };
938
+ }).filter((p) => !p.empty).sort((a, b) => b.y - a.y);
939
+ ctx.save();
940
+ ctx.font = `600 ${theme.size.xs * pr}px ${theme.family}`;
941
+ ctx.textBaseline = "middle";
942
+ ctx.textAlign = "left";
943
+ let prev = Infinity;
944
+ for (const p of placed) {
945
+ if (prev - p.y < line) continue;
946
+ ctx.fillStyle = colors[p.k];
947
+ ctx.fillRect(x, Math.round(p.y - pr), 6 * pr, 2 * pr);
948
+ ctx.fillStyle = theme.foreground;
949
+ ctx.fillText(p.s.label, x + 10 * pr, p.y);
950
+ prev = p.y;
951
+ }
952
+ ctx.restore();
953
+ };
954
+ return {
955
+ series: uSeries,
956
+ data: stack,
957
+ bands,
958
+ ceiling,
959
+ splits: gridlines.length ? gridlines : max != null ? fixedSplits(max) : scale.splits,
960
+ format: (v) => fmt.current(v),
961
+ crosshair: true,
962
+ cursorPoints: false,
963
+ draw,
964
+ padRight
965
+ };
966
+ },
967
+ [data, series, stack, totals, max, gridlines, labelled, fmt]
968
+ );
969
+ const legend = [
970
+ ...series,
971
+ ...marks.length && markLegend ? [{ key: "__mark", label: markLegend, color: "var(--color-muted-foreground)", kind: "dashed" }] : []
972
+ ];
973
+ const summary = React2.useMemo(() => {
974
+ if (blank) return "Stacked area chart with nothing recorded in this period.";
975
+ const last = data.length - 1;
976
+ return [
977
+ `Stacked area chart over ${data.length} periods, ${data[0].label} to ${data[last].label}.`,
978
+ `${format(totals[0])} at the start, ${format(totals[last])} at the end.`,
979
+ `At the end: ${series.map((s) => `${s.label} ${format(data[last].values[s.key] ?? 0)}`).join(", ")}.`,
980
+ marks.length ? `Writes marked: ${marks.map((m) => `${m.label} (${data[m.index]?.label})`).join(", ")}.` : ""
981
+ ].filter(Boolean).join(" ");
982
+ }, [blank, data, series, totals, marks, format]);
983
+ return /* @__PURE__ */ jsx(
984
+ ChartFrame,
985
+ {
986
+ ref,
987
+ summary,
988
+ labels: data.map((d) => d.label),
989
+ height,
990
+ ticks,
991
+ marks,
992
+ legend,
993
+ empty: blank ? empty ?? "Nothing recorded in this period" : void 0,
994
+ build,
995
+ tooltip: (i) => ({
996
+ title: data[i].label,
997
+ // Top-down, the order the bands are read in.
998
+ rows: [...series].reverse().map((s) => ({
999
+ key: s.key,
1000
+ label: s.label,
1001
+ color: s.color,
1002
+ value: format(data[i].values[s.key] ?? 0)
1003
+ })),
1004
+ note: `${format(totals[i])} in all`
1005
+ }),
1006
+ table: {
1007
+ columns: [
1008
+ { accessorKey: "period", header: "Period" },
1009
+ ...series.map((s) => ({ accessorKey: s.key, header: s.label })),
1010
+ { accessorKey: "__total", header: "Total" }
1011
+ ],
1012
+ rows: data.map((d, i) => ({
1013
+ period: d.label,
1014
+ ...Object.fromEntries(series.map((s) => [s.key, format(d.values[s.key] ?? 0)])),
1015
+ __total: format(totals[i])
1016
+ }))
1017
+ },
1018
+ ...props
1019
+ }
1020
+ );
1021
+ }
1022
+ );
1023
+ StackedAreaChart.displayName = "StackedAreaChart";
1024
+ var Sparkline = React2.forwardRef(
1025
+ ({
1026
+ values,
1027
+ width = 72,
1028
+ height = 20,
1029
+ color = "var(--color-data-1)",
1030
+ area,
1031
+ strokeWidth = 2,
1032
+ endMarker = true,
1033
+ label,
1034
+ className,
1035
+ ...props
1036
+ }, ref) => {
1037
+ const pad = 3;
1038
+ const min = Math.min(...values);
1039
+ const max = Math.max(...values);
1040
+ const span = max - min || 1;
1041
+ const stepX = values.length > 1 ? (width - pad * 2) / (values.length - 1) : 0;
1042
+ const points = values.map((v, i) => [
1043
+ pad + i * stepX,
1044
+ height - pad - (v - min) / span * (height - pad * 2)
1045
+ ]);
1046
+ const d = points.map((p, i) => `${i ? "L" : "M"}${p[0]} ${p[1]}`).join(" ");
1047
+ const last = points[points.length - 1];
1048
+ return /* @__PURE__ */ jsxs(
1049
+ "svg",
1050
+ {
1051
+ ref,
1052
+ width,
1053
+ height,
1054
+ viewBox: `0 0 ${width} ${height}`,
1055
+ className: cn("shrink-0 overflow-visible", className),
1056
+ role: label ? "img" : void 0,
1057
+ "aria-label": label,
1058
+ "aria-hidden": label ? void 0 : true,
1059
+ ...props,
1060
+ children: [
1061
+ area ? /* @__PURE__ */ jsx(
1062
+ "path",
1063
+ {
1064
+ d: `${d} L${last[0]} ${height} L${points[0][0]} ${height} Z`,
1065
+ fill: color,
1066
+ opacity: 0.1
1067
+ }
1068
+ ) : null,
1069
+ /* @__PURE__ */ jsx(
1070
+ "path",
1071
+ {
1072
+ d,
1073
+ fill: "none",
1074
+ stroke: color,
1075
+ strokeWidth,
1076
+ strokeLinecap: "round",
1077
+ strokeLinejoin: "round"
1078
+ }
1079
+ ),
1080
+ endMarker ? /* @__PURE__ */ jsx(
1081
+ "circle",
1082
+ {
1083
+ cx: last[0],
1084
+ cy: last[1],
1085
+ r: 4,
1086
+ fill: color,
1087
+ stroke: "var(--color-card)",
1088
+ strokeWidth: 2
1089
+ }
1090
+ ) : null
1091
+ ]
1092
+ }
1093
+ );
1094
+ }
1095
+ );
1096
+ Sparkline.displayName = "Sparkline";
1097
+ var TONE = {
1098
+ primary: "bg-primary",
1099
+ muted: "bg-muted-foreground",
1100
+ success: "bg-success",
1101
+ warning: "bg-warning",
1102
+ destructive: "bg-destructive"
1103
+ };
1104
+ var percent = (v) => `${Math.round(v * 100)}%`;
1105
+ var InlineMeter = React2.forwardRef(
1106
+ ({ value, max = 1, format = percent, tone = "primary", color, label, className, ...props }, ref) => {
1107
+ const none = value == null || value <= 0;
1108
+ const share = none ? 0 : Math.min(1, value / (max || 1));
1109
+ return /* @__PURE__ */ jsxs(
1110
+ "div",
1111
+ {
1112
+ ref,
1113
+ role: none ? void 0 : "meter",
1114
+ "aria-label": label,
1115
+ "aria-valuenow": none ? void 0 : value,
1116
+ "aria-valuemin": none ? void 0 : 0,
1117
+ "aria-valuemax": none ? void 0 : max,
1118
+ className: cn("flex min-w-0 items-center gap-2", className),
1119
+ ...props,
1120
+ children: [
1121
+ none ? null : /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-1.5 w-16 shrink-0 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
1122
+ "span",
1123
+ {
1124
+ className: cn("block h-full rounded-full", color ? void 0 : TONE[tone]),
1125
+ style: { width: `${share * 100}%`, background: color }
1126
+ }
1127
+ ) }),
1128
+ /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-muted-foreground", children: none ? "\u2014" : format(value) })
1129
+ ]
1130
+ }
1131
+ );
1132
+ }
1133
+ );
1134
+ InlineMeter.displayName = "InlineMeter";
1135
+ var whole2 = new Intl.NumberFormat();
1136
+ var SegmentedBar = React2.forwardRef(
1137
+ ({ values, series, format = (v) => whole2.format(v), className, ...props }, ref) => {
1138
+ const total = series.reduce((sum, s) => sum + Math.max(0, values[s.key] ?? 0), 0);
1139
+ const parts = series.map((s) => ({ ...s, value: Math.max(0, values[s.key] ?? 0) })).filter((p) => p.value > 0);
1140
+ const pct = (v) => `${Math.round(v / total * 100)}%`;
1141
+ const summary = total ? parts.map((p) => `${p.label} ${pct(p.value)}`).join(", ") : "nothing counted";
1142
+ return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 0, children: /* @__PURE__ */ jsx(
1143
+ "div",
1144
+ {
1145
+ ref,
1146
+ role: "img",
1147
+ "aria-label": summary,
1148
+ className: cn(
1149
+ "flex h-2 w-full min-w-16 gap-[2px] overflow-hidden rounded-full",
1150
+ total === 0 && "bg-muted",
1151
+ className
1152
+ ),
1153
+ ...props,
1154
+ children: parts.map((p) => /* @__PURE__ */ jsxs(Tooltip, { children: [
1155
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
1156
+ "span",
1157
+ {
1158
+ className: "h-full min-w-[2px]",
1159
+ style: { flexGrow: p.value, flexBasis: 0, background: p.color }
1160
+ }
1161
+ ) }),
1162
+ /* @__PURE__ */ jsx(TooltipContent, { children: /* @__PURE__ */ jsxs("span", { className: "tabular-nums", children: [
1163
+ p.label,
1164
+ " \xB7 ",
1165
+ format(p.value),
1166
+ " \xB7 ",
1167
+ pct(p.value)
1168
+ ] }) })
1169
+ ] }, p.key))
1170
+ }
1171
+ ) });
1172
+ }
1173
+ );
1174
+ SegmentedBar.displayName = "SegmentedBar";
1175
+ var SegmentedBarLegend = React2.forwardRef(
1176
+ ({ series, ...props }, ref) => /* @__PURE__ */ jsx(Legend, { ref, ...props, children: series.map((s) => /* @__PURE__ */ jsx(LegendItem, { color: s.color, label: s.label }, s.key)) })
1177
+ );
1178
+ SegmentedBarLegend.displayName = "SegmentedBarLegend";
1179
+ var MARK = {
1180
+ refused: "bg-destructive",
1181
+ egress: "bg-warning"
1182
+ };
1183
+ var HeatLane = React2.forwardRef(
1184
+ ({ values, marks, pinned, onPin, cellLabel, fill = "bg-info", stale, className, style, ...props }, ref) => {
1185
+ const markAt = React2.useMemo(() => new Map((marks ?? []).map((m) => [m.at, m.kind])), [marks]);
1186
+ return /* @__PURE__ */ jsx(
1187
+ "div",
1188
+ {
1189
+ ref,
1190
+ className: cn("grid h-3 gap-px", stale && "opacity-40 grayscale", className),
1191
+ style: { gridTemplateColumns: `repeat(${values.length}, minmax(0, 1fr))`, ...style },
1192
+ ...props,
1193
+ children: values.map((v, i) => {
1194
+ const mark = markAt.get(i);
1195
+ const label = `${cellLabel ? cellLabel(i) : i}${mark ? ` \xB7 ${mark}` : v == null ? " \xB7 not touched" : ""}`;
1196
+ const cls = cn(
1197
+ "block h-full min-w-0 rounded-[1px]",
1198
+ mark ? MARK[mark] : v == null ? "border border-border" : v === 0 ? "bg-muted" : fill,
1199
+ pinned === i && "ring-1 ring-foreground ring-offset-1 ring-offset-background"
1200
+ );
1201
+ const opacity = mark || v == null || v === 0 ? void 0 : Math.max(0.18, Math.min(1, v));
1202
+ return onPin && v != null ? /* @__PURE__ */ jsx(
1203
+ "button",
1204
+ {
1205
+ type: "button",
1206
+ title: label,
1207
+ "aria-label": label,
1208
+ "aria-pressed": pinned === i,
1209
+ className: cn(cls, "cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"),
1210
+ style: { opacity },
1211
+ onClick: () => onPin(pinned === i ? null : i)
1212
+ },
1213
+ i
1214
+ ) : /* @__PURE__ */ jsx("span", { title: label, className: cls, style: { opacity } }, i);
1215
+ })
1216
+ }
1217
+ );
1218
+ }
1219
+ );
1220
+ HeatLane.displayName = "HeatLane";
1221
+ var BarChartH = React2.forwardRef(
1222
+ ({
1223
+ data,
1224
+ max,
1225
+ labelWidth,
1226
+ color = "var(--color-data-1)",
1227
+ caption,
1228
+ variant = "tips",
1229
+ negativeColor = "var(--color-destructive)",
1230
+ diverging,
1231
+ className,
1232
+ ...props
1233
+ }, ref) => {
1234
+ if (variant === "ranked") {
1235
+ const ceiling2 = max ?? (Math.max(...data.map((d) => Math.abs(d.value)), 0) || 1);
1236
+ const share = (d) => Math.min(100, Math.abs(d.value) / ceiling2 * 100);
1237
+ const fill = (d) => d.color ?? (d.muted ? "color-mix(in srgb, var(--color-muted-foreground) 40%, transparent)" : d.value < 0 ? negativeColor : color);
1238
+ const columns = labelWidth != null ? { gridTemplateColumns: `${labelWidth}px minmax(0, 1fr) 40px` } : void 0;
1239
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("@container flex flex-col gap-1.5", className), ...props, children: [
1240
+ caption != null ? /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: caption }) : null,
1241
+ data.map((d, i) => /* @__PURE__ */ jsxs(
1242
+ "div",
1243
+ {
1244
+ className: cn(
1245
+ "grid items-center gap-[7px] text-sm",
1246
+ labelWidth == null && "grid-cols-[96px_minmax(0,1fr)_40px] @max-[290px]:grid-cols-[74px_minmax(0,1fr)_36px]"
1247
+ ),
1248
+ style: columns,
1249
+ children: [
1250
+ /* @__PURE__ */ jsx("span", { className: cn("min-w-0 truncate", d.muted && "text-muted-foreground"), children: d.label }),
1251
+ diverging ? /* @__PURE__ */ jsx("span", { className: "relative block h-2 before:absolute before:-inset-y-[3px] before:left-1/2 before:border-l before:border-border", children: /* @__PURE__ */ jsx(
1252
+ "span",
1253
+ {
1254
+ title: `${d.label}: ${d.display ?? d.value}`,
1255
+ className: "absolute top-0 h-2 rounded-[1px] opacity-85",
1256
+ style: {
1257
+ left: d.value < 0 ? `${50 - share(d) / 2}%` : "50%",
1258
+ width: `${share(d) / 2}%`,
1259
+ background: fill(d)
1260
+ }
1261
+ }
1262
+ ) }) : /* @__PURE__ */ jsx(
1263
+ "span",
1264
+ {
1265
+ title: `${d.label}: ${d.display ?? d.value}`,
1266
+ className: "h-2 rounded-[1px] opacity-85",
1267
+ style: { width: `${share(d)}%`, background: fill(d) }
1268
+ }
1269
+ ),
1270
+ /* @__PURE__ */ jsx("span", { className: "text-end font-mono text-xs tabular-nums", children: d.display ?? d.value })
1271
+ ]
1272
+ },
1273
+ i
1274
+ )),
1275
+ diverging ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [
1276
+ /* @__PURE__ */ jsx("span", { children: diverging.below }),
1277
+ /* @__PURE__ */ jsx("span", { children: "0" }),
1278
+ /* @__PURE__ */ jsx("span", { children: diverging.above })
1279
+ ] }) : null
1280
+ ] });
1281
+ }
1282
+ const ceiling = max ?? (Math.max(...data.map((d) => d.value), 0) || 1);
1283
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-1", className), ...props, children: [
1284
+ caption != null ? /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: caption }) : null,
1285
+ data.map((d, i) => /* @__PURE__ */ jsxs("div", { className: "flex h-4 items-center gap-2", children: [
1286
+ /* @__PURE__ */ jsx(
1287
+ "span",
1288
+ {
1289
+ className: "shrink-0 truncate text-sm text-muted-foreground",
1290
+ style: { width: labelWidth ?? 78 },
1291
+ children: d.label
1292
+ }
1293
+ ),
1294
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [
1295
+ /* @__PURE__ */ jsx(
1296
+ "span",
1297
+ {
1298
+ title: `${d.label}: ${d.display ?? d.value}`,
1299
+ className: "h-2.5 shrink-0 rounded-e-[4px]",
1300
+ style: {
1301
+ width: `${Math.max(0, d.value / ceiling * 100)}%`,
1302
+ background: d.color ?? color
1303
+ }
1304
+ }
1305
+ ),
1306
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums", children: d.display ?? d.value })
1307
+ ] })
1308
+ ] }, i))
1309
+ ] });
1310
+ }
1311
+ );
1312
+ BarChartH.displayName = "BarChartH";
1313
+ var isGrouped = (d) => "values" in d && Array.isArray(d.values);
1314
+ var BarChartV = React2.forwardRef(
1315
+ ({
1316
+ data,
1317
+ series,
1318
+ max,
1319
+ gridlines = [],
1320
+ target,
1321
+ height = 160,
1322
+ color = "var(--color-data-1)",
1323
+ highlightColor,
1324
+ highlightIndex,
1325
+ labelMode = "last",
1326
+ caption,
1327
+ variant = "default",
1328
+ planLabel,
1329
+ className,
1330
+ ...props
1331
+ }, ref) => {
1332
+ const grouped = data.some(isGrouped);
1333
+ const seriesColor = (k) => series?.[k]?.color ?? (k === 0 ? color : `var(--color-data-${k + 1})`);
1334
+ const emphasis = highlightIndex === void 0 ? grouped ? void 0 : data.length - 1 : highlightIndex ?? void 0;
1335
+ const marks = data.map(
1336
+ (d, i) => isGrouped(d) ? d.values.map((value, k) => ({
1337
+ value,
1338
+ display: d.display?.[k] ?? value,
1339
+ color: seriesColor(k)
1340
+ })) : [
1341
+ {
1342
+ plan: d.plan,
1343
+ value: d.value,
1344
+ display: d.display ?? d.value,
1345
+ color: i === emphasis ? highlightColor ?? color : color
1346
+ }
1347
+ ]
1348
+ );
1349
+ const largest = Math.max(
1350
+ 0,
1351
+ ...marks.flat().map((m) => Math.max(m.value, m.plan ?? 0)),
1352
+ target?.value ?? 0
1353
+ );
1354
+ const planned = marks.some((g) => g.some((m) => m.plan != null));
1355
+ const ceiling = max ?? (largest || 1);
1356
+ const labelled = (i) => labelMode === "all" || labelMode === "last" && i === emphasis;
1357
+ const at = (v) => `${v / ceiling * 100}%`;
1358
+ const comparison = variant === "comparison";
1359
+ const legend = series != null && series.length > 1 || planned && planLabel != null;
1360
+ const rules = [
1361
+ ...gridlines.map((g) => ({ key: `g${g}`, value: g, label: g })),
1362
+ // With a legend the target is named there, and a gutter label would only
1363
+ // take width from the plot.
1364
+ ...target?.label != null && !legend ? [{ key: "target", value: target.value, label: target.label }] : []
1365
+ ];
1366
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-1", className), ...props, children: [
1367
+ caption != null ? /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: caption }) : null,
1368
+ /* @__PURE__ */ jsxs("div", { className: "mt-2 grid grid-cols-[minmax(0,1fr)_auto] gap-y-1", children: [
1369
+ /* @__PURE__ */ jsxs("div", { className: "relative", style: { height }, children: [
1370
+ gridlines.map((g) => /* @__PURE__ */ jsx(
1371
+ "span",
1372
+ {
1373
+ "aria-hidden": true,
1374
+ className: "absolute inset-x-0 h-px bg-border",
1375
+ style: { bottom: at(g) }
1376
+ },
1377
+ g
1378
+ )),
1379
+ comparison ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-x-0 bottom-0 h-px bg-border" }) : null,
1380
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-end justify-between gap-2", children: marks.map((group, i) => /* @__PURE__ */ jsx("div", { className: "flex h-full min-w-0 flex-1 justify-center", children: /* @__PURE__ */ jsx(
1381
+ "div",
1382
+ {
1383
+ className: cn(
1384
+ "flex h-full min-w-0 items-end justify-center gap-0.5",
1385
+ comparison ? "w-2/3" : "w-full"
1386
+ ),
1387
+ children: group.map((m, k) => /* @__PURE__ */ jsxs(
1388
+ "div",
1389
+ {
1390
+ className: cn(
1391
+ "relative flex h-full min-w-0 flex-1 flex-col items-center justify-end gap-1",
1392
+ !comparison && "max-w-6"
1393
+ ),
1394
+ children: [
1395
+ m.plan != null ? /* @__PURE__ */ jsx(
1396
+ "span",
1397
+ {
1398
+ "aria-hidden": true,
1399
+ className: "absolute inset-x-0 bottom-0 border border-dashed border-muted-foreground",
1400
+ style: { height: `${Math.max(0, m.plan / ceiling * 100)}%` }
1401
+ }
1402
+ ) : null,
1403
+ labelled(i) ? /* @__PURE__ */ jsx(
1404
+ "span",
1405
+ {
1406
+ className: cn(
1407
+ "text-sm whitespace-nowrap tabular-nums",
1408
+ comparison && "text-muted-foreground"
1409
+ ),
1410
+ children: m.display
1411
+ }
1412
+ ) : null,
1413
+ /* @__PURE__ */ jsx(
1414
+ "span",
1415
+ {
1416
+ title: `${textOf(data[i].label)}: ${textOf(m.display)}`,
1417
+ className: cn(
1418
+ m.plan != null ? "relative w-2/3" : "w-full",
1419
+ !comparison && "rounded-t-[4px]"
1420
+ ),
1421
+ style: {
1422
+ height: `${Math.max(0, m.value / ceiling * 100)}%`,
1423
+ background: m.color,
1424
+ opacity: emphasis == null || i === emphasis ? 1 : 0.55
1425
+ }
1426
+ }
1427
+ )
1428
+ ]
1429
+ },
1430
+ k
1431
+ ))
1432
+ }
1433
+ ) }, i)) }),
1434
+ target ? /* @__PURE__ */ jsx(
1435
+ "span",
1436
+ {
1437
+ "aria-hidden": true,
1438
+ className: "absolute inset-x-0 border-t border-dashed",
1439
+ style: {
1440
+ bottom: at(target.value),
1441
+ borderColor: target.color ?? "var(--color-muted-foreground)"
1442
+ }
1443
+ }
1444
+ ) : null
1445
+ ] }),
1446
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1447
+ rules.map((r) => /* @__PURE__ */ jsx(
1448
+ "span",
1449
+ {
1450
+ "aria-hidden": true,
1451
+ className: "invisible block h-0 pl-1 text-sm whitespace-nowrap",
1452
+ children: r.label
1453
+ },
1454
+ r.key
1455
+ )),
1456
+ rules.map((r) => /* @__PURE__ */ jsx(
1457
+ "span",
1458
+ {
1459
+ className: "absolute left-0 translate-y-1/2 pl-1 text-sm whitespace-nowrap tabular-nums text-muted-foreground",
1460
+ style: { bottom: at(r.value) },
1461
+ children: r.label
1462
+ },
1463
+ r.key
1464
+ ))
1465
+ ] }),
1466
+ /* @__PURE__ */ jsx("div", { className: "flex justify-between gap-2", children: data.map((d, i) => /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col items-center", children: [
1467
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: d.label }),
1468
+ d.sublabel != null ? /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground/70", children: d.sublabel }) : null
1469
+ ] }, i)) })
1470
+ ] }),
1471
+ legend ? /* @__PURE__ */ jsxs(Legend, { children: [
1472
+ (series ?? []).map((s, k) => /* @__PURE__ */ jsx(LegendItem, { kind: "box", color: seriesColor(k), label: s.name }, k)),
1473
+ planned && planLabel != null ? /* @__PURE__ */ jsx(LegendItem, { kind: "outline", color: "var(--color-muted-foreground)", label: planLabel }) : null,
1474
+ target?.label != null ? /* @__PURE__ */ jsx(
1475
+ LegendItem,
1476
+ {
1477
+ kind: "dashed",
1478
+ color: target.color ?? "var(--color-muted-foreground)",
1479
+ label: target.label
1480
+ }
1481
+ ) : null
1482
+ ] }) : null
1483
+ ] });
1484
+ }
1485
+ );
1486
+ BarChartV.displayName = "BarChartV";
1487
+ function textOf(node) {
1488
+ return typeof node === "string" || typeof node === "number" ? String(node) : "";
1489
+ }
1490
+ var DivergingBar = React2.forwardRef(
1491
+ ({
1492
+ data,
1493
+ max,
1494
+ labelWidth = 104,
1495
+ positiveColor = "var(--color-success)",
1496
+ negativeColor = "var(--color-destructive)",
1497
+ caption,
1498
+ className,
1499
+ ...props
1500
+ }, ref) => {
1501
+ const ceiling = max ?? (Math.max(...data.map((d) => Math.abs(d.value)), 0) || 1);
1502
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-1", className), ...props, children: [
1503
+ caption != null ? /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: caption }) : null,
1504
+ data.map((d, i) => {
1505
+ const positive = d.value >= 0;
1506
+ const width = `${Math.abs(d.value) / ceiling * 50}%`;
1507
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-4 items-center gap-2", children: [
1508
+ /* @__PURE__ */ jsx(
1509
+ "span",
1510
+ {
1511
+ className: "shrink-0 truncate text-sm text-muted-foreground",
1512
+ style: { width: labelWidth },
1513
+ children: d.label
1514
+ }
1515
+ ),
1516
+ /* @__PURE__ */ jsxs("span", { className: "relative flex min-w-0 flex-1 items-center", children: [
1517
+ /* @__PURE__ */ jsx(
1518
+ "span",
1519
+ {
1520
+ "aria-hidden": true,
1521
+ className: "absolute left-1/2 h-4 w-px -translate-x-1/2 bg-border"
1522
+ }
1523
+ ),
1524
+ /* @__PURE__ */ jsx("span", { className: "flex w-1/2 justify-end", children: !positive ? /* @__PURE__ */ jsx(
1525
+ "span",
1526
+ {
1527
+ title: `${d.label}: ${d.display ?? d.value}`,
1528
+ className: "h-2.5 rounded-s-[4px]",
1529
+ style: { width, background: negativeColor }
1530
+ }
1531
+ ) : null }),
1532
+ /* @__PURE__ */ jsx("span", { className: "flex w-1/2 justify-start", children: positive ? /* @__PURE__ */ jsx(
1533
+ "span",
1534
+ {
1535
+ title: `${d.label}: ${d.display ?? d.value}`,
1536
+ className: "h-2.5 rounded-e-[4px]",
1537
+ style: { width, background: positiveColor }
1538
+ }
1539
+ ) : null })
1540
+ ] }),
1541
+ /* @__PURE__ */ jsx("span", { className: "w-8 shrink-0 text-right text-sm tabular-nums", children: d.display ?? (d.value > 0 ? `+${d.value}` : d.value) })
1542
+ ] }, i);
1543
+ })
1544
+ ] });
1545
+ }
1546
+ );
1547
+ DivergingBar.displayName = "DivergingBar";
1548
+ var INDENT = 12;
1549
+ var everyRow = (rows) => rows.flatMap((r) => [r, ...everyRow(r.children ?? [])]);
1550
+ function Cells({
1551
+ cells,
1552
+ byKey,
1553
+ cellSize,
1554
+ className
1555
+ }) {
1556
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-1", className), children: cells.map((c, i) => {
1557
+ const s = byKey.get(c.state);
1558
+ return /* @__PURE__ */ jsx(
1559
+ "span",
1560
+ {
1561
+ title: `${c.at} \xB7 ${s?.label ?? c.state}${c.detail ? ` \xB7 ${c.detail}` : ""}`,
1562
+ className: cn("shrink-0 rounded-[2px]", s?.hollow && "border"),
1563
+ style: {
1564
+ width: cellSize,
1565
+ height: cellSize,
1566
+ background: s?.hollow ? "transparent" : s?.color ?? "var(--color-muted)",
1567
+ borderColor: s?.hollow ? s.color ?? "var(--color-border)" : void 0
1568
+ }
1569
+ },
1570
+ i
1571
+ );
1572
+ }) });
1573
+ }
1574
+ function Ticks({
1575
+ count,
1576
+ ticks,
1577
+ cellSize,
1578
+ className
1579
+ }) {
1580
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-1", className), "aria-hidden": true, children: Array.from({ length: count }, (_, i) => {
1581
+ const tick = ticks.find((t) => t.at === i);
1582
+ return /* @__PURE__ */ jsx(
1583
+ "span",
1584
+ {
1585
+ className: "shrink-0 text-sm tabular-nums text-muted-foreground",
1586
+ style: { width: cellSize },
1587
+ children: tick ? tick.label : ""
1588
+ },
1589
+ i
1590
+ );
1591
+ }) });
1592
+ }
1593
+ var HeatStrip = React2.forwardRef(
1594
+ ({
1595
+ cells,
1596
+ rows,
1597
+ states,
1598
+ ticks,
1599
+ cellSize = 14,
1600
+ labelWidth,
1601
+ expanded,
1602
+ defaultExpanded,
1603
+ onExpandedChange,
1604
+ className,
1605
+ ...props
1606
+ }, ref) => {
1607
+ const byKey = React2.useMemo(
1608
+ () => new Map(states.map((s) => [s.key, s])),
1609
+ [states]
1610
+ );
1611
+ const parents = everyRow(rows ?? []).filter((r) => r.children?.length).map((r) => r.key);
1612
+ const nested = parents.length > 0;
1613
+ const { isOpen, toggle } = useExpandedKeys({
1614
+ expanded,
1615
+ defaultExpanded,
1616
+ onExpandedChange,
1617
+ keys: parents
1618
+ });
1619
+ const shown = [];
1620
+ const walk = (list, depth) => {
1621
+ for (const row of list) {
1622
+ shown.push({ row, depth });
1623
+ if (row.children?.length && isOpen(row.key)) walk(row.children, depth + 1);
1624
+ }
1625
+ };
1626
+ walk(rows ?? [], 0);
1627
+ const count = Math.max(cells?.length ?? 0, ...shown.map((r) => r.row.cells.length));
1628
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-1.5", className), ...props, children: [
1629
+ rows ? /* @__PURE__ */ jsxs(
1630
+ "div",
1631
+ {
1632
+ className: "grid items-center gap-x-2 gap-y-1",
1633
+ style: {
1634
+ gridTemplateColumns: `${labelWidth != null ? `${labelWidth}px` : "fit-content(40%)"} minmax(0, 1fr)`
1635
+ },
1636
+ children: [
1637
+ shown.map(({ row, depth }) => {
1638
+ const kids = (row.children?.length ?? 0) > 0;
1639
+ const open = kids && isOpen(row.key);
1640
+ return /* @__PURE__ */ jsxs(React2.Fragment, { children: [
1641
+ /* @__PURE__ */ jsxs(
1642
+ "span",
1643
+ {
1644
+ className: cn("flex min-w-0 items-center gap-1 text-sm", depth > 0 && "text-muted-foreground"),
1645
+ style: { paddingLeft: depth * INDENT },
1646
+ children: [
1647
+ kids ? /* @__PURE__ */ jsx(
1648
+ ExpandToggle,
1649
+ {
1650
+ open,
1651
+ label: typeof row.label === "string" ? row.label : row.key,
1652
+ onClick: () => toggle(row.key)
1653
+ }
1654
+ ) : nested ? /* @__PURE__ */ jsx(ExpandToggle.Spacer, {}) : null,
1655
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: row.label })
1656
+ ]
1657
+ }
1658
+ ),
1659
+ /* @__PURE__ */ jsx(
1660
+ Cells,
1661
+ {
1662
+ cells: row.cells,
1663
+ byKey,
1664
+ cellSize,
1665
+ className: "min-w-0 overflow-hidden"
1666
+ }
1667
+ )
1668
+ ] }, row.key);
1669
+ }),
1670
+ ticks?.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
1671
+ /* @__PURE__ */ jsx("span", {}),
1672
+ /* @__PURE__ */ jsx(Ticks, { count, ticks, cellSize, className: "min-w-0 overflow-hidden" })
1673
+ ] }) : null
1674
+ ]
1675
+ }
1676
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1677
+ /* @__PURE__ */ jsx(Cells, { cells: cells ?? [], byKey, cellSize, className: "flex-wrap" }),
1678
+ ticks?.length ? /* @__PURE__ */ jsx(Ticks, { count, ticks, cellSize, className: "flex-wrap" }) : null
1679
+ ] }),
1680
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-sm", children: states.map((s) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5", children: [
1681
+ /* @__PURE__ */ jsx(
1682
+ "span",
1683
+ {
1684
+ "aria-hidden": true,
1685
+ className: cn("size-2.5 shrink-0 rounded-[2px]", s.hollow && "border"),
1686
+ style: {
1687
+ background: s.hollow ? "transparent" : s.color ?? "var(--color-muted)",
1688
+ borderColor: s.hollow ? s.color ?? "var(--color-border)" : void 0
1689
+ }
1690
+ }
1691
+ ),
1692
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: s.label })
1693
+ ] }, s.key)) })
1694
+ ] });
1695
+ }
1696
+ );
1697
+ HeatStrip.displayName = "HeatStrip";
1698
+
1699
+ export { BarChartH, BarChartV, DivergingBar, HeatLane, HeatStrip, InlineMeter, LineChart, SegmentedBar, SegmentedBarLegend, Sparkline, StackedAreaChart, StackedBarChartV };
1700
+ //# sourceMappingURL=index.js.map
1701
+ //# sourceMappingURL=index.js.map