@seekalfred/primitives 0.7.1 → 0.7.4

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.
Files changed (147) hide show
  1. package/README.md +255 -5
  2. package/dist/alert/alert.d.ts +49 -4
  3. package/dist/alert/alert.js +66 -38
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert/index.d.ts +2 -2
  6. package/dist/alert/index.js +2 -2
  7. package/dist/alert-dialog/alert-dialog.d.ts +31 -4
  8. package/dist/alert-dialog/alert-dialog.js +75 -26
  9. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  10. package/dist/announcement-bar/announcement-bar.d.ts +15 -4
  11. package/dist/announcement-bar/announcement-bar.js +31 -6
  12. package/dist/announcement-bar/announcement-bar.js.map +1 -1
  13. package/dist/avatar/avatar.d.ts +7 -0
  14. package/dist/avatar/avatar.js +33 -12
  15. package/dist/avatar/avatar.js.map +1 -1
  16. package/dist/badge/badge.d.ts +1 -1
  17. package/dist/bar-chart/bar-chart-series.d.ts +75 -0
  18. package/dist/bar-chart/bar-chart-series.js +401 -0
  19. package/dist/bar-chart/bar-chart-series.js.map +1 -0
  20. package/dist/bar-chart/bar-chart.d.ts +25 -1
  21. package/dist/bar-chart/bar-chart.js +26 -1
  22. package/dist/bar-chart/bar-chart.js.map +1 -1
  23. package/dist/bar-chart/index.d.ts +2 -1
  24. package/dist/button/button.d.ts +1 -1
  25. package/dist/calendar/calendar.d.ts +1 -1
  26. package/dist/card/card.d.ts +22 -1
  27. package/dist/card/card.js +60 -7
  28. package/dist/card/card.js.map +1 -1
  29. package/dist/carousel/carousel.d.ts +21 -0
  30. package/dist/carousel/carousel.js +34 -8
  31. package/dist/carousel/carousel.js.map +1 -1
  32. package/dist/chip-group/chip-group.d.ts +11 -0
  33. package/dist/chip-group/chip-group.js +50 -34
  34. package/dist/chip-group/chip-group.js.map +1 -1
  35. package/dist/chip-group/index.d.ts +2 -2
  36. package/dist/copy-button/copy-button.d.ts +37 -5
  37. package/dist/copy-button/copy-button.js +35 -11
  38. package/dist/copy-button/copy-button.js.map +1 -1
  39. package/dist/donut-chart/donut-chart.d.ts +22 -0
  40. package/dist/donut-chart/donut-chart.js +35 -18
  41. package/dist/donut-chart/donut-chart.js.map +1 -1
  42. package/dist/donut-chart/index.d.ts +2 -2
  43. package/dist/empty-state/empty-state.d.ts +21 -0
  44. package/dist/empty-state/empty-state.js +16 -4
  45. package/dist/empty-state/empty-state.js.map +1 -1
  46. package/dist/empty-state/index.d.ts +2 -2
  47. package/dist/empty-state/index.js +2 -2
  48. package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
  49. package/dist/file-drop-zone/file-drop-zone.js +26 -8
  50. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  51. package/dist/filter-bar/filter-bar.d.ts +4 -1
  52. package/dist/filter-bar/filter-bar.js +53 -9
  53. package/dist/filter-bar/filter-bar.js.map +1 -1
  54. package/dist/gauge/gauge.d.ts +18 -1
  55. package/dist/gauge/gauge.js +35 -21
  56. package/dist/gauge/gauge.js.map +1 -1
  57. package/dist/gauge/index.d.ts +2 -2
  58. package/dist/index.d.ts +15 -14
  59. package/dist/index.js +9 -9
  60. package/dist/input/input.d.ts +7 -0
  61. package/dist/input/input.js +7 -5
  62. package/dist/input/input.js.map +1 -1
  63. package/dist/lib/field-shell.js +6 -3
  64. package/dist/lib/field-shell.js.map +1 -1
  65. package/dist/lib/field-surface.js +22 -1
  66. package/dist/lib/field-surface.js.map +1 -1
  67. package/dist/lib/required-marker.js +20 -0
  68. package/dist/lib/required-marker.js.map +1 -0
  69. package/dist/lib/use-copy-feedback.js +17 -3
  70. package/dist/lib/use-copy-feedback.js.map +1 -1
  71. package/dist/line-chart/index.d.ts +2 -2
  72. package/dist/line-chart/line-chart.d.ts +144 -2
  73. package/dist/line-chart/line-chart.js +314 -46
  74. package/dist/line-chart/line-chart.js.map +1 -1
  75. package/dist/money-input/index.d.ts +2 -2
  76. package/dist/money-input/money-input.d.ts +36 -1
  77. package/dist/money-input/money-input.js +30 -14
  78. package/dist/money-input/money-input.js.map +1 -1
  79. package/dist/morph-select/morph-select.d.ts +26 -1
  80. package/dist/morph-select/morph-select.js +28 -6
  81. package/dist/morph-select/morph-select.js.map +1 -1
  82. package/dist/multi-select/index.d.ts +2 -2
  83. package/dist/multi-select/index.js +2 -2
  84. package/dist/multi-select/multi-select.d.ts +74 -2
  85. package/dist/multi-select/multi-select.js +213 -39
  86. package/dist/multi-select/multi-select.js.map +1 -1
  87. package/dist/password-field/password-field.d.ts +16 -0
  88. package/dist/password-field/password-field.js +37 -8
  89. package/dist/password-field/password-field.js.map +1 -1
  90. package/dist/popover/index.d.ts +2 -2
  91. package/dist/popover/index.js +2 -2
  92. package/dist/popover/popover.d.ts +39 -2
  93. package/dist/popover/popover.js +29 -2
  94. package/dist/popover/popover.js.map +1 -1
  95. package/dist/progress/index.d.ts +2 -2
  96. package/dist/progress/index.js +2 -2
  97. package/dist/progress/progress.d.ts +68 -3
  98. package/dist/progress/progress.js +156 -39
  99. package/dist/progress/progress.js.map +1 -1
  100. package/dist/radio-group/index.d.ts +2 -2
  101. package/dist/radio-group/index.js +2 -2
  102. package/dist/radio-group/radio-group.d.ts +33 -3
  103. package/dist/radio-group/radio-group.js +109 -9
  104. package/dist/radio-group/radio-group.js.map +1 -1
  105. package/dist/scroll-area/scroll-area.js +4 -3
  106. package/dist/scroll-area/scroll-area.js.map +1 -1
  107. package/dist/scrub-input/scrub-input.d.ts +11 -1
  108. package/dist/scrub-input/scrub-input.js +22 -10
  109. package/dist/scrub-input/scrub-input.js.map +1 -1
  110. package/dist/search-field/search-field.d.ts +22 -1
  111. package/dist/search-field/search-field.js +22 -7
  112. package/dist/search-field/search-field.js.map +1 -1
  113. package/dist/select/select.d.ts +10 -1
  114. package/dist/select/select.js +11 -8
  115. package/dist/select/select.js.map +1 -1
  116. package/dist/separator/separator.d.ts +21 -9
  117. package/dist/separator/separator.js +33 -11
  118. package/dist/separator/separator.js.map +1 -1
  119. package/dist/sparkline/sparkline.d.ts +27 -2
  120. package/dist/sparkline/sparkline.js +115 -18
  121. package/dist/sparkline/sparkline.js.map +1 -1
  122. package/dist/stat-card/index.d.ts +2 -2
  123. package/dist/stat-card/index.js +2 -2
  124. package/dist/stat-card/stat-card.d.ts +65 -1
  125. package/dist/stat-card/stat-card.js +98 -25
  126. package/dist/stat-card/stat-card.js.map +1 -1
  127. package/dist/styles.css +1 -1
  128. package/dist/table/index.d.ts +2 -2
  129. package/dist/table/index.js +2 -2
  130. package/dist/table/table.d.ts +115 -5
  131. package/dist/table/table.js +374 -94
  132. package/dist/table/table.js.map +1 -1
  133. package/dist/text-shimmer/text-shimmer.d.ts +18 -2
  134. package/dist/text-shimmer/text-shimmer.js +5 -2
  135. package/dist/text-shimmer/text-shimmer.js.map +1 -1
  136. package/dist/textarea/textarea.d.ts +7 -0
  137. package/dist/textarea/textarea.js +3 -1
  138. package/dist/textarea/textarea.js.map +1 -1
  139. package/dist/time-picker/time-picker.d.ts +36 -1
  140. package/dist/time-picker/time-picker.js +33 -5
  141. package/dist/time-picker/time-picker.js.map +1 -1
  142. package/dist/toast/toast.d.ts +2 -2
  143. package/dist/usage-meter/index.d.ts +2 -2
  144. package/dist/usage-meter/usage-meter.d.ts +30 -0
  145. package/dist/usage-meter/usage-meter.js +56 -38
  146. package/dist/usage-meter/usage-meter.js.map +1 -1
  147. package/package.json +3 -3
