@seekalfred/primitives 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -4
- package/dist/alert/alert.d.ts +49 -4
- package/dist/alert/alert.js +66 -38
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert/index.d.ts +2 -2
- package/dist/alert/index.js +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +17 -4
- package/dist/alert-dialog/alert-dialog.js +26 -2
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/badge/badge.d.ts +1 -1
- package/dist/button/button.d.ts +2 -2
- package/dist/calendar/calendar.d.ts +1 -1
- package/dist/card/card.d.ts +22 -1
- package/dist/card/card.js +60 -7
- package/dist/card/card.js.map +1 -1
- package/dist/carousel/carousel.d.ts +21 -0
- package/dist/carousel/carousel.js +34 -8
- package/dist/carousel/carousel.js.map +1 -1
- package/dist/copy-button/copy-button.d.ts +18 -2
- package/dist/copy-button/copy-button.js +29 -8
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +22 -0
- package/dist/donut-chart/donut-chart.js +35 -18
- package/dist/donut-chart/donut-chart.js.map +1 -1
- package/dist/donut-chart/index.d.ts +2 -2
- package/dist/empty-state/empty-state.d.ts +21 -0
- package/dist/empty-state/empty-state.js +16 -4
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/filter-bar/filter-bar.d.ts +4 -1
- package/dist/filter-bar/filter-bar.js +53 -9
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +7 -7
- package/dist/input/input.js +4 -4
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +10 -3
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/field-surface.js +22 -1
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/use-copy-feedback.js +17 -3
- package/dist/lib/use-copy-feedback.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +42 -2
- package/dist/line-chart/line-chart.js +143 -14
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/money-input/money-input.d.ts +22 -1
- package/dist/money-input/money-input.js +13 -7
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +19 -1
- package/dist/morph-select/morph-select.js +21 -2
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/index.d.ts +2 -2
- package/dist/multi-select/index.js +2 -2
- package/dist/multi-select/multi-select.d.ts +43 -2
- package/dist/multi-select/multi-select.js +93 -36
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/password-field/password-field.d.ts +16 -0
- package/dist/password-field/password-field.js +37 -8
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/popover/index.d.ts +2 -2
- package/dist/popover/index.js +2 -2
- package/dist/popover/popover.d.ts +39 -2
- package/dist/popover/popover.js +29 -2
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/progress.d.ts +16 -2
- package/dist/progress/progress.js +15 -8
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/index.d.ts +2 -2
- package/dist/radio-group/index.js +2 -2
- package/dist/radio-group/radio-group.d.ts +33 -3
- package/dist/radio-group/radio-group.js +109 -9
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.js +4 -3
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/search-field/search-field.d.ts +22 -1
- package/dist/search-field/search-field.js +22 -7
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +1 -1
- package/dist/select/select.js +8 -6
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +21 -9
- package/dist/separator/separator.js +33 -11
- package/dist/separator/separator.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +34 -1
- package/dist/stat-card/stat-card.js +51 -21
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/table.d.ts +1 -1
- package/dist/time-picker/time-picker.d.ts +36 -1
- package/dist/time-picker/time-picker.js +33 -5
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/usage-meter/index.d.ts +2 -2
- package/dist/usage-meter/usage-meter.d.ts +30 -0
- package/dist/usage-meter/usage-meter.js +56 -38
- package/dist/usage-meter/usage-meter.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
//#region src/lib/use-copy-feedback.ts
|
|
3
|
+
/**
|
|
4
|
+
* Runs a consumer's callback so that a throw inside it cannot reject the
|
|
5
|
+
* promise our press handler fires and forgets (an unhandled rejection). The
|
|
6
|
+
* error is reported the way an uncaught one would be, so it still shows up.
|
|
7
|
+
*/
|
|
8
|
+
function callSafely(callback) {
|
|
9
|
+
try {
|
|
10
|
+
callback();
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
13
|
+
else console.error(error);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
3
16
|
/** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */
|
|
4
17
|
function useCopyFeedback(duration = 1900) {
|
|
5
18
|
const [state, setState] = useState("idle");
|
|
@@ -14,16 +27,17 @@ function useCopyFeedback(duration = 1900) {
|
|
|
14
27
|
}, []);
|
|
15
28
|
return {
|
|
16
29
|
state,
|
|
17
|
-
copy: useCallback(async (value) => {
|
|
30
|
+
copy: useCallback(async (value, onError) => {
|
|
18
31
|
if (timeout.current) clearTimeout(timeout.current);
|
|
19
32
|
try {
|
|
20
33
|
await navigator.clipboard.writeText(value);
|
|
21
34
|
setState("copied");
|
|
22
35
|
timeout.current = setTimeout(reset, duration);
|
|
23
36
|
return true;
|
|
24
|
-
} catch {
|
|
37
|
+
} catch (error) {
|
|
25
38
|
setState("error");
|
|
26
39
|
timeout.current = setTimeout(reset, duration);
|
|
40
|
+
callSafely(() => onError?.(error));
|
|
27
41
|
return false;
|
|
28
42
|
}
|
|
29
43
|
}, [duration, reset]),
|
|
@@ -31,6 +45,6 @@ function useCopyFeedback(duration = 1900) {
|
|
|
31
45
|
};
|
|
32
46
|
}
|
|
33
47
|
//#endregion
|
|
34
|
-
export { useCopyFeedback };
|
|
48
|
+
export { callSafely, useCopyFeedback };
|
|
35
49
|
|
|
36
50
|
//# sourceMappingURL=use-copy-feedback.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-copy-feedback.js","names":[],"sources":["../../src/lib/use-copy-feedback.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type CopyFeedbackState = \"idle\" | \"copied\" | \"error\";\n\n/** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */\nexport function useCopyFeedback(duration = 1900) {\n const [state, setState] = useState<CopyFeedbackState>(\"idle\");\n const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const reset = useCallback(() => {\n if (timeout.current) clearTimeout(timeout.current);\n timeout.current = null;\n setState(\"idle\");\n }, []);\n\n useEffect(\n () => () => {\n if (timeout.current) clearTimeout(timeout.current);\n },\n [],\n );\n\n /** Resolves true if the text reached the clipboard. */\n const copy = useCallback(\n async (value: string) => {\n if (timeout.current) clearTimeout(timeout.current);\n try {\n await navigator.clipboard.writeText(value);\n setState(\"copied\");\n timeout.current = setTimeout(reset, duration);\n return true;\n } catch {\n setState(\"error\");\n timeout.current = setTimeout(reset, duration);\n return false;\n }\n },\n [duration, reset],\n );\n\n return { state, copy, reset };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-copy-feedback.js","names":[],"sources":["../../src/lib/use-copy-feedback.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type CopyFeedbackState = \"idle\" | \"copied\" | \"error\";\n\n/**\n * Runs a consumer's callback so that a throw inside it cannot reject the\n * promise our press handler fires and forgets (an unhandled rejection). The\n * error is reported the way an uncaught one would be, so it still shows up.\n */\nexport function callSafely(callback: () => void) {\n try {\n callback();\n } catch (error) {\n if (typeof globalThis.reportError === \"function\") globalThis.reportError(error);\n else console.error(error);\n }\n}\n\n/** Clipboard state for actions that render their own button: copy, show copied or failed for a while, then go back to idle. */\nexport function useCopyFeedback(duration = 1900) {\n const [state, setState] = useState<CopyFeedbackState>(\"idle\");\n const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const reset = useCallback(() => {\n if (timeout.current) clearTimeout(timeout.current);\n timeout.current = null;\n setState(\"idle\");\n }, []);\n\n useEffect(\n () => () => {\n if (timeout.current) clearTimeout(timeout.current);\n },\n [],\n );\n\n /** Resolves true if the text reached the clipboard. `onError` gets whatever the clipboard rejected with. */\n const copy = useCallback(\n async (value: string, onError?: (error: unknown) => void) => {\n if (timeout.current) clearTimeout(timeout.current);\n try {\n await navigator.clipboard.writeText(value);\n setState(\"copied\");\n timeout.current = setTimeout(reset, duration);\n return true;\n } catch (error) {\n setState(\"error\");\n timeout.current = setTimeout(reset, duration);\n callSafely(() => onError?.(error));\n return false;\n }\n },\n [duration, reset],\n );\n\n return { state, copy, reset };\n}\n"],"mappings":";;;;;;;AASA,SAAgB,WAAW,UAAsB;CAC/C,IAAI;EACF,SAAS;CACX,SAAS,OAAO;EACd,IAAI,OAAO,WAAW,gBAAgB,YAAY,WAAW,YAAY,KAAK;OACzE,QAAQ,MAAM,KAAK;CAC1B;AACF;;AAGA,SAAgB,gBAAgB,WAAW,MAAM;CAC/C,MAAM,CAAC,OAAO,YAAY,SAA4B,MAAM;CAC5D,MAAM,UAAU,OAA6C,IAAI;CAEjE,MAAM,QAAQ,kBAAkB;EAC9B,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;EACjD,QAAQ,UAAU;EAClB,SAAS,MAAM;CACjB,GAAG,CAAC,CAAC;CAEL,sBACc;EACV,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;CACnD,GACA,CAAC,CACH;CAqBA,OAAO;EAAE;EAAO,MAlBH,YACX,OAAO,OAAe,YAAuC;GAC3D,IAAI,QAAQ,SAAS,aAAa,QAAQ,OAAO;GACjD,IAAI;IACF,MAAM,UAAU,UAAU,UAAU,KAAK;IACzC,SAAS,QAAQ;IACjB,QAAQ,UAAU,WAAW,OAAO,QAAQ;IAC5C,OAAO;GACT,SAAS,OAAO;IACd,SAAS,OAAO;IAChB,QAAQ,UAAU,WAAW,OAAO,QAAQ;IAC5C,iBAAiB,UAAU,KAAK,CAAC;IACjC,OAAO;GACT;EACF,GACA,CAAC,UAAU,KAAK,CAGC;EAAG;CAAM;AAC9B"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { LINE_CHART_DRAW_MS, LineChart, LineChartDatum, LineChartProps, LineChartSeries } from "./line-chart.js";
|
|
2
|
-
export { LINE_CHART_DRAW_MS, LineChart, type LineChartDatum, type LineChartProps, type LineChartSeries };
|
|
1
|
+
import { LINE_CHART_DRAW_MS, LineChart, LineChartDatum, LineChartProps, LineChartReference, LineChartSeries } from "./line-chart.js";
|
|
2
|
+
export { LINE_CHART_DRAW_MS, LineChart, type LineChartDatum, type LineChartProps, type LineChartReference, type LineChartSeries };
|
|
@@ -34,9 +34,36 @@ export interface LineChartDatum {
|
|
|
34
34
|
label: string;
|
|
35
35
|
/** Short label on the x axis. Leave it out to keep the axis quiet at that point. */
|
|
36
36
|
axisLabel?: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* One value per series key. A missing value (`undefined` or `null`) counts as
|
|
39
|
+
* zero, unless the chart's `missing` is `"gap"`.
|
|
40
|
+
*/
|
|
41
|
+
values: Record<string, number | null | undefined>;
|
|
39
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* A reference line: a horizontal line at a value (a target, a threshold), or a
|
|
45
|
+
* vertical line at one datum (today, a launch). Drawn dashed and quiet, behind
|
|
46
|
+
* the series, with its label at the line's end.
|
|
47
|
+
*/
|
|
48
|
+
export type LineChartReference = {
|
|
49
|
+
/** Stable identity. */
|
|
50
|
+
key: string;
|
|
51
|
+
/** Short text beside the line, such as "Target" or "Today". Also read to screen readers. */
|
|
52
|
+
label: string;
|
|
53
|
+
} & ({
|
|
54
|
+
/**
|
|
55
|
+
* A value on the value axis: a horizontal line. The scale grows to keep it
|
|
56
|
+
* in view, even when every value is missing. A value that is not finite
|
|
57
|
+
* draws nothing. Read with `formatValue` (given the chart's first series)
|
|
58
|
+
* and `unit`, like the series.
|
|
59
|
+
*/
|
|
60
|
+
value: number;
|
|
61
|
+
at?: never;
|
|
62
|
+
} | {
|
|
63
|
+
/** The `key` of a datum: a vertical line at that point. A key not in `data` draws nothing. */
|
|
64
|
+
at: string;
|
|
65
|
+
value?: never;
|
|
66
|
+
});
|
|
40
67
|
export interface LineChartProps {
|
|
41
68
|
data: LineChartDatum[];
|
|
42
69
|
series: LineChartSeries[];
|
|
@@ -66,6 +93,19 @@ export interface LineChartProps {
|
|
|
66
93
|
categoryLabel?: string;
|
|
67
94
|
/** Smooth monotone curves that never swing past the data, or straight segments. @default "smooth" */
|
|
68
95
|
curve?: "smooth" | "linear";
|
|
96
|
+
/**
|
|
97
|
+
* How a missing value (`undefined`, `null` or not a finite number) is drawn.
|
|
98
|
+
* `"zero"` counts it as zero, as before. `"gap"` breaks the line (and its
|
|
99
|
+
* area) there, leaves it out of the scale, hides the crosshair dot on it and
|
|
100
|
+
* reads it as `missingLabel` in the tooltip and the data table. A point with
|
|
101
|
+
* missing neighbours on both sides is drawn as a dot.
|
|
102
|
+
* @default "zero"
|
|
103
|
+
*/
|
|
104
|
+
missing?: "zero" | "gap";
|
|
105
|
+
/** Read in place of a missing value when `missing` is `"gap"`. @default "No data" */
|
|
106
|
+
missingLabel?: string;
|
|
107
|
+
/** Reference lines: targets at a value, or markers such as "today" at a datum. */
|
|
108
|
+
references?: LineChartReference[];
|
|
69
109
|
/** Extra classes for the root. Merged last. */
|
|
70
110
|
className?: string;
|
|
71
111
|
}
|
|
@@ -88,6 +88,61 @@ function curveFor(values, smooth) {
|
|
|
88
88
|
points.push([1, values[last]]);
|
|
89
89
|
return points;
|
|
90
90
|
}
|
|
91
|
+
/** 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. */
|
|
92
|
+
function fillGaps(values) {
|
|
93
|
+
const present = values.flatMap((value, index) => value === null ? [] : [index]);
|
|
94
|
+
if (!present.length) return values.map(() => 0);
|
|
95
|
+
return values.map((value, index) => {
|
|
96
|
+
if (value !== null) return value;
|
|
97
|
+
const after = present.find((at) => at > index);
|
|
98
|
+
const before = [...present].reverse().find((at) => at < index);
|
|
99
|
+
if (before === void 0) return values[after];
|
|
100
|
+
if (after === void 0) return values[before];
|
|
101
|
+
return values[before] + (values[after] - values[before]) * (index - before) / (after - before);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Where a line with missing values is not drawn: before its first value, after its last, and between two values with missing ones between them. */
|
|
105
|
+
function breaksFor(values) {
|
|
106
|
+
const present = values.flatMap((value, index) => value === null ? [] : [index]);
|
|
107
|
+
if (!present.length) return values.length ? [[-Infinity, Infinity]] : [];
|
|
108
|
+
const last = values.length - 1;
|
|
109
|
+
if (last === 0) return [];
|
|
110
|
+
const breaks = [];
|
|
111
|
+
if (present[0] > 0) breaks.push([-Infinity, present[0] / last]);
|
|
112
|
+
for (let at = 1; at < present.length; at++) if (present[at] - present[at - 1] > 1) breaks.push([present[at - 1] / last, present[at] / last]);
|
|
113
|
+
if (present[present.length - 1] < last) breaks.push([present[present.length - 1] / last, Infinity]);
|
|
114
|
+
return breaks;
|
|
115
|
+
}
|
|
116
|
+
const EDGE = 1e-9;
|
|
117
|
+
/** Radius of a lone point's dot, inside its 2px stroke. */
|
|
118
|
+
const DOT = 1.5;
|
|
119
|
+
const inBreak = (breaks, x) => breaks.some(([from, to]) => x > from + EDGE && x < to - EDGE);
|
|
120
|
+
/** 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. */
|
|
121
|
+
function pathsFor(shape, breaks, toX, toY, bottom) {
|
|
122
|
+
const runs = [];
|
|
123
|
+
let run = null;
|
|
124
|
+
for (const [x, value] of shape) {
|
|
125
|
+
if (breaks.length && inBreak(breaks, x)) {
|
|
126
|
+
run = null;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!run) runs.push(run = []);
|
|
130
|
+
run.push(`${toX(x).toFixed(1)},${toY(value).toFixed(1)}`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
line: runs.map((points) => {
|
|
134
|
+
if (points.length > 1) return `M${points.join("L")}`;
|
|
135
|
+
const [x, y] = points[0].split(",").map(Number);
|
|
136
|
+
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`;
|
|
137
|
+
}).join(""),
|
|
138
|
+
area: runs.map((points) => {
|
|
139
|
+
const first = points[0].split(",")[0];
|
|
140
|
+
const lastX = points[points.length - 1].split(",")[0];
|
|
141
|
+
return `M${points.join("L")}L${lastX},${bottom}L${first},${bottom}Z`;
|
|
142
|
+
}).join("")
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const isFiniteValue = (reference) => typeof reference.value === "number" && Number.isFinite(reference.value);
|
|
91
146
|
/** Value of a sampled line at x, straight between samples. */
|
|
92
147
|
function valueAt(shape, x) {
|
|
93
148
|
if (!shape.length) return 0;
|
|
@@ -112,7 +167,7 @@ function axisPicks(data, width) {
|
|
|
112
167
|
const stride = Math.ceil(labeled.length / room);
|
|
113
168
|
return labeled.reverse().filter((_, rank) => rank % stride === 0).reverse();
|
|
114
169
|
}
|
|
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) {
|
|
170
|
+
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, className }, ref) {
|
|
116
171
|
const figure = useRef(null);
|
|
117
172
|
const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
|
|
118
173
|
const plot = useRef(null);
|
|
@@ -131,15 +186,33 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
131
186
|
};
|
|
132
187
|
const visible = series.filter((line) => !hidden.includes(line.key));
|
|
133
188
|
const showLegend = legend ?? series.length > 1;
|
|
134
|
-
const
|
|
135
|
-
const
|
|
189
|
+
const gaps = missing === "gap";
|
|
190
|
+
const read = (item, key) => {
|
|
191
|
+
const value = item.values[key];
|
|
192
|
+
if (!gaps) return value ?? 0;
|
|
193
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
194
|
+
};
|
|
195
|
+
const signature = `${curve}|${missing}|${series.map((line) => line.key).join(",")}|${data.map((item) => `${item.key}:${series.map((line) => read(item, line.key)).join(",")}`).join(";")}`;
|
|
196
|
+
const targets = useMemo(() => new Map(series.map((line) => {
|
|
197
|
+
const values = data.map((item) => read(item, line.key));
|
|
198
|
+
return [line.key, {
|
|
199
|
+
shape: curveFor(fillGaps(values), curve === "smooth"),
|
|
200
|
+
breaks: breaksFor(values)
|
|
201
|
+
}];
|
|
202
|
+
})), [signature]);
|
|
136
203
|
let low = Infinity;
|
|
137
204
|
let high = -Infinity;
|
|
138
205
|
for (const line of visible) for (const item of data) {
|
|
139
|
-
const value = item
|
|
206
|
+
const value = read(item, line.key);
|
|
207
|
+
if (value === null) continue;
|
|
140
208
|
low = Math.min(low, value);
|
|
141
209
|
high = Math.max(high, value);
|
|
142
210
|
}
|
|
211
|
+
for (const reference of references ?? []) {
|
|
212
|
+
if (!isFiniteValue(reference)) continue;
|
|
213
|
+
low = Math.min(low, reference.value);
|
|
214
|
+
high = Math.max(high, reference.value);
|
|
215
|
+
}
|
|
143
216
|
const range = Number.isFinite(low) ? niceScale(low, high) : null;
|
|
144
217
|
const [steady, setSteady] = useState(() => range ?? niceScale(0, 100));
|
|
145
218
|
if (range && (range.min !== steady.min || range.max !== steady.max)) setSteady(range);
|
|
@@ -160,6 +233,12 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
160
233
|
max: steady.max
|
|
161
234
|
});
|
|
162
235
|
const cursor = useRef(1);
|
|
236
|
+
const referenceAt = (reference) => {
|
|
237
|
+
if (typeof reference.value === "number") return isFiniteValue(reference) ? null : void 0;
|
|
238
|
+
const at = data.findIndex((item) => item.key === reference.at);
|
|
239
|
+
return at < 0 ? void 0 : last > 0 ? at / last : .5;
|
|
240
|
+
};
|
|
241
|
+
const drawnReferences = (references ?? []).filter((reference) => referenceAt(reference) !== void 0);
|
|
163
242
|
const live = useRef({
|
|
164
243
|
series,
|
|
165
244
|
hidden
|
|
@@ -185,17 +264,26 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
185
264
|
if (!track) continue;
|
|
186
265
|
const p = track.presence;
|
|
187
266
|
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")}` : "";
|
|
189
267
|
const group = root.querySelector(`[data-series="${CSS.escape(line.key)}"]`);
|
|
190
|
-
|
|
191
|
-
|
|
268
|
+
if (track.breaks.length) {
|
|
269
|
+
const paths = pathsFor(track.shape, track.breaks, (x) => x * w, y, height);
|
|
270
|
+
group?.querySelector("[data-part=line]")?.setAttribute("d", paths.line);
|
|
271
|
+
group?.querySelector("[data-part=area]")?.setAttribute("d", paths.area);
|
|
272
|
+
} else {
|
|
273
|
+
const path = track.shape.length ? `M${track.shape.map(([x, value]) => `${(x * w).toFixed(1)},${y(value).toFixed(1)}`).join("L")}` : "";
|
|
274
|
+
group?.querySelector("[data-part=line]")?.setAttribute("d", path);
|
|
275
|
+
group?.querySelector("[data-part=area]")?.setAttribute("d", path ? `${path}L${w},${height}L0,${height}Z` : "");
|
|
276
|
+
}
|
|
192
277
|
if (group) group.style.opacity = String(p);
|
|
193
278
|
const dot = root.querySelector(`[data-dot="${CSS.escape(line.key)}"]`);
|
|
194
279
|
if (dot && track.shape.length) {
|
|
280
|
+
const gap = inBreak(track.breaks, at);
|
|
195
281
|
const cy = y(valueAt(track.shape, at));
|
|
196
282
|
dot.setAttribute("cx", (at * w).toFixed(1));
|
|
197
283
|
dot.setAttribute("cy", cy.toFixed(1));
|
|
198
|
-
if (
|
|
284
|
+
if (gap) dot.setAttribute("data-gap", "");
|
|
285
|
+
else dot.removeAttribute("data-gap");
|
|
286
|
+
if (p > .5 && !gap) dotYs.push(cy);
|
|
199
287
|
}
|
|
200
288
|
}
|
|
201
289
|
for (const node of root.querySelectorAll("[data-grid]")) {
|
|
@@ -204,6 +292,20 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
204
292
|
node.setAttribute("y2", yy);
|
|
205
293
|
}
|
|
206
294
|
for (const node of gutter.current?.querySelectorAll("[data-tick]") ?? []) node.style.translate = `0 ${Math.round(toY(Number(node.dataset.tick)))}px`;
|
|
295
|
+
for (const node of root.querySelectorAll("[data-reference-value]")) {
|
|
296
|
+
const yy = String(Math.round(toY(Number(node.dataset.referenceValue))) + .5);
|
|
297
|
+
node.setAttribute("y1", yy);
|
|
298
|
+
node.setAttribute("y2", yy);
|
|
299
|
+
}
|
|
300
|
+
for (const node of root.querySelectorAll("[data-reference-at]")) {
|
|
301
|
+
const xx = String(Math.round(Number(node.dataset.referenceAt) * w) + .5);
|
|
302
|
+
node.setAttribute("x1", xx);
|
|
303
|
+
node.setAttribute("x2", xx);
|
|
304
|
+
}
|
|
305
|
+
for (const node of plot.current?.querySelectorAll("[data-reference-label]") ?? []) {
|
|
306
|
+
const { referenceValue, referenceAt } = node.dataset;
|
|
307
|
+
node.style.translate = referenceValue !== void 0 ? `0 ${Math.round(toY(Number(referenceValue)))}px` : `${Math.round(Number(referenceAt) * w)}px 0`;
|
|
308
|
+
}
|
|
207
309
|
const cx = Math.round(at * w) + .5;
|
|
208
310
|
const cross = root.querySelector("[data-part=crosshair]");
|
|
209
311
|
cross?.setAttribute("x1", String(cx));
|
|
@@ -235,16 +337,18 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
235
337
|
useIsomorphicLayoutEffect(paint);
|
|
236
338
|
useEffect(() => {
|
|
237
339
|
const moves = [];
|
|
238
|
-
for (const [key, target] of targets) {
|
|
340
|
+
for (const [key, { shape: target, breaks }] of targets) {
|
|
239
341
|
const track = tracks.current.get(key);
|
|
240
342
|
if (!track) {
|
|
241
343
|
tracks.current.set(key, {
|
|
242
344
|
shape: target,
|
|
345
|
+
breaks,
|
|
243
346
|
presence: live.current.hidden.includes(key) ? 0 : 1,
|
|
244
347
|
stop: null
|
|
245
348
|
});
|
|
246
349
|
continue;
|
|
247
350
|
}
|
|
351
|
+
track.breaks = breaks;
|
|
248
352
|
if (!track.shape.length || !target.length) {
|
|
249
353
|
track.shape = target;
|
|
250
354
|
continue;
|
|
@@ -375,11 +479,13 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
375
479
|
const onPointerMove = (event) => {
|
|
376
480
|
if (!empty && (event.pointerType === "mouse" || event.buttons)) setActive(pointAt(event.clientX));
|
|
377
481
|
};
|
|
378
|
-
const
|
|
482
|
+
const formatNumber = (value, line) => `${formatValue ? formatValue(value, line) : GROUPED.format(value)}${unit ? ` ${unit}` : ""}`;
|
|
483
|
+
const format = (value, line) => value === null ? missingLabel : formatNumber(value, line);
|
|
484
|
+
const formatReference = (value) => series[0] ? formatNumber(value, series[0]) : `${GROUPED.format(value)}${unit ? ` ${unit}` : ""}`;
|
|
379
485
|
const reading = index === null ? null : data[index];
|
|
380
486
|
const shown = reading ?? data[last];
|
|
381
|
-
const valueText = reading ? `${reading.label}: ${visible.map((line) => `${line.label} ${format(reading
|
|
382
|
-
const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${data[0].label} to ${data[last].label}. ${visible.map((line) => `${line.label}, latest ${format(data[last]
|
|
487
|
+
const valueText = reading ? `${reading.label}: ${visible.map((line) => `${line.label} ${format(read(reading, line.key), line)}`).join(", ")}` : empty ? emptyLabel : `${visible.length} of ${series.length} series shown`;
|
|
488
|
+
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)}`).join(". ")}.${drawnReferences.map((reference) => typeof reference.value === "number" ? ` ${reference.label}: ${formatReference(reference.value)}.` : ` ${reference.label}: ${data.find((item) => item.key === reference.at).label}.`).join("")}`;
|
|
383
489
|
const picks = axisPicks(data, plotWidth);
|
|
384
490
|
const allHidden = !empty && visible.length === 0;
|
|
385
491
|
return /* @__PURE__ */ jsxs("figure", {
|
|
@@ -477,6 +583,19 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
477
583
|
y1: height - .5,
|
|
478
584
|
y2: height - .5
|
|
479
585
|
}),
|
|
586
|
+
drawnReferences.map((reference) => typeof reference.value === "number" ? /* @__PURE__ */ jsx("line", {
|
|
587
|
+
"data-reference": reference.key,
|
|
588
|
+
"data-reference-value": reference.value,
|
|
589
|
+
className: "line-chart-reference",
|
|
590
|
+
x1: 0,
|
|
591
|
+
x2: "100%"
|
|
592
|
+
}, reference.key) : /* @__PURE__ */ jsx("line", {
|
|
593
|
+
"data-reference": reference.key,
|
|
594
|
+
"data-reference-at": referenceAt(reference),
|
|
595
|
+
className: "line-chart-reference",
|
|
596
|
+
y1: 8,
|
|
597
|
+
y2: height
|
|
598
|
+
}, reference.key)),
|
|
480
599
|
/* @__PURE__ */ jsx("line", {
|
|
481
600
|
"data-part": "crosshair",
|
|
482
601
|
className: "line-chart-crosshair",
|
|
@@ -508,6 +627,16 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
508
627
|
}, line.key))
|
|
509
628
|
]
|
|
510
629
|
}),
|
|
630
|
+
drawnReferences.map((reference) => /* @__PURE__ */ jsx("span", {
|
|
631
|
+
"aria-hidden": "true",
|
|
632
|
+
"data-slot": "line-chart-reference-label",
|
|
633
|
+
"data-reference-label": reference.key,
|
|
634
|
+
"data-reference-value": typeof reference.value === "number" ? reference.value : void 0,
|
|
635
|
+
"data-reference-at": typeof reference.value === "number" ? void 0 : referenceAt(reference),
|
|
636
|
+
"data-axis": typeof reference.value === "number" ? "value" : "category",
|
|
637
|
+
className: "line-chart-reference-label",
|
|
638
|
+
children: reference.label
|
|
639
|
+
}, reference.key)),
|
|
511
640
|
/* @__PURE__ */ jsxs("div", {
|
|
512
641
|
ref: tip,
|
|
513
642
|
"aria-hidden": "true",
|
|
@@ -530,7 +659,7 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
530
659
|
}),
|
|
531
660
|
/* @__PURE__ */ jsx("span", {
|
|
532
661
|
className: "ms-2 flex-none whitespace-nowrap font-medium tabular-nums text-foreground",
|
|
533
|
-
children: format(shown
|
|
662
|
+
children: shown ? format(read(shown, line.key), line) : format(0, line)
|
|
534
663
|
})
|
|
535
664
|
]
|
|
536
665
|
}, line.key))]
|
|
@@ -593,7 +722,7 @@ const LineChart = forwardRef(function LineChart({ data, series, label, unit = ""
|
|
|
593
722
|
/* @__PURE__ */ jsx("tbody", { children: data.map((item) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
|
|
594
723
|
scope: "row",
|
|
595
724
|
children: item.label
|
|
596
|
-
}), series.map((line) => /* @__PURE__ */ jsx("td", { children: format(item
|
|
725
|
+
}), series.map((line) => /* @__PURE__ */ jsx("td", { children: format(read(item, line.key), line) }, line.key))] }, item.key)) })
|
|
597
726
|
] })
|
|
598
727
|
}),
|
|
599
728
|
/* @__PURE__ */ jsx("p", {
|