@@ -4,11 +4,13 @@ import { mergeRefs } from "../lib/merge-refs.js";
4
4
  import { prefersReducedMotion, useIsomorphicLayoutEffect } from "../lib/motion.js";
5
5
  import { SMOOTH, SNAPPY, tween } from "../lib/spring-tween.js";
6
6
  import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
7
- import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/line-chart/line-chart.tsx
9
9
  /** Room above the top gridline, and the gap between the crosshair and its tooltip. */
10
10
  const TOP = 12;
11
11
  const GAP = 14;
12
+ /** Between a point and its label. */
13
+ const LABEL_GAP = 6;
12
14
  /** The first draw: 480ms x 1.75, slow at both ends. */
13
15
  const LINE_CHART_DRAW_MS = 840;
14
16
  const PALETTE = [
@@ -25,9 +27,9 @@ const GRID = Array.from({ length: 241 }, (_, index) => index / 240);
25
27
  const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
26
28
  const colorOf = (line, index) => line.color ?? PALETTE[Math.min(index, PALETTE.length - 1)];
27
29
  const clean = (value) => Number(value.toPrecision(12));
28
- /** Clean gridlines: the smallest step of 1, 2, 2.5 or 5 that covers the data in four rows or fewer, from zero unless the data dips below it. */
29
- function niceScale(low, high) {
30
- const lo = Math.min(0, low);
30
+ /** Clean gridlines: the smallest step of 1, 2, 2.5 or 5 that covers the data in four rows or fewer, from zero unless the data dips below it (or `fromZero` is off). */
31
+ function niceScale(low, high, fromZero = true) {
32
+ const lo = fromZero ? Math.min(0, low) : low;
31
33
  const hi = Math.max(high, lo + 1);
32
34
  const magnitude = 10 ** Math.floor(Math.log10((hi - lo) / 4));
33
35
  for (const factor of [
@@ -56,6 +58,23 @@ function niceScale(low, high) {
56
58
  ticks: [lo, hi]
57
59
  };
58
60
  }
61
+ /** The nice scale with either end pinned: a pinned end is a tick, and the nice ticks within half a step of it give way. */
62
+ function pinScale(scale, min, max) {
63
+ if (min === void 0 && max === void 0) return scale;
64
+ const lo = min ?? scale.min;
65
+ const hi = max ?? scale.max;
66
+ if (!(hi > lo)) return scale;
67
+ const step = scale.ticks.length > 1 ? scale.ticks[1] - scale.ticks[0] : hi - lo;
68
+ return {
69
+ min: lo,
70
+ max: hi,
71
+ ticks: [
72
+ lo,
73
+ ...scale.ticks.filter((tick) => tick > lo + step / 2 && tick < hi - step / 2),
74
+ hi
75
+ ]
76
+ };
77
+ }
59
78
  /** The line through every value, sampled densely on x from 0 to 1 in data units. Monotone tangents keep a smooth curve from swinging past the data. */
60
79
  function curveFor(values, smooth) {
61
80
  if (values.length === 0) return [];
@@ -88,6 +107,65 @@ function curveFor(values, smooth) {
88
107
  points.push([1, values[last]]);
89
108
  return points;
90
109
  }
110
+ /** Fills each missing value straight between its present neighbours (or with the nearest one at the ends), so a gapped line still has a shape to morph. */
111
+ function fillGaps(values) {
112
+ const present = values.flatMap((value, index) => value === null ? [] : [index]);
113
+ if (!present.length) return values.map(() => 0);
114
+ return values.map((value, index) => {
115
+ if (value !== null) return value;
116
+ const after = present.find((at) => at > index);
117
+ const before = [...present].reverse().find((at) => at < index);
118
+ if (before === void 0) return values[after];
119
+ if (after === void 0) return values[before];
120
+ return values[before] + (values[after] - values[before]) * (index - before) / (after - before);
121
+ });
122
+ }
123
+ /** Where a line with missing values is not drawn: before its first value, after its last, and (unless it connects across them) between two values with missing ones between them. */
124
+ function breaksFor(values, across = false) {
125
+ const present = values.flatMap((value, index) => value === null ? [] : [index]);
126
+ if (!present.length) return values.length ? [[-Infinity, Infinity]] : [];
127
+ const last = values.length - 1;
128
+ if (last === 0) return [];
129
+ const breaks = [];
130
+ if (present[0] > 0) breaks.push([-Infinity, present[0] / last]);
131
+ for (let at = 1; at < present.length; at++) if (!across && present[at] - present[at - 1] > 1) breaks.push([present[at - 1] / last, present[at] / last]);
132
+ if (present[present.length - 1] < last) breaks.push([present[present.length - 1] / last, Infinity]);
133
+ return breaks;
134
+ }
135
+ function holesFor(values) {
136
+ const last = values.length - 1;
137
+ return values.flatMap((value, index) => value === null && last > 0 ? [index / last] : []);
138
+ }
139
+ const EDGE = 1e-9;
140
+ /** Radius of a lone point's dot, inside its 2px stroke. */
141
+ const DOT = 1.5;
142
+ const inBreak = (breaks, x) => breaks.some(([from, to]) => x > from + EDGE && x < to - EDGE);
143
+ /** The drawn runs of a line, split at its gaps. A run of one point is a dot: a small closed circle the stroke fills out to about 5px. */
144
+ function pathsFor(shape, breaks, toX, toY, bottom) {
145
+ const runs = [];
146
+ let run = null;
147
+ for (const [x, value] of shape) {
148
+ if (breaks.length && inBreak(breaks, x)) {
149
+ run = null;
150
+ continue;
151
+ }
152
+ if (!run) runs.push(run = []);
153
+ run.push(`${toX(x).toFixed(1)},${toY(value).toFixed(1)}`);
154
+ }
155
+ return {
156
+ line: runs.map((points) => {
157
+ if (points.length > 1) return `M${points.join("L")}`;
158
+ const [x, y] = points[0].split(",").map(Number);
159
+ return `M${(x - DOT).toFixed(1)},${y.toFixed(1)}a${DOT},${DOT} 0 1,0 ${DOT * 2},0a${DOT},${DOT} 0 1,0 -3,0`;
160
+ }).join(""),
161
+ area: runs.map((points) => {
162
+ const first = points[0].split(",")[0];
163
+ const lastX = points[points.length - 1].split(",")[0];
164
+ return `M${points.join("L")}L${lastX},${bottom}L${first},${bottom}Z`;
165
+ }).join("")
166
+ };
167
+ }
168
+ const isFiniteValue = (reference) => typeof reference.value === "number" && Number.isFinite(reference.value);
91
169
  /** Value of a sampled line at x, straight between samples. */
92
170
  function valueAt(shape, x) {
93
171
  if (!shape.length) return 0;
@@ -112,7 +190,7 @@ function axisPicks(data, width) {
112
190
  const stride = Math.ceil(labeled.length / room);
113
191
  return labeled.reverse().filter((_, rank) => rank % stride === 0).reverse();
114
192
  }
115
- const LineChart = forwardRef(function LineChart({ data, series, label, unit = "", height = 220, formatValue, formatTick = (value) => COMPACT.format(value), hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, onActiveChange, loading = false, emptyLabel = "No data for this range", legend, categoryLabel = "Date", curve = "smooth", className }, ref) {
193
+ const LineChart = forwardRef(function LineChart({ data, series, label, unit = "", height = 220, formatValue, formatTick = (value) => COMPACT.format(value), hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, onActiveChange, loading = false, emptyLabel = "No data for this range", legend, categoryLabel = "Date", curve = "smooth", missing = "zero", missingLabel = "No data", references, markers, reversed = false, valueDomain, bands, renderTooltip, className }, ref) {
116
194
  const figure = useRef(null);
117
195
  const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
118
196
  const plot = useRef(null);
@@ -131,18 +209,40 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
131
209
  };
132
210
  const visible = series.filter((line) => !hidden.includes(line.key));
133
211
  const showLegend = legend ?? series.length > 1;
134
- const signature = `${curve}|${series.map((line) => line.key).join(",")}|${data.map((item) => `${item.key}:${series.map((line) => item.values[line.key] ?? 0).join(",")}`).join(";")}`;
135
- const targets = useMemo(() => new Map(series.map((line) => [line.key, curveFor(data.map((item) => item.values[line.key] ?? 0), curve === "smooth")])), [signature]);
212
+ const gaps = missing !== "zero";
213
+ const read = (item, key) => {
214
+ const value = item.values[key];
215
+ if (!gaps) return value ?? 0;
216
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
217
+ };
218
+ const signature = `${curve}|${missing}|${series.map((line) => line.key).join(",")}|${data.map((item) => `${item.key}:${series.map((line) => read(item, line.key)).join(",")}`).join(";")}`;
219
+ const targets = useMemo(() => new Map(series.map((line) => {
220
+ const values = data.map((item) => read(item, line.key));
221
+ return [line.key, {
222
+ shape: curveFor(fillGaps(values), curve === "smooth"),
223
+ breaks: breaksFor(values, missing === "connect"),
224
+ holes: holesFor(values)
225
+ }];
226
+ })), [signature]);
136
227
  let low = Infinity;
137
228
  let high = -Infinity;
138
229
  for (const line of visible) for (const item of data) {
139
- const value = item.values[line.key] ?? 0;
230
+ const value = read(item, line.key);
231
+ if (value === null) continue;
140
232
  low = Math.min(low, value);
141
233
  high = Math.max(high, value);
142
234
  }
143
- const range = Number.isFinite(low) ? niceScale(low, high) : null;
144
- const [steady, setSteady] = useState(() => range ?? niceScale(0, 100));
145
- if (range && (range.min !== steady.min || range.max !== steady.max)) setSteady(range);
235
+ for (const reference of references ?? []) {
236
+ if (!isFiniteValue(reference)) continue;
237
+ low = Math.min(low, reference.value);
238
+ high = Math.max(high, reference.value);
239
+ }
240
+ const pinMin = Number.isFinite(valueDomain?.min) ? valueDomain.min : void 0;
241
+ const pinMax = Number.isFinite(valueDomain?.max) ? valueDomain.max : void 0;
242
+ const fit = (from, to) => pinScale(niceScale(pinMin ?? from, pinMax ?? to, pinMin === void 0), pinMin, pinMax);
243
+ const range = Number.isFinite(low) ? fit(low, high) : pinMin !== void 0 && pinMax !== void 0 ? fit(pinMin, pinMax) : null;
244
+ const [steady, setSteady] = useState(() => range ?? fit(0, 100));
245
+ if (range && (range.min !== steady.min || range.max !== steady.max || range.ticks.join() !== steady.ticks.join())) setSteady(range);
146
246
  const [active, setActive] = useState(null);
147
247
  const [seen, setSeen] = useState(signature);
148
248
  if (seen !== signature) {
@@ -160,6 +260,15 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
160
260
  max: steady.max
161
261
  });
162
262
  const cursor = useRef(1);
263
+ const referenceAt = (reference) => {
264
+ if (typeof reference.value === "number") return isFiniteValue(reference) ? null : void 0;
265
+ const at = data.findIndex((item) => item.key === reference.at);
266
+ return at < 0 ? void 0 : last > 0 ? at / last : .5;
267
+ };
268
+ const drawnReferences = (references ?? []).filter((reference) => referenceAt(reference) !== void 0);
269
+ const markerIndex = (marker) => data.findIndex((item) => item.key === marker.at);
270
+ const drawnMarkers = (markers ?? []).filter((marker) => markerIndex(marker) >= 0);
271
+ const drawnBands = (bands ?? []).filter((band) => Number.isFinite(band.from) && Number.isFinite(band.to));
163
272
  const live = useRef({
164
273
  series,
165
274
  hidden
@@ -177,25 +286,35 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
177
286
  if (!w || !root) return;
178
287
  const lo = scale.current.min;
179
288
  const span = scale.current.max - lo || 1;
180
- const toY = (value) => TOP + (1 - (value - lo) / span) * (height - TOP);
289
+ const toY = (value) => TOP + (reversed ? (value - lo) / span : 1 - (value - lo) / span) * (height - TOP);
290
+ const base = reversed ? scale.current.max : lo;
181
291
  const at = cursor.current;
182
292
  const dotYs = [];
183
293
  for (const line of live.current.series) {
184
294
  const track = tracks.current.get(line.key);
185
295
  if (!track) continue;
186
296
  const p = track.presence;
187
- const y = (value) => toY(lo + (value - lo) * p);
188
- const path = track.shape.length ? `M${track.shape.map(([x, value]) => `${(x * w).toFixed(1)},${y(value).toFixed(1)}`).join("L")}` : "";
297
+ const y = (value) => toY(base + (value - base) * p);
189
298
  const group = root.querySelector(`[data-series="${CSS.escape(line.key)}"]`);
190
- group?.querySelector("[data-part=line]")?.setAttribute("d", path);
191
- group?.querySelector("[data-part=area]")?.setAttribute("d", path ? `${path}L${w},${height}L0,${height}Z` : "");
299
+ if (track.breaks.length) {
300
+ const paths = pathsFor(track.shape, track.breaks, (x) => x * w, y, height);
301
+ group?.querySelector("[data-part=line]")?.setAttribute("d", paths.line);
302
+ group?.querySelector("[data-part=area]")?.setAttribute("d", paths.area);
303
+ } else {
304
+ const path = track.shape.length ? `M${track.shape.map(([x, value]) => `${(x * w).toFixed(1)},${y(value).toFixed(1)}`).join("L")}` : "";
305
+ group?.querySelector("[data-part=line]")?.setAttribute("d", path);
306
+ group?.querySelector("[data-part=area]")?.setAttribute("d", path ? `${path}L${w},${height}L0,${height}Z` : "");
307
+ }
192
308
  if (group) group.style.opacity = String(p);
193
309
  const dot = root.querySelector(`[data-dot="${CSS.escape(line.key)}"]`);
194
310
  if (dot && track.shape.length) {
311
+ const gap = inBreak(track.breaks, at) || track.holes.some((x) => Math.abs(x - at) < 1e-6);
195
312
  const cy = y(valueAt(track.shape, at));
196
313
  dot.setAttribute("cx", (at * w).toFixed(1));
197
314
  dot.setAttribute("cy", cy.toFixed(1));
198
- if (p > .5) dotYs.push(cy);
315
+ if (gap) dot.setAttribute("data-gap", "");
316
+ else dot.removeAttribute("data-gap");
317
+ if (p > .5 && !gap) dotYs.push(cy);
199
318
  }
200
319
  }
201
320
  for (const node of root.querySelectorAll("[data-grid]")) {
@@ -204,6 +323,43 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
204
323
  node.setAttribute("y2", yy);
205
324
  }
206
325
  for (const node of gutter.current?.querySelectorAll("[data-tick]") ?? []) node.style.translate = `0 ${Math.round(toY(Number(node.dataset.tick)))}px`;
326
+ for (const node of root.querySelectorAll("[data-band]")) {
327
+ const ys = [toY(Number(node.dataset.from)), toY(Number(node.dataset.to))];
328
+ const top = clamp(Math.min(...ys), TOP, height);
329
+ const bottom = clamp(Math.max(...ys), TOP, height);
330
+ node.setAttribute("y", String(Math.round(top)));
331
+ node.setAttribute("height", String(Math.max(0, Math.round(bottom) - Math.round(top))));
332
+ const text = plot.current?.querySelector(`[data-band-label="${CSS.escape(node.dataset.band)}"]`);
333
+ if (text) {
334
+ text.style.translate = `0 ${Math.round(top)}px`;
335
+ text.style.visibility = bottom - top >= text.offsetHeight ? "" : "hidden";
336
+ }
337
+ }
338
+ for (const node of root.querySelectorAll("[data-reference-value]")) {
339
+ const yy = String(Math.round(toY(Number(node.dataset.referenceValue))) + .5);
340
+ node.setAttribute("y1", yy);
341
+ node.setAttribute("y2", yy);
342
+ }
343
+ for (const node of root.querySelectorAll("[data-reference-at]")) {
344
+ const xx = String(Math.round(Number(node.dataset.referenceAt) * w) + .5);
345
+ node.setAttribute("x1", xx);
346
+ node.setAttribute("x2", xx);
347
+ }
348
+ for (const node of plot.current?.querySelectorAll("[data-reference-label]") ?? []) {
349
+ const { referenceValue, referenceAt } = node.dataset;
350
+ node.style.translate = referenceValue !== void 0 ? `0 ${Math.round(toY(Number(referenceValue)))}px` : `${Math.round(Number(referenceAt) * w)}px 0`;
351
+ }
352
+ for (const node of plot.current?.querySelectorAll("[data-point-label]") ?? []) {
353
+ const track = tracks.current.get(node.dataset.pointLabel);
354
+ if (!track?.shape.length) continue;
355
+ const x = Number(node.dataset.x);
356
+ const cy = toY(base + (valueAt(track.shape, x) - base) * track.presence);
357
+ const lw = node.offsetWidth;
358
+ const lh = node.offsetHeight;
359
+ const top = cy - lh - LABEL_GAP < 0 ? cy + LABEL_GAP : cy - lh - LABEL_GAP;
360
+ node.style.translate = `${Math.round(clamp(x * w - lw / 2, 0, Math.max(0, w - lw)))}px ${Math.round(clamp(top, 0, Math.max(0, height - lh)))}px`;
361
+ node.style.opacity = String(track.presence);
362
+ }
207
363
  const cx = Math.round(at * w) + .5;
208
364
  const cross = root.querySelector("[data-part=crosshair]");
209
365
  cross?.setAttribute("x1", String(cx));
@@ -217,7 +373,7 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
217
373
  const middle = dotYs.length ? dotYs.reduce((sum, value) => sum + value, 0) / dotYs.length : height / 2;
218
374
  bubble.style.translate = `${clamp(x, 0, Math.max(0, w - tw))}px ${clamp(middle - th / 2, 0, Math.max(0, height - th))}px`;
219
375
  }
220
- }, [height]);
376
+ }, [height, reversed]);
221
377
  useIsomorphicLayoutEffect(() => {
222
378
  const node = plot.current;
223
379
  if (!node) return;
@@ -235,16 +391,20 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
235
391
  useIsomorphicLayoutEffect(paint);
236
392
  useEffect(() => {
237
393
  const moves = [];
238
- for (const [key, target] of targets) {
394
+ for (const [key, { shape: target, breaks, holes }] of targets) {
239
395
  const track = tracks.current.get(key);
240
396
  if (!track) {
241
397
  tracks.current.set(key, {
242
398
  shape: target,
399
+ breaks,
400
+ holes,
243
401
  presence: live.current.hidden.includes(key) ? 0 : 1,
244
402
  stop: null
245
403
  });
246
404
  continue;
247
405
  }
406
+ track.breaks = breaks;
407
+ track.holes = holes;
248
408
  if (!track.shape.length || !target.length) {
249
409
  track.shape = target;
250
410
  continue;
@@ -375,11 +535,66 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
375
535
  const onPointerMove = (event) => {
376
536
  if (!empty && (event.pointerType === "mouse" || event.buttons)) setActive(pointAt(event.clientX));
377
537
  };
378
- const format = (value, line) => `${formatValue ? formatValue(value, line) : GROUPED.format(value)}${unit ? ` ${unit}` : ""}`;
538
+ const formatShort = (value, line) => formatValue ? formatValue(value, line) : GROUPED.format(value);
539
+ const formatNumber = (value, line) => `${formatShort(value, line)}${unit ? ` ${unit}` : ""}`;
540
+ const format = (value, line) => value === null ? missingLabel : formatNumber(value, line);
541
+ const formatReference = (value) => series[0] ? formatNumber(value, series[0]) : `${GROUPED.format(value)}${unit ? ` ${unit}` : ""}`;
379
542
  const reading = index === null ? null : data[index];
380
543
  const shown = reading ?? data[last];
381
- const valueText = reading ? `${reading.label}: ${visible.map((line) => `${line.label} ${format(reading.values[line.key] ?? 0, line)}`).join(", ")}` : empty ? emptyLabel : `${visible.length} of ${series.length} series shown`;
382
- const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${data[0].label} to ${data[last].label}. ${visible.map((line) => `${line.label}, latest ${format(data[last].values[line.key] ?? 0, line)}`).join(". ")}.`;
544
+ const markedHere = index === null ? [] : drawnMarkers.filter((marker) => markerIndex(marker) === index);
545
+ const valueText = reading ? `${reading.label}: ${visible.map((line) => `${line.label} ${format(read(reading, line.key), line)}`).join(", ")}${markedHere.map((marker) => `. ${marker.description ?? marker.label}`).join("")}` : empty ? emptyLabel : `${visible.length} of ${series.length} series shown`;
546
+ const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${data[0].label} to ${data[last].label}. ${visible.map((line) => `${line.label}, latest ${format(read(data[last], line.key), line)}${typeof line.endLabel === "string" ? `, ${line.endLabel}` : ""}`).join(". ")}.${drawnReferences.map((reference) => typeof reference.value === "number" ? ` ${reference.label}: ${formatReference(reference.value)}.` : ` ${reference.label}: ${data.find((item) => item.key === reference.at).label}.`).join("")}${drawnMarkers.map((marker) => ` ${marker.description ?? marker.label}: ${data[markerIndex(marker)].label}.`).join("")}${drawnBands.map((band) => ` ${band.description ?? `${band.label}: ${formatReference(Math.min(band.from, band.to))} to ${formatReference(Math.max(band.from, band.to))}`}.`).join("")}`;
547
+ const pointLabels = empty ? [] : series.flatMap((line) => {
548
+ if (!line.pointLabels && !line.endLabel) return [];
549
+ const present = data.flatMap((item, at) => {
550
+ const value = read(item, line.key);
551
+ return value === null ? [] : [{
552
+ at,
553
+ value
554
+ }];
555
+ });
556
+ const end = line.endLabel ? present.at(-1) : void 0;
557
+ return [...(line.pointLabels ? present.filter((point) => point !== end) : []).map((point) => ({
558
+ line,
559
+ ...point,
560
+ end: false,
561
+ text: formatShort(point.value, line)
562
+ })), ...end ? [{
563
+ line,
564
+ ...end,
565
+ end: true,
566
+ text: typeof line.endLabel === "string" ? line.endLabel : formatShort(end.value, line)
567
+ }] : []];
568
+ });
569
+ const readout = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
570
+ className: "m-0 mb-0.5 whitespace-nowrap text-xs tabular-nums text-text-secondary",
571
+ children: shown?.label ?? ""
572
+ }), visible.map((line) => /* @__PURE__ */ jsxs("p", {
573
+ className: "m-0 flex min-w-0 items-center gap-2 text-sm",
574
+ style: { "--series": colorOf(line, series.indexOf(line)) },
575
+ children: [
576
+ /* @__PURE__ */ jsx("span", {
577
+ "data-dashed": line.dashed || void 0,
578
+ className: "chart-tip-swatch"
579
+ }),
580
+ /* @__PURE__ */ jsx("span", {
581
+ className: "min-w-0 flex-1 truncate text-text-secondary",
582
+ children: line.label
583
+ }),
584
+ /* @__PURE__ */ jsx("span", {
585
+ className: "ms-2 flex-none whitespace-nowrap font-medium tabular-nums text-foreground",
586
+ children: shown ? format(read(shown, line.key), line) : format(0, line)
587
+ })
588
+ ]
589
+ }, line.key))] });
590
+ const tipContent = renderTooltip && shown ? renderTooltip({
591
+ datum: shown,
592
+ index: index ?? last,
593
+ series: visible,
594
+ values: Object.fromEntries(visible.map((line) => [line.key, read(shown, line.key)])),
595
+ format,
596
+ readout
597
+ }) : readout;
383
598
  const picks = axisPicks(data, plotWidth);
384
599
  const allHidden = !empty && visible.length === 0;
385
600
  return /* @__PURE__ */ jsxs("figure", {
@@ -464,7 +679,16 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
464
679
  style: { width: drawn ? plotWidth + 16 : 0 }
465
680
  })
466
681
  }) }),
467
- steady.ticks.slice(1).map((value) => /* @__PURE__ */ jsx("line", {
682
+ drawnBands.map((band) => /* @__PURE__ */ jsx("rect", {
683
+ "data-band": band.key,
684
+ "data-from": band.from,
685
+ "data-to": band.to,
686
+ "data-tone": band.tone,
687
+ className: "line-chart-band",
688
+ x: 0,
689
+ width: "100%"
690
+ }, band.key)),
691
+ (reversed ? steady.ticks.slice(0, -1) : steady.ticks.slice(1)).map((value) => /* @__PURE__ */ jsx("line", {
468
692
  "data-grid": value,
469
693
  className: "line-chart-grid",
470
694
  x1: 0,
@@ -477,6 +701,21 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
477
701
  y1: height - .5,
478
702
  y2: height - .5
479
703
  }),
704
+ drawnReferences.map((reference) => typeof reference.value === "number" ? /* @__PURE__ */ jsx("line", {
705
+ "data-reference": reference.key,
706
+ "data-reference-value": reference.value,
707
+ "data-tone": reference.tone,
708
+ className: "line-chart-reference",
709
+ x1: 0,
710
+ x2: "100%"
711
+ }, reference.key) : /* @__PURE__ */ jsx("line", {
712
+ "data-reference": reference.key,
713
+ "data-reference-at": referenceAt(reference),
714
+ "data-tone": reference.tone,
715
+ className: "line-chart-reference",
716
+ y1: 8,
717
+ y2: height
718
+ }, reference.key)),
480
719
  /* @__PURE__ */ jsx("line", {
481
720
  "data-part": "crosshair",
482
721
  className: "line-chart-crosshair",
@@ -508,32 +747,61 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
508
747
  }, line.key))
509
748
  ]
510
749
  }),
511
- /* @__PURE__ */ jsxs("div", {
750
+ drawnBands.map((band) => /* @__PURE__ */ jsx("span", {
751
+ "aria-hidden": "true",
752
+ "data-slot": "line-chart-band-label",
753
+ "data-band-label": band.key,
754
+ "data-tone": band.tone,
755
+ className: "line-chart-band-label",
756
+ children: band.label
757
+ }, band.key)),
758
+ drawnReferences.map((reference) => /* @__PURE__ */ jsx("span", {
759
+ "aria-hidden": "true",
760
+ "data-slot": "line-chart-reference-label",
761
+ "data-reference-label": reference.key,
762
+ "data-reference-value": typeof reference.value === "number" ? reference.value : void 0,
763
+ "data-reference-at": typeof reference.value === "number" ? void 0 : referenceAt(reference),
764
+ "data-axis": typeof reference.value === "number" ? "value" : "category",
765
+ "data-tone": reference.tone,
766
+ className: "line-chart-reference-label",
767
+ children: reference.label
768
+ }, reference.key)),
769
+ drawnMarkers.map((marker) => {
770
+ const share = last > 0 ? markerIndex(marker) / last : .5;
771
+ return /* @__PURE__ */ jsx("span", {
772
+ "aria-hidden": "true",
773
+ "data-slot": "line-chart-marker",
774
+ "data-marker": marker.key,
775
+ "data-tone": marker.tone,
776
+ className: "line-chart-marker",
777
+ style: { left: `${share * 100}%` },
778
+ children: /* @__PURE__ */ jsx("span", {
779
+ className: "line-chart-marker-label",
780
+ style: { translate: `${-share * 100}% 0` },
781
+ children: marker.label
782
+ })
783
+ }, marker.key);
784
+ }),
785
+ pointLabels.map(({ line, at, end, text }) => /* @__PURE__ */ jsxs("span", {
786
+ "aria-hidden": "true",
787
+ "data-slot": end ? "line-chart-end-label" : "line-chart-point-label",
788
+ "data-point-label": line.key,
789
+ "data-x": last > 0 ? at / last : .5,
790
+ className: "line-chart-point-label",
791
+ "data-end": end || void 0,
792
+ style: { "--series": colorOf(line, series.indexOf(line)) },
793
+ children: [end ? /* @__PURE__ */ jsx("span", {
794
+ "data-dashed": line.dashed || void 0,
795
+ className: "chart-tip-swatch"
796
+ }) : null, text]
797
+ }, `${line.key}:${data[at].key}`)),
798
+ /* @__PURE__ */ jsx("div", {
512
799
  ref: tip,
513
800
  "aria-hidden": "true",
514
801
  "data-slot": "line-chart-tooltip",
802
+ "data-empty": !tipContent || void 0,
515
803
  className: "line-chart-tooltip",
516
- children: [/* @__PURE__ */ jsx("p", {
517
- className: "m-0 mb-0.5 whitespace-nowrap text-xs tabular-nums text-text-secondary",
518
- children: shown?.label ?? ""
519
- }), visible.map((line) => /* @__PURE__ */ jsxs("p", {
520
- className: "m-0 flex min-w-0 items-center gap-2 text-sm",
521
- style: { "--series": colorOf(line, series.indexOf(line)) },
522
- children: [
523
- /* @__PURE__ */ jsx("span", {
524
- "data-dashed": line.dashed || void 0,
525
- className: "chart-tip-swatch"
526
- }),
527
- /* @__PURE__ */ jsx("span", {
528
- className: "min-w-0 flex-1 truncate text-text-secondary",
529
- children: line.label
530
- }),
531
- /* @__PURE__ */ jsx("span", {
532
- className: "ms-2 flex-none whitespace-nowrap font-medium tabular-nums text-foreground",
533
- children: format(shown?.values[line.key] ?? 0, line)
534
- })
535
- ]
536
- }, line.key))]
804
+ children: tipContent
537
805
  }),
538
806
  (empty || allHidden) && !loading ? /* @__PURE__ */ jsx("p", {
539
807
  className: "absolute inset-0 m-0 grid place-items-center text-sm text-muted-foreground pointer-events-none motion-safe:animate-fade-in",
@@ -593,7 +861,7 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
593
861
  /* @__PURE__ */ jsx("tbody", { children: data.map((item) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
594
862
  scope: "row",
595
863
  children: item.label
596
- }), series.map((line) => /* @__PURE__ */ jsx("td", { children: format(item.values[line.key] ?? 0, line) }, line.key))] }, item.key)) })
864
+ }), series.map((line) => /* @__PURE__ */ jsx("td", { children: format(read(item, line.key), line) }, line.key))] }, item.key)) })
597
865
  ] })
598
866
  }),
599
867
  /* @__PURE__ */ jsx("p", {