@hjmds/react 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions.d.ts +10 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +9 -3
- package/dist/actions.js.map +1 -1
- package/dist/authoring-tools.d.ts +38 -0
- package/dist/authoring-tools.d.ts.map +1 -0
- package/dist/authoring-tools.js +65 -0
- package/dist/authoring-tools.js.map +1 -0
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +1 -0
- package/dist/calendar.js.map +1 -1
- package/dist/chart.d.ts +157 -0
- package/dist/chart.d.ts.map +1 -0
- package/dist/chart.js +467 -0
- package/dist/chart.js.map +1 -0
- package/dist/collection-editing.d.ts +66 -0
- package/dist/collection-editing.d.ts.map +1 -0
- package/dist/collection-editing.js +126 -0
- package/dist/collection-editing.js.map +1 -0
- package/dist/communication.d.ts +51 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +71 -0
- package/dist/communication.js.map +1 -0
- package/dist/data-table.d.ts +21 -1
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +105 -10
- package/dist/data-table.js.map +1 -1
- package/dist/data-workflows.d.ts +67 -0
- package/dist/data-workflows.d.ts.map +1 -0
- package/dist/data-workflows.js +60 -0
- package/dist/data-workflows.js.map +1 -0
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/evidence.js.map +1 -1
- package/dist/internal/use-composition-action.d.ts +17 -0
- package/dist/internal/use-composition-action.d.ts.map +1 -0
- package/dist/internal/use-composition-action.js +18 -0
- package/dist/internal/use-composition-action.js.map +1 -0
- package/dist/media-transport.d.ts +22 -0
- package/dist/media-transport.d.ts.map +1 -0
- package/dist/media-transport.js +50 -0
- package/dist/media-transport.js.map +1 -0
- package/dist/navigation.d.ts +3 -1
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +29 -5
- package/dist/navigation.js.map +1 -1
- package/dist/screen-flows.d.ts +3 -1
- package/dist/screen-flows.d.ts.map +1 -1
- package/dist/screen-flows.js +6 -4
- package/dist/screen-flows.js.map +1 -1
- package/dist/screens.d.ts +8 -2
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +11 -5
- package/dist/screens.js.map +1 -1
- package/dist/social-compositions.d.ts +52 -0
- package/dist/social-compositions.d.ts.map +1 -0
- package/dist/social-compositions.js +41 -0
- package/dist/social-compositions.js.map +1 -0
- package/dist/streaming-answer.d.ts +24 -0
- package/dist/streaming-answer.d.ts.map +1 -0
- package/dist/streaming-answer.js +57 -0
- package/dist/streaming-answer.js.map +1 -0
- package/dist/styles.css +145 -1
- package/dist/styles.layered.css +145 -1
- package/dist/table-family.d.ts +68 -0
- package/dist/table-family.d.ts.map +1 -0
- package/dist/table-family.js +75 -0
- package/dist/table-family.js.map +1 -0
- package/dist/task-controls.d.ts +47 -0
- package/dist/task-controls.d.ts.map +1 -0
- package/dist/task-controls.js +46 -0
- package/dist/task-controls.js.map +1 -0
- package/dist/toggle-feedback.d.ts +26 -0
- package/dist/toggle-feedback.d.ts.map +1 -0
- package/dist/toggle-feedback.js +47 -0
- package/dist/toggle-feedback.js.map +1 -0
- package/dist/transfer-task.d.ts +18 -0
- package/dist/transfer-task.d.ts.map +1 -0
- package/dist/transfer-task.js +56 -0
- package/dist/transfer-task.js.map +1 -0
- package/package.json +58 -3
package/dist/chart.js
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { resolveDatavizSeriesColor } from "@hjmds/design-contracts/dataviz";
|
|
4
|
+
import { prepareChartLineMorph, areaPath, chartRecipe, describeChart, isChartNumber, linePath, nearestChartIndex, nearestScatterPoint, observedIndices, resolveBarGeometry, resolveLineGeometry, resolvePieGeometry, resolvePieVisibility, resolveRadarGeometry, resolveScatterGeometry, scaleLinear, seriesExtremes, splitSegments, stackValues, stepChartIndex, validateChartSeries, } from "@hjmds/design-contracts/components/chart";
|
|
5
|
+
import { spacing } from "@hjmds/design-contracts/foundations";
|
|
6
|
+
import { useOptionalHjmTheme } from "./provider.js";
|
|
7
|
+
import { classNames } from "./internal.js";
|
|
8
|
+
/*
|
|
9
|
+
* Web chart renderers (2026-10-14). Geometry, domains, stacking, gaps and the text alternative come
|
|
10
|
+
* from `@hjmds/design-contracts/components/chart`; this file only paints plain SVG and translates
|
|
11
|
+
* pointer/keyboard input into the contract's focus index. Prop names mirror
|
|
12
|
+
* `@hjmds/react-native/chart` so products write one chart description for both surfaces.
|
|
13
|
+
* Interaction follows react-native-wagmi-charts (press-and-drag cursor, release returns to the
|
|
14
|
+
* latest value) with pointer events and `touch-action: pan-y` so vertical page scrolling still wins.
|
|
15
|
+
* Optional data morphing updates SVG hosts directly and respects reduced motion.
|
|
16
|
+
*/
|
|
17
|
+
// ---------------------------------------------------------------------------------------------
|
|
18
|
+
// Shared helpers
|
|
19
|
+
// ---------------------------------------------------------------------------------------------
|
|
20
|
+
function kebab(value) {
|
|
21
|
+
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Chrome colors stay recipe references resolved through the provider's CSS variables, so a theme
|
|
25
|
+
* switch repaints without re-rendering and the recipe (not styles.css) owns the choice.
|
|
26
|
+
*/
|
|
27
|
+
function cssColor(reference) {
|
|
28
|
+
const name = reference.source === "theme"
|
|
29
|
+
? `--hjm-color-${kebab(reference.key)}`
|
|
30
|
+
: reference.source === "accent" ? `--hjm-accent-${kebab(reference.key)}` : `--hjm-accent-fill-${kebab(reference.key)}`;
|
|
31
|
+
return reference.alpha === undefined || reference.alpha === 1
|
|
32
|
+
? `var(${name})`
|
|
33
|
+
: `color-mix(in srgb, var(${name}) ${Math.round(reference.alpha * 100)}%, transparent)`;
|
|
34
|
+
}
|
|
35
|
+
const chrome = chartRecipe.chrome;
|
|
36
|
+
const chromeStyle = {
|
|
37
|
+
"--hjm-chart-axis-line": cssColor(chrome.axisLine),
|
|
38
|
+
"--hjm-chart-axis-label": cssColor(chrome.axisLabel),
|
|
39
|
+
"--hjm-chart-grid-line": cssColor(chrome.gridLine),
|
|
40
|
+
"--hjm-chart-grid-width": `${chrome.gridWidth}px`,
|
|
41
|
+
"--hjm-chart-legend-label": cssColor(chrome.legendLabel),
|
|
42
|
+
"--hjm-chart-tooltip-surface": cssColor(chrome.tooltipSurface),
|
|
43
|
+
"--hjm-chart-tooltip-border": cssColor(chrome.tooltipBorder),
|
|
44
|
+
"--hjm-chart-tooltip-content": cssColor(chrome.tooltipContent),
|
|
45
|
+
};
|
|
46
|
+
function useSeriesColor() {
|
|
47
|
+
const theme = useOptionalHjmTheme()?.environment.theme ?? "light";
|
|
48
|
+
return useCallback((series, index) => series.color ?? resolveDatavizSeriesColor(theme, index), [theme]);
|
|
49
|
+
}
|
|
50
|
+
function useSvgId() {
|
|
51
|
+
return useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
52
|
+
}
|
|
53
|
+
/** Responsive width: `fallback` renders on the server and before the first measurement. */
|
|
54
|
+
function useMeasuredWidth(fallback) {
|
|
55
|
+
const [width, setWidth] = useState(fallback);
|
|
56
|
+
const [node, setNode] = useState(null);
|
|
57
|
+
useLayoutEffect(() => {
|
|
58
|
+
if (!node)
|
|
59
|
+
return;
|
|
60
|
+
const update = (next) => { if (next > 0)
|
|
61
|
+
setWidth(Math.round(next)); };
|
|
62
|
+
update(node.getBoundingClientRect().width);
|
|
63
|
+
if (typeof ResizeObserver === "undefined")
|
|
64
|
+
return;
|
|
65
|
+
const observer = new ResizeObserver((entries) => update(entries[0]?.contentRect.width ?? 0));
|
|
66
|
+
observer.observe(node);
|
|
67
|
+
return () => observer.disconnect();
|
|
68
|
+
}, [node]);
|
|
69
|
+
return [setNode, width];
|
|
70
|
+
}
|
|
71
|
+
/** Controlled/uncontrolled focus plus a transient scrub index (same model as the Native renderer). */
|
|
72
|
+
function useFocus({ focusIndex, defaultFocusIndex = null, onFocusChange, holdOnRelease = false }, fallback, categories) {
|
|
73
|
+
const categoryKey = (index) => index === null || categories?.[index] === undefined ? null : JSON.stringify([categories[index], categories.slice(0, index).filter(value => value === categories[index]).length]);
|
|
74
|
+
const selectedKey = useRef(categoryKey(defaultFocusIndex));
|
|
75
|
+
const [own, setOwn] = useState(defaultFocusIndex);
|
|
76
|
+
const [scrub, setScrub] = useState(null);
|
|
77
|
+
const last = useRef(null);
|
|
78
|
+
const mapped = categories && selectedKey.current !== null ? categories.findIndex((_, index) => categoryKey(index) === selectedKey.current) : own;
|
|
79
|
+
const ownSelection = mapped === -1 ? null : mapped;
|
|
80
|
+
const settled = focusIndex !== undefined ? (categories && (focusIndex === null || categories[focusIndex] === undefined) ? null : focusIndex) : ownSelection;
|
|
81
|
+
const focus = scrub ?? settled ?? fallback;
|
|
82
|
+
const set = useCallback((index, scrubbing) => {
|
|
83
|
+
if (scrubbing) {
|
|
84
|
+
setScrub(index);
|
|
85
|
+
last.current = index;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
setScrub(null);
|
|
89
|
+
if (focusIndex === undefined) {
|
|
90
|
+
selectedKey.current = categoryKey(index);
|
|
91
|
+
setOwn(index);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
onFocusChange?.(index, { scrubbing });
|
|
95
|
+
}, [focusIndex, onFocusChange, categories]);
|
|
96
|
+
const release = useCallback(() => set(holdOnRelease ? last.current : null, false), [set, holdOnRelease]);
|
|
97
|
+
return { focus, scrubbing: scrub !== null, set, release };
|
|
98
|
+
}
|
|
99
|
+
/** Press-and-drag; the pointer is captured so a drag that leaves the plot keeps scrubbing. */
|
|
100
|
+
function useScrub(pick, onScrub, onEnd) {
|
|
101
|
+
const active = useRef(null);
|
|
102
|
+
const at = (event) => {
|
|
103
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
104
|
+
return pick(event.clientX - rect.left, event.clientY - rect.top);
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
onPointerDown(event) {
|
|
108
|
+
if (event.button !== 0)
|
|
109
|
+
return;
|
|
110
|
+
active.current = event.pointerId;
|
|
111
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
112
|
+
onScrub(at(event));
|
|
113
|
+
},
|
|
114
|
+
onPointerMove(event) {
|
|
115
|
+
if (active.current !== event.pointerId)
|
|
116
|
+
return;
|
|
117
|
+
onScrub(at(event));
|
|
118
|
+
},
|
|
119
|
+
onPointerUp(event) {
|
|
120
|
+
if (active.current !== event.pointerId)
|
|
121
|
+
return;
|
|
122
|
+
active.current = null;
|
|
123
|
+
onEnd();
|
|
124
|
+
},
|
|
125
|
+
onPointerCancel(event) {
|
|
126
|
+
if (active.current !== event.pointerId)
|
|
127
|
+
return;
|
|
128
|
+
active.current = null;
|
|
129
|
+
onEnd();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function valueText(categories, series, index, format, missing) {
|
|
134
|
+
const values = series.map((item) => { const v = item.values[index]; return isChartNumber(v) ? format(v) : missing; });
|
|
135
|
+
return series.length === 1 ? `${categories[index]} ${values[0]}` : `${categories[index]} ${series.map((item, s) => `${item.label} ${values[s]}`).join(", ")}`;
|
|
136
|
+
}
|
|
137
|
+
/** Arrow/Home/End stepping over observed indices only; returns undefined for unrelated keys. */
|
|
138
|
+
function keyStep(event, available, focus, nextKeys, previousKeys) {
|
|
139
|
+
if (nextKeys.includes(event.key))
|
|
140
|
+
return stepChartIndex(available, focus, 1);
|
|
141
|
+
if (previousKeys.includes(event.key))
|
|
142
|
+
return stepChartIndex(available, focus, -1);
|
|
143
|
+
if (event.key === "Home")
|
|
144
|
+
return available[0] ?? null;
|
|
145
|
+
if (event.key === "End")
|
|
146
|
+
return available.at(-1) ?? null;
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
function CursorBubble({ x, width, text }) {
|
|
150
|
+
// Clamped like the Native bubble so the label never leaves the plot.
|
|
151
|
+
const half = 44, center = Math.min(width - half, Math.max(half, x));
|
|
152
|
+
return _jsx("span", { className: "hjm-chart__bubble", "aria-hidden": "true", style: { left: center }, children: text });
|
|
153
|
+
}
|
|
154
|
+
// Paths/rectangles update their host attributes directly; values, axes and accessibility
|
|
155
|
+
// always come from the latest real data. No per-frame React reconciliation.
|
|
156
|
+
function MotionLinePath({ points, lower, floor, curve, animate, ...rest }) {
|
|
157
|
+
const reducedMotion = useOptionalHjmTheme()?.environment.reducedMotion;
|
|
158
|
+
const node = useRef(null);
|
|
159
|
+
const previous = useRef(null);
|
|
160
|
+
const path = (p, base, edge) => base === undefined ? linePath(p, curve) : areaPath(p, base, curve, edge);
|
|
161
|
+
const signature = JSON.stringify([points, lower, floor, curve]);
|
|
162
|
+
useLayoutEffect(() => {
|
|
163
|
+
const before = previous.current;
|
|
164
|
+
const target = { points, lower, floor };
|
|
165
|
+
previous.current = target;
|
|
166
|
+
// A bounded morph avoids multiplying frame work for dense historical datasets.
|
|
167
|
+
const morph = animate && !reducedMotion && !document.hidden && before && points.length + before.points.length <= 1024 ? prepareChartLineMorph(before.points, points) : null;
|
|
168
|
+
if (!morph) {
|
|
169
|
+
node.current?.setAttribute("d", path(points, floor, lower));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const lowerMorph = before?.lower && lower ? prepareChartLineMorph(before.lower, lower) : null;
|
|
173
|
+
let frame = 0, start;
|
|
174
|
+
const paint = (t) => {
|
|
175
|
+
const current = t === 1 ? target : { points: morph(t), lower: lowerMorph?.(t) ?? lower, floor: floor === undefined ? undefined : (before?.floor ?? floor) + (floor - (before?.floor ?? floor)) * t };
|
|
176
|
+
previous.current = current;
|
|
177
|
+
const d = path(current.points, current.floor, current.lower);
|
|
178
|
+
node.current?.setAttribute("d", d);
|
|
179
|
+
};
|
|
180
|
+
const tick = (now) => { start ??= now; const t = Math.min(1, (now - start) / 220); paint(1 - (1 - t) ** 3); if (t < 1)
|
|
181
|
+
frame = requestAnimationFrame(tick); };
|
|
182
|
+
paint(0);
|
|
183
|
+
frame = requestAnimationFrame(tick);
|
|
184
|
+
const visibility = () => { if (document.hidden) {
|
|
185
|
+
cancelAnimationFrame(frame);
|
|
186
|
+
paint(1);
|
|
187
|
+
} };
|
|
188
|
+
document.addEventListener("visibilitychange", visibility);
|
|
189
|
+
return () => { cancelAnimationFrame(frame); document.removeEventListener("visibilitychange", visibility); };
|
|
190
|
+
}, [signature, animate, reducedMotion]);
|
|
191
|
+
return _jsx("path", { ...rest, ref: node, d: path(points, floor, lower) });
|
|
192
|
+
}
|
|
193
|
+
function MotionBar({ bar, animate, vertical = true, rounded = true, ...rest }) {
|
|
194
|
+
const reducedMotion = useOptionalHjmTheme()?.environment.reducedMotion;
|
|
195
|
+
const node = useRef(null), previous = useRef(null);
|
|
196
|
+
const signature = JSON.stringify([bar, vertical, rounded]);
|
|
197
|
+
useLayoutEffect(() => {
|
|
198
|
+
const before = previous.current;
|
|
199
|
+
previous.current = bar;
|
|
200
|
+
if (!animate || reducedMotion || !before || document.hidden) {
|
|
201
|
+
node.current?.setAttribute("d", barPath(bar, vertical, bar.value < 0, rounded));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let frame = 0, start;
|
|
205
|
+
const paint = (t) => {
|
|
206
|
+
const value = { ...bar, x: before.x + (bar.x - before.x) * t, y: before.y + (bar.y - before.y) * t, width: before.width + (bar.width - before.width) * t, height: before.height + (bar.height - before.height) * t };
|
|
207
|
+
previous.current = value;
|
|
208
|
+
node.current?.setAttribute("d", barPath(value, vertical, value.value < 0, rounded));
|
|
209
|
+
};
|
|
210
|
+
const tick = (now) => { start ??= now; const t = Math.min(1, (now - start) / 220); paint(1 - (1 - t) ** 3); if (t < 1)
|
|
211
|
+
frame = requestAnimationFrame(tick); };
|
|
212
|
+
paint(0);
|
|
213
|
+
frame = requestAnimationFrame(tick);
|
|
214
|
+
const visibility = () => { if (document.hidden) {
|
|
215
|
+
cancelAnimationFrame(frame);
|
|
216
|
+
paint(1);
|
|
217
|
+
} };
|
|
218
|
+
document.addEventListener("visibilitychange", visibility);
|
|
219
|
+
return () => { cancelAnimationFrame(frame); document.removeEventListener("visibilitychange", visibility); };
|
|
220
|
+
}, [signature, animate, reducedMotion]);
|
|
221
|
+
return _jsx("path", { ...rest, ref: node, d: barPath(bar, vertical, bar.value < 0, rounded) });
|
|
222
|
+
}
|
|
223
|
+
export function LineChart({ categories, series, accessibilityLabel, formatValue, missingLabel, height = chartRecipe.heights.regular, width: fallbackWidth = 320, curve = "linear", area, stacked = false, invert = false, domain, includeZero, referenceValue, extremes, showAxis = false, valueAxis = false, ticks: tickOverride, cursorLabel, animateChanges = false, scrubEmphasis, className, layoutStyle, ...focusProps }) {
|
|
224
|
+
validateChartSeries(categories, series);
|
|
225
|
+
const colorOf = useSeriesColor(), id = useSvgId(), descriptionId = `${id}-description`;
|
|
226
|
+
const [measure, width] = useMeasuredWidth(fallbackWidth);
|
|
227
|
+
const available = useMemo(() => observedIndices(series), [series]);
|
|
228
|
+
const { focus, scrubbing, set, release } = useFocus(focusProps, available.at(-1) ?? null, categories);
|
|
229
|
+
const insets = showAxis || valueAxis ? chartRecipe.insets.axis : chartRecipe.insets.plot;
|
|
230
|
+
const g = useMemo(() => resolveLineGeometry({
|
|
231
|
+
categories, series, width, height, insets, invert, stacked,
|
|
232
|
+
...(domain ? { domain } : {}), ...(includeZero === undefined ? {} : { includeZero }),
|
|
233
|
+
}), [categories, series, width, height, insets, invert, stacked, domain, includeZero]);
|
|
234
|
+
// Release returns to the default (latest) focus unless holdOnRelease keeps the scrubbed point.
|
|
235
|
+
const handlers = useScrub((x) => nearestChartIndex(g.xs, x, available), (index) => set(index, true), release);
|
|
236
|
+
const showArea = area ?? series.length === 1;
|
|
237
|
+
const reference = referenceValue === "first" ? series[0]?.values.find(isChartNumber) : referenceValue;
|
|
238
|
+
const marks = extremes && !scrubbing ? seriesExtremes(series[0]?.values ?? []) : null;
|
|
239
|
+
const onKeyDown = (event) => {
|
|
240
|
+
const next = keyStep(event, available, focus, ["ArrowRight", "ArrowUp"], ["ArrowLeft", "ArrowDown"]);
|
|
241
|
+
if (next === undefined)
|
|
242
|
+
return;
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
set(next, false);
|
|
245
|
+
};
|
|
246
|
+
return (_jsxs("div", { ref: measure, className: classNames("hjm-chart", "hjm-chart--line", className), style: { ...chromeStyle, ...layoutStyle }, "data-scrubbing": scrubbing || undefined, children: [_jsxs("div", { className: "hjm-chart__plot", role: "slider", tabIndex: 0, "aria-label": accessibilityLabel, "aria-describedby": descriptionId, "aria-valuemin": 0, "aria-valuemax": Math.max(0, categories.length - 1), "aria-valuenow": focus ?? 0, "aria-valuetext": focus === null ? missingLabel : valueText(categories, series, focus, formatValue, missingLabel), style: { blockSize: height }, onKeyDown: onKeyDown, ...handlers, children: [_jsxs("svg", { width: width, height: height, viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", focusable: "false", children: [_jsxs("defs", { children: [_jsx("clipPath", { id: `${id}-scrub`, children: _jsx("rect", { x: scrubEmphasis === "after" && focus !== null ? g.xs[focus] ?? g.plot.left : g.plot.left, y: 0, width: focus === null ? width : scrubEmphasis === "after" ? g.plot.right - (g.xs[focus] ?? g.plot.left) : (g.xs[focus] ?? g.plot.right) - g.plot.left, height: height }) }), series.map((item, s) => (_jsxs("linearGradient", { id: `${id}-area-${s}`, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0", stopColor: colorOf(item, s), stopOpacity: chartRecipe.area.topOpacity }), _jsx("stop", { offset: "1", stopColor: colorOf(item, s), stopOpacity: chartRecipe.area.bottomOpacity })] }, item.id)))] }), (tickOverride ?? g.ticks).map((tick) => _jsx("line", { className: "hjm-chart__grid", x1: g.plot.left, x2: g.plot.right, y1: g.y(tick), y2: g.y(tick) }, tick)), showAxis || valueAxis ? (tickOverride ?? g.ticks).map((tick) => _jsx("text", { className: "hjm-chart__label", x: g.plot.left - 6, y: g.y(tick), dominantBaseline: "middle", textAnchor: "end", children: formatValue(tick) }, `t${tick}`)) : null, isChartNumber(reference) ? (_jsx("line", { className: "hjm-chart__reference", x1: g.plot.left, x2: g.plot.right, y1: g.y(reference), y2: g.y(reference), strokeDasharray: chartRecipe.reference.dash.join(" ") })) : null, series.map((item, s) => {
|
|
247
|
+
const color = colorOf(item, s), segments = splitSegments(g.plots[s]);
|
|
248
|
+
const lower = g.lower?.[s];
|
|
249
|
+
return (_jsxs("g", { "data-series": item.id, children: [showArea || stacked ? segments.map((segment, i) => (_jsx(MotionLinePath, { animate: animateChanges, points: segment, floor: g.plot.bottom, curve: curve, lower: lower ? segment.map((p) => lower[p.index]) : undefined, className: "hjm-chart__area", fill: `url(#${id}-area-${s})` }, `a${i}`))) : null, segments.map((segment, i) => (_jsxs("g", { children: [_jsx(MotionLinePath, { animate: animateChanges, points: segment, curve: curve, className: "hjm-chart__line", stroke: scrubEmphasis && focus !== null ? "var(--hjm-chart-axis-label)" : color, strokeWidth: chartRecipe.line.strokeWidth, fill: "none", strokeLinejoin: "round", strokeLinecap: "round" }), scrubEmphasis && focus !== null && _jsx("g", { clipPath: `url(#${id}-scrub)`, children: _jsx(MotionLinePath, { animate: animateChanges, points: segment, curve: curve, className: "hjm-chart__line", stroke: color, strokeWidth: chartRecipe.line.strokeWidth, fill: "none", strokeLinejoin: "round", strokeLinecap: "round" }) })] }, `l${i}`))), segments.filter((segment) => segment.length === 1).map((segment) => (_jsx("circle", { cx: segment[0].x, cy: segment[0].y, r: 3, fill: color }, `d${segment[0].index}`)))] }, item.id));
|
|
250
|
+
}), focus !== null && g.xs[focus] !== undefined ? (_jsxs("g", { className: "hjm-chart__focus", children: [_jsx("line", { className: "hjm-chart__cursor", x1: g.xs[focus], x2: g.xs[focus], y1: g.plot.top - 4, y2: g.plot.bottom, strokeWidth: chartRecipe.cursor.strokeWidth, strokeDasharray: scrubbing ? undefined : chartRecipe.cursor.dash.join(" ") }), series.map((item, s) => {
|
|
251
|
+
const p = g.plots[s][focus];
|
|
252
|
+
if (!p)
|
|
253
|
+
return null;
|
|
254
|
+
const color = colorOf(item, s);
|
|
255
|
+
return (_jsxs("g", { children: [_jsx("circle", { cx: p.x, cy: p.y, r: chartRecipe.line.focusHaloRadius, fill: color, opacity: chartRecipe.line.focusHaloOpacity }), _jsx("circle", { className: "hjm-chart__dot", cx: p.x, cy: p.y, r: chartRecipe.line.pointRadius, fill: color, strokeWidth: 2 })] }, item.id));
|
|
256
|
+
})] })) : null, marks && extremes ? [[marks.max, extremes.high, -10], [marks.min, extremes.low, 16]]
|
|
257
|
+
.filter(([index], i) => i === 0 || index !== marks.max)
|
|
258
|
+
.map(([index, label, dy]) => {
|
|
259
|
+
const p = g.plots[0][index];
|
|
260
|
+
if (!p)
|
|
261
|
+
return null;
|
|
262
|
+
return (_jsx("text", { className: "hjm-chart__label hjm-chart__extreme", x: Math.min(g.plot.right - 4, Math.max(g.plot.left + 4, p.x)), y: Math.max(10, Math.min(height - 2, p.y + dy)), textAnchor: p.x < width * 0.2 ? "start" : p.x > width * 0.8 ? "end" : "middle", children: `${label} ${formatValue(p.value)}` }, label));
|
|
263
|
+
}) : null, showAxis && categories.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("text", { className: "hjm-chart__label", x: g.xs[0], y: height - 4, textAnchor: "start", children: categories[0] }), categories.length > 1 ? _jsx("text", { className: "hjm-chart__label", x: g.xs.at(-1), y: height - 4, textAnchor: "end", children: categories.at(-1) }) : null] })) : null] }), scrubbing && focus !== null && g.xs[focus] !== undefined ? _jsx(CursorBubble, { x: g.xs[focus], width: width, text: cursorLabel?.(focus) ?? categories[focus] }) : null] }), _jsx("p", { id: descriptionId, className: "hjm-visually-hidden", children: describeChart(categories, series, formatValue, missingLabel) })] }));
|
|
264
|
+
}
|
|
265
|
+
/** Rounds only the value end of a bar so stacks read as one column. */
|
|
266
|
+
function barPath(bar, vertical, negative, round) {
|
|
267
|
+
const { x, y, width: w, height: h } = bar;
|
|
268
|
+
const r = round ? Math.min(chartRecipe.bar.radius, vertical ? w / 2 : h / 2, vertical ? h : w) : 0;
|
|
269
|
+
const n = (value) => Number(value.toFixed(2));
|
|
270
|
+
if (r <= 0)
|
|
271
|
+
return `M${n(x)},${n(y)}h${n(w)}v${n(h)}h${n(-w)}Z`;
|
|
272
|
+
if (vertical && !negative)
|
|
273
|
+
return `M${n(x)},${n(y + h)}V${n(y + r)}Q${n(x)},${n(y)} ${n(x + r)},${n(y)}H${n(x + w - r)}Q${n(x + w)},${n(y)} ${n(x + w)},${n(y + r)}V${n(y + h)}Z`;
|
|
274
|
+
if (vertical)
|
|
275
|
+
return `M${n(x)},${n(y)}H${n(x + w)}V${n(y + h - r)}Q${n(x + w)},${n(y + h)} ${n(x + w - r)},${n(y + h)}H${n(x + r)}Q${n(x)},${n(y + h)} ${n(x)},${n(y + h - r)}Z`;
|
|
276
|
+
if (!negative)
|
|
277
|
+
return `M${n(x)},${n(y)}H${n(x + w - r)}Q${n(x + w)},${n(y)} ${n(x + w)},${n(y + r)}V${n(y + h - r)}Q${n(x + w)},${n(y + h)} ${n(x + w - r)},${n(y + h)}H${n(x)}Z`;
|
|
278
|
+
return `M${n(x + w)},${n(y)}V${n(y + h)}H${n(x + r)}Q${n(x)},${n(y + h)} ${n(x)},${n(y + h - r)}V${n(y + r)}Q${n(x)},${n(y)} ${n(x + r)},${n(y)}Z`;
|
|
279
|
+
}
|
|
280
|
+
export function BarChart({ categories, series, accessibilityLabel, formatValue, missingLabel, height, width: fallbackWidth = 320, orientation = "vertical", layout = "grouped", domain, showAxis = false, showValues = false, animateChanges = false, className, layoutStyle, ...focusProps }) {
|
|
281
|
+
validateChartSeries(categories, series);
|
|
282
|
+
const colorOf = useSeriesColor(), id = useSvgId(), descriptionId = `${id}-description`;
|
|
283
|
+
const [measure, width] = useMeasuredWidth(fallbackWidth);
|
|
284
|
+
const vertical = orientation === "vertical";
|
|
285
|
+
// Horizontal bars grow with the category count so labels never overlap (same rule as Native).
|
|
286
|
+
const resolvedHeight = height ?? (vertical ? chartRecipe.heights.regular : Math.max(chartRecipe.heights.compact, categories.length * 36 + spacing.lg));
|
|
287
|
+
const available = useMemo(() => observedIndices(series), [series]);
|
|
288
|
+
const { focus, scrubbing, set } = useFocus(focusProps, null, categories);
|
|
289
|
+
const insets = vertical
|
|
290
|
+
? (showAxis ? chartRecipe.insets.axis : { ...chartRecipe.insets.plot, bottom: spacing.lg })
|
|
291
|
+
: { top: spacing.xs, right: showValues ? 48 : spacing.sm, bottom: spacing.xs, left: 72 };
|
|
292
|
+
const g = useMemo(() => resolveBarGeometry({ categories, series, width, height: resolvedHeight, insets, orientation, layout, ...(domain ? { domain } : {}) }), [categories, series, width, resolvedHeight, insets, orientation, layout, domain]);
|
|
293
|
+
const categoryKeys = useMemo(() => {
|
|
294
|
+
const occurrences = new Map();
|
|
295
|
+
return categories.map(label => { const occurrence = occurrences.get(label) ?? 0; occurrences.set(label, occurrence + 1); return JSON.stringify([label, occurrence]); });
|
|
296
|
+
}, [categories]);
|
|
297
|
+
// Only the outermost segment of each stack (per sign) gets rounded corners.
|
|
298
|
+
const outer = useMemo(() => {
|
|
299
|
+
if (layout !== "stacked")
|
|
300
|
+
return null;
|
|
301
|
+
const stacks = stackValues(series), result = new Set();
|
|
302
|
+
categories.forEach((_, i) => {
|
|
303
|
+
let positive = -1, negative = -1;
|
|
304
|
+
series.forEach((item, s) => { const v = item.values[i]; if (!isChartNumber(v) || !stacks[s][i])
|
|
305
|
+
return; if (v >= 0)
|
|
306
|
+
positive = s;
|
|
307
|
+
else
|
|
308
|
+
negative = s; });
|
|
309
|
+
if (positive >= 0)
|
|
310
|
+
result.add(`${positive}-${i}`);
|
|
311
|
+
if (negative >= 0)
|
|
312
|
+
result.add(`${negative}-${i}`);
|
|
313
|
+
});
|
|
314
|
+
return result;
|
|
315
|
+
}, [categories, series, layout]);
|
|
316
|
+
const handlers = useScrub((x, y) => nearestChartIndex(g.centers, vertical ? x : y, available), (index) => set(index, true), () => set(focus, false));
|
|
317
|
+
const onKeyDown = (event) => {
|
|
318
|
+
if (event.key === "Escape" && focus !== null) {
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
set(null, false);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const next = vertical
|
|
324
|
+
? keyStep(event, available, focus, ["ArrowRight", "ArrowUp"], ["ArrowLeft", "ArrowDown"])
|
|
325
|
+
: keyStep(event, available, focus, ["ArrowDown", "ArrowRight"], ["ArrowUp", "ArrowLeft"]);
|
|
326
|
+
if (next === undefined)
|
|
327
|
+
return;
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
set(next, false);
|
|
330
|
+
};
|
|
331
|
+
const totals = layout === "stacked" && showValues
|
|
332
|
+
? categories.map((_, i) => series.reduce((sum, item) => sum + (isChartNumber(item.values[i]) ? item.values[i] : 0), 0))
|
|
333
|
+
: null;
|
|
334
|
+
return (_jsxs("div", { ref: measure, className: classNames("hjm-chart", "hjm-chart--bar", className), style: { ...chromeStyle, ...layoutStyle }, "data-orientation": orientation, "data-scrubbing": scrubbing || undefined, children: [_jsxs("div", { className: "hjm-chart__plot", role: "slider", tabIndex: 0, "aria-label": accessibilityLabel, "aria-describedby": descriptionId, "aria-orientation": vertical ? "horizontal" : "vertical", "aria-valuemin": 0, "aria-valuemax": Math.max(0, categories.length - 1), "aria-valuenow": focus ?? 0, "aria-valuetext": focus === null ? accessibilityLabel : valueText(categories, series, focus, formatValue, missingLabel), style: { blockSize: resolvedHeight }, onKeyDown: onKeyDown, ...handlers, children: [_jsxs("svg", { width: width, height: resolvedHeight, viewBox: `0 0 ${width} ${resolvedHeight}`, "aria-hidden": "true", focusable: "false", children: [g.ticks.map((tick) => vertical
|
|
335
|
+
? _jsx("line", { className: "hjm-chart__grid", x1: g.plot.left, x2: g.plot.right, y1: g.value(tick), y2: g.value(tick) }, tick)
|
|
336
|
+
: _jsx("line", { className: "hjm-chart__grid", y1: g.plot.top, y2: g.plot.bottom, x1: g.value(tick), x2: g.value(tick) }, tick)), showAxis && vertical ? g.ticks.map((tick) => _jsx("text", { className: "hjm-chart__label", x: g.plot.left - 6, y: g.value(tick), dominantBaseline: "middle", textAnchor: "end", children: formatValue(tick) }, `t${tick}`)) : null, g.bars.map((bar) => (_jsx(MotionBar, { bar: bar, animate: animateChanges && g.bars.length <= 200, vertical: vertical, rounded: outer ? outer.has(`${bar.seriesIndex}-${bar.index}`) : true, className: "hjm-chart__bar", "data-index": bar.index, fill: colorOf(series[bar.seriesIndex], bar.seriesIndex), opacity: focus === null || focus === bar.index ? 1 : chartRecipe.bar.restOpacity }, `${series[bar.seriesIndex].id}-${categoryKeys[bar.index]}`))), showValues && layout === "grouped" ? g.bars.map((bar) => (_jsx("text", { className: "hjm-chart__label hjm-chart__value", x: vertical ? bar.x + bar.width / 2 : bar.x + bar.width + 4, y: vertical ? bar.y - 4 : bar.y + bar.height / 2, dominantBaseline: vertical ? undefined : "middle", textAnchor: vertical ? "middle" : "start", children: formatValue(bar.value) }, `v${bar.seriesIndex}-${bar.index}`))) : null, totals ? totals.map((total, i) => {
|
|
337
|
+
if (!available.includes(i))
|
|
338
|
+
return null;
|
|
339
|
+
const end = g.value(Math.max(0, ...stackValues(series).map((row) => row[i]?.[1] ?? 0)));
|
|
340
|
+
return (_jsx("text", { className: "hjm-chart__label hjm-chart__value", x: vertical ? g.centers[i] : end + 4, y: vertical ? end - 4 : g.centers[i], dominantBaseline: vertical ? undefined : "middle", textAnchor: vertical ? "middle" : "start", children: formatValue(total) }, `total${i}`));
|
|
341
|
+
}) : null, categories.map((category, i) => vertical
|
|
342
|
+
? (categories.length <= 12 || i % Math.ceil(categories.length / 12) === 0)
|
|
343
|
+
? _jsx("text", { className: "hjm-chart__label", "data-focused": focus === i || undefined, x: g.centers[i], y: resolvedHeight - 4, textAnchor: "middle", children: category }, `c${i}`)
|
|
344
|
+
: null
|
|
345
|
+
: _jsx("text", { className: "hjm-chart__label", "data-focused": focus === i || undefined, x: g.plot.left - 8, y: g.centers[i], dominantBaseline: "middle", textAnchor: "end", children: category }, `c${i}`))] }), scrubbing && focus !== null && vertical && g.centers[focus] !== undefined
|
|
346
|
+
? _jsx(CursorBubble, { x: g.centers[focus], width: width, text: valueText(categories, series, focus, formatValue, missingLabel) })
|
|
347
|
+
: null] }), _jsx("p", { id: descriptionId, className: "hjm-visually-hidden", children: describeChart(categories, series, formatValue, missingLabel) })] }));
|
|
348
|
+
}
|
|
349
|
+
/** Swatch + label (+ value). Color is never the only encoding: every row carries its label. */
|
|
350
|
+
export function ChartLegend({ items, selectedId, onSelect, exclusion, className, layoutStyle }) {
|
|
351
|
+
return (_jsx("ul", { className: classNames("hjm-chart-legend", className), style: { ...chromeStyle, ...layoutStyle }, "data-interactive": onSelect ? "" : undefined, children: items.map((item) => {
|
|
352
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: "hjm-chart-legend__swatch", "aria-hidden": "true", style: { background: item.color } }), _jsx("span", { className: "hjm-chart-legend__label", children: item.label }), item.value !== undefined ? _jsx("span", { className: "hjm-chart-legend__value", children: item.value }) : null] }));
|
|
353
|
+
const dimmed = selectedId != null && selectedId !== item.id;
|
|
354
|
+
return (_jsxs("li", { className: "hjm-chart-legend__item", "data-dimmed": dimmed || undefined, children: [exclusion ? _jsx("input", { type: "checkbox", checked: !exclusion.excludedIds.includes(item.id), "aria-label": exclusion.label(item, !exclusion.excludedIds.includes(item.id)), onChange: () => exclusion.onToggle(item.id) }) : null, onSelect ? (_jsx("button", { type: "button", className: "hjm-chart-legend__button", "aria-pressed": selectedId === item.id, onClick: () => onSelect(selectedId === item.id ? null : item.id), children: content })) : content] }, item.id));
|
|
355
|
+
}) }));
|
|
356
|
+
}
|
|
357
|
+
export function PieChart({ data, accessibilityLabel, formatValue, donut = true, size = 180, centerContent, selectedId, defaultSelectedId = null, onSelect, showLegend = true, exclusion, className, layoutStyle, }) {
|
|
358
|
+
const colorOf = useSeriesColor(), id = useSvgId(), descriptionId = `${id}-description`;
|
|
359
|
+
const [own, setOwn] = useState(defaultSelectedId);
|
|
360
|
+
const candidate = selectedId !== undefined ? selectedId : own;
|
|
361
|
+
const selected = candidate !== null && exclusion?.excludedIds.includes(candidate) ? null : candidate;
|
|
362
|
+
const interactive = onSelect !== undefined || selectedId !== undefined;
|
|
363
|
+
const select = (next) => { if (selectedId === undefined)
|
|
364
|
+
setOwn(next); onSelect?.(next); };
|
|
365
|
+
const toggle = (next) => select(selected === next ? null : next);
|
|
366
|
+
// The selected slice moves outward by this much, so it is reserved inside the box.
|
|
367
|
+
const lift = 4;
|
|
368
|
+
const radius = size / 2 - lift - 2, inner = donut ? radius * chartRecipe.pie.donutRatio : 0;
|
|
369
|
+
const visibility = resolvePieVisibility(data, exclusion?.excludedIds);
|
|
370
|
+
const slices = resolvePieGeometry(visibility.values, { cx: size / 2, cy: size / 2, radius, innerRadius: inner });
|
|
371
|
+
const center = typeof centerContent === "function" ? centerContent(visibility) : centerContent;
|
|
372
|
+
const legendExclusion = exclusion ? { excludedIds: exclusion.excludedIds, label: (item, included) => exclusion.label(data.find(datum => datum.id === item.id), included),
|
|
373
|
+
onToggle: (id) => exclusion.onExcludedIdsChange(exclusion.excludedIds.includes(id) ? exclusion.excludedIds.filter(key => key !== id) : [...exclusion.excludedIds, id]) } : undefined;
|
|
374
|
+
const colorAt = (index) => colorOf(data[index], index);
|
|
375
|
+
const percentOf = (index) => slices.find((slice) => slice.index === index)?.percent;
|
|
376
|
+
const percentText = (index) => { const p = percentOf(index); return p === undefined ? "" : `${Math.round(p)}%`; };
|
|
377
|
+
const rowValue = (d, i) => isChartNumber(d.value) ? `${formatValue(d.value)}${percentOf(i) !== undefined ? ` · ${percentText(i)}` : ""}` : "—";
|
|
378
|
+
const summary = data.map((d, i) => exclusion?.excludedIds.includes(d.id) ? "" : `${d.label} ${isChartNumber(d.value) ? formatValue(d.value) : ""}${percentOf(i) !== undefined ? ` (${percentText(i)})` : ""}`.trim()).filter(Boolean).join(", ");
|
|
379
|
+
// With a legend, its rows are the keyboard path; slices stay pointer targets so each item has
|
|
380
|
+
// one tab stop. Without a legend the slices themselves become the buttons.
|
|
381
|
+
const sliceButtons = interactive && !showLegend;
|
|
382
|
+
return (_jsxs("div", { className: classNames("hjm-chart", "hjm-chart--pie", className), style: { ...chromeStyle, ...layoutStyle }, children: [_jsxs("div", { className: "hjm-chart__pie", role: sliceButtons ? "group" : "img", "aria-label": accessibilityLabel, "aria-describedby": descriptionId, style: { inlineSize: size, blockSize: size }, children: [_jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": sliceButtons ? undefined : true, focusable: "false", children: [slices.map((slice) => {
|
|
383
|
+
const datum = data[slice.index], isSelected = selected === datum.id;
|
|
384
|
+
const mid = (slice.startAngle + slice.endAngle) / 2;
|
|
385
|
+
return (_jsx("path", { className: "hjm-chart__slice", "data-id": datum.id, "data-selected": isSelected || undefined, d: slice.path, fill: colorAt(slice.index), opacity: selected !== null && !isSelected ? chartRecipe.bar.restOpacity : 1, transform: isSelected ? `translate(${(Math.sin(mid) * lift).toFixed(2)} ${(-Math.cos(mid) * lift).toFixed(2)})` : undefined, ...(interactive ? { onClick: () => toggle(datum.id) } : {}), ...(sliceButtons ? {
|
|
386
|
+
role: "button",
|
|
387
|
+
tabIndex: 0,
|
|
388
|
+
"aria-pressed": isSelected,
|
|
389
|
+
"aria-label": `${datum.label} ${rowValue(datum, slice.index)}`,
|
|
390
|
+
onKeyDown: (event) => {
|
|
391
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
392
|
+
return;
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
toggle(datum.id);
|
|
395
|
+
},
|
|
396
|
+
} : {}) }, datum.id));
|
|
397
|
+
}), !donut ? slices.filter((slice) => slice.percent >= chartRecipe.pie.minLabelPercent * 2).map((slice) => (_jsx("text", { className: "hjm-chart__slice-label", x: slice.centroid.x, y: slice.centroid.y, dominantBaseline: "middle", textAnchor: "middle", children: percentText(slice.index) }, `t${slice.index}`))) : null] }), donut && center != null ? _jsx("div", { className: "hjm-chart__pie-center", children: center }) : null] }), showLegend ? (_jsx(ChartLegend, { items: data.map((d, i) => ({ id: d.id, label: d.label, color: colorAt(i), value: rowValue(d, i) })), selectedId: selected, ...(legendExclusion ? { exclusion: legendExclusion } : {}), ...(interactive ? { onSelect: select } : {}) })) : null, _jsx("p", { id: descriptionId, className: "hjm-visually-hidden", children: summary })] }));
|
|
398
|
+
}
|
|
399
|
+
export function RadarChart({ categories, series, accessibilityLabel, formatValue, missingLabel, domain, levels = chartRecipe.radar.levels, size = 240, showLegend, className, layoutStyle, }) {
|
|
400
|
+
validateChartSeries(categories, series);
|
|
401
|
+
const colorOf = useSeriesColor(), id = useSvgId(), descriptionId = `${id}-description`;
|
|
402
|
+
// Room around the polygon for axis labels (same reservation as Native).
|
|
403
|
+
const pad = 44, center = size / 2;
|
|
404
|
+
const g = resolveRadarGeometry({ categories, series, cx: center, cy: center, radius: center - pad, levels, ...(domain ? { domain } : {}) });
|
|
405
|
+
return (_jsxs("div", { className: classNames("hjm-chart", "hjm-chart--radar", className), style: { ...chromeStyle, ...layoutStyle }, children: [_jsx("div", { className: "hjm-chart__radar", role: "img", "aria-label": accessibilityLabel, "aria-describedby": descriptionId, style: { inlineSize: size, blockSize: size }, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, "aria-hidden": "true", focusable: "false", children: [g.rings.map((ring, i) => _jsx("path", { className: "hjm-chart__grid", d: ring, fill: "none" }, i)), g.axes.map((axis) => _jsx("line", { className: "hjm-chart__grid", x1: center, y1: center, x2: axis.end.x, y2: axis.end.y }, axis.index)), g.polygons.map((polygon) => {
|
|
406
|
+
const color = colorOf(series[polygon.seriesIndex], polygon.seriesIndex);
|
|
407
|
+
return (_jsxs("g", { "data-series": series[polygon.seriesIndex].id, children: [_jsx("path", { className: "hjm-chart__polygon", d: polygon.path, fill: color, fillOpacity: chartRecipe.radar.fillOpacity, stroke: color, strokeWidth: chartRecipe.radar.strokeWidth, strokeLinejoin: "round" }), polygon.points.map((p) => p.value !== null ? _jsx("circle", { cx: p.x, cy: p.y, r: 3, fill: color }, p.index) : null)] }, polygon.seriesIndex));
|
|
408
|
+
}), g.axes.map((axis) => {
|
|
409
|
+
const first = series[0]?.values[axis.index];
|
|
410
|
+
return (_jsxs("g", { children: [_jsx("text", { className: "hjm-chart__label hjm-chart__axis-name", x: axis.labelAt.x, y: axis.labelAt.y, dominantBaseline: "middle", textAnchor: axis.anchor, children: axis.label }), series.length === 1 ? (_jsx("text", { className: "hjm-chart__label", x: axis.labelAt.x, y: axis.labelAt.y + 14, dominantBaseline: "middle", textAnchor: axis.anchor, children: isChartNumber(first) ? formatValue(first) : missingLabel })) : null] }, `l${axis.index}`));
|
|
411
|
+
})] }) }), showLegend ?? series.length > 1 ? _jsx(ChartLegend, { items: series.map((item, s) => ({ id: item.id, label: item.label, color: colorOf(item, s) })) }) : null, _jsx("p", { id: descriptionId, className: "hjm-visually-hidden", children: describeChart(categories, series, formatValue, missingLabel) })] }));
|
|
412
|
+
}
|
|
413
|
+
export function ScatterChart({ points, accessibilityLabel, xLabel, yLabel, formatX, formatY, height = chartRecipe.heights.tall, width: fallbackWidth = 320, seriesLabels, focusId, onFocusChange, className, layoutStyle, }) {
|
|
414
|
+
const colorOf = useSeriesColor(), id = useSvgId(), descriptionId = `${id}-description`;
|
|
415
|
+
const [measure, width] = useMeasuredWidth(fallbackWidth);
|
|
416
|
+
const [own, setOwn] = useState(null);
|
|
417
|
+
const focused = focusId !== undefined ? focusId : own;
|
|
418
|
+
const choose = (next) => { if (focusId === undefined)
|
|
419
|
+
setOwn(next); onFocusChange?.(next); };
|
|
420
|
+
const g = useMemo(() => resolveScatterGeometry({ points, width, height }), [points, width, height]);
|
|
421
|
+
// Keyboard order follows the x axis so arrows read left to right.
|
|
422
|
+
const ordered = useMemo(() => [...points].sort((a, b) => a.x - b.x || a.y - b.y), [points]);
|
|
423
|
+
const handlers = useScrub((x, y) => nearestScatterPoint(g.points, x, y), (index) => { if (index !== null)
|
|
424
|
+
choose(points[index].id); }, () => undefined);
|
|
425
|
+
const focusPoint = g.points.find((p) => p.id === focused);
|
|
426
|
+
const sx = scaleLinear(g.xDomain, [g.plot.left, g.plot.right]), sy = scaleLinear(g.yDomain, [g.plot.bottom, g.plot.top]);
|
|
427
|
+
const describe = (p) => `${p.label}, ${xLabel} ${formatX(p.x)}, ${yLabel} ${formatY(p.y)}`;
|
|
428
|
+
const position = ordered.findIndex((p) => p.id === focused);
|
|
429
|
+
const onKeyDown = (event) => {
|
|
430
|
+
let next;
|
|
431
|
+
if (event.key === "ArrowRight" || event.key === "ArrowUp")
|
|
432
|
+
next = ordered[Math.min(ordered.length - 1, position + 1)];
|
|
433
|
+
else if (event.key === "ArrowLeft" || event.key === "ArrowDown")
|
|
434
|
+
next = ordered[position < 0 ? ordered.length - 1 : Math.max(0, position - 1)];
|
|
435
|
+
else if (event.key === "Home")
|
|
436
|
+
next = ordered[0];
|
|
437
|
+
else if (event.key === "End")
|
|
438
|
+
next = ordered.at(-1);
|
|
439
|
+
else if (event.key === "Escape" && focused !== null) {
|
|
440
|
+
event.preventDefault();
|
|
441
|
+
choose(null);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
else
|
|
445
|
+
return;
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
if (next)
|
|
448
|
+
choose(next.id);
|
|
449
|
+
};
|
|
450
|
+
return (_jsxs("div", { ref: measure, className: classNames("hjm-chart", "hjm-chart--scatter", className), style: { ...chromeStyle, ...layoutStyle }, children: [_jsxs("div", { className: "hjm-chart__plot", role: "slider", tabIndex: 0, "aria-label": accessibilityLabel, "aria-describedby": descriptionId, "aria-valuemin": 0, "aria-valuemax": Math.max(0, points.length - 1), "aria-valuenow": Math.max(0, position), "aria-valuetext": focusPoint ? describe(focusPoint) : accessibilityLabel, style: { blockSize: height }, onKeyDown: onKeyDown, ...handlers, children: [_jsxs("svg", { width: width, height: height, viewBox: `0 0 ${width} ${height}`, "aria-hidden": "true", focusable: "false", children: [g.yTicks.map((tick) => (_jsxs("g", { children: [_jsx("line", { className: "hjm-chart__grid", x1: g.plot.left, x2: g.plot.right, y1: sy(tick), y2: sy(tick) }), _jsx("text", { className: "hjm-chart__label", x: g.plot.left - 6, y: sy(tick), dominantBaseline: "middle", textAnchor: "end", children: formatY(tick) })] }, `y${tick}`))), g.xTicks.map((tick) => _jsx("text", { className: "hjm-chart__label", x: sx(tick), y: height - 4, textAnchor: "middle", children: formatX(tick) }, `x${tick}`)), g.points.map((p) => {
|
|
451
|
+
const isFocused = p.id === focused;
|
|
452
|
+
return (_jsx("circle", { className: classNames("hjm-chart__point", isFocused && "hjm-chart__dot"), "data-id": p.id, cx: p.px, cy: p.py, r: isFocused ? chartRecipe.scatter.focusRadius : chartRecipe.scatter.radius, fill: colorOf({}, p.seriesIndex ?? 0), opacity: focused !== null && !isFocused ? 0.45 : 0.9, strokeWidth: isFocused ? 2 : 0 }, p.id));
|
|
453
|
+
})] }), focusPoint ? _jsx(CursorBubble, { x: focusPoint.px, width: width, text: `${focusPoint.label} · ${formatX(focusPoint.x)} / ${formatY(focusPoint.y)}` }) : null] }), _jsxs("div", { className: "hjm-chart__axis-titles", "aria-hidden": "true", children: [_jsxs("span", { children: [yLabel, " \u2191"] }), _jsxs("span", { children: [xLabel, " \u2192"] })] }), seriesLabels && seriesLabels.length > 1 ? _jsx(ChartLegend, { items: seriesLabels.map((label, i) => ({ id: String(i), label, color: colorOf({}, i) })) }) : null, _jsx("p", { id: descriptionId, className: "hjm-visually-hidden", children: points.map(describe).join(". ") })] }));
|
|
454
|
+
}
|
|
455
|
+
export function Sparkline({ values, color, invert = false, domain, width = chartRecipe.sparkline.width, height = chartRecipe.sparkline.height, accessibilityLabel, className, layoutStyle, }) {
|
|
456
|
+
const colorOf = useSeriesColor(), id = useSvgId();
|
|
457
|
+
const stroke = color ?? colorOf({}, 0);
|
|
458
|
+
const pad = chartRecipe.sparkline.padding;
|
|
459
|
+
const g = resolveLineGeometry({
|
|
460
|
+
categories: values.map((_, i) => String(i)), series: [{ id: "s", label: "s", values }], width, height,
|
|
461
|
+
insets: { top: pad, right: pad, bottom: pad, left: pad }, invert, ...(domain ? { domain } : {}),
|
|
462
|
+
});
|
|
463
|
+
const segments = splitSegments(g.plots[0]);
|
|
464
|
+
const labelled = accessibilityLabel !== undefined && accessibilityLabel.trim().length > 0;
|
|
465
|
+
return (_jsxs("svg", { className: classNames("hjm-sparkline", className), style: layoutStyle, width: width, height: height, viewBox: `0 0 ${width} ${height}`, focusable: "false", ...(labelled ? { role: "img", "aria-label": accessibilityLabel } : { "aria-hidden": true }), children: [_jsx("defs", { children: _jsxs("linearGradient", { id: `${id}-spark`, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0", stopColor: stroke, stopOpacity: 0.28 }), _jsx("stop", { offset: "1", stopColor: stroke, stopOpacity: 0 })] }) }), segments.map((segment, i) => _jsx("path", { d: areaPath(segment, height), fill: `url(#${id}-spark)` }, `a${i}`)), segments.map((segment, i) => _jsx("path", { className: "hjm-chart__line", d: linePath(segment), stroke: stroke, strokeWidth: chartRecipe.sparkline.strokeWidth, fill: "none", strokeLinejoin: "round", strokeLinecap: "round" }, `l${i}`))] }));
|
|
466
|
+
}
|
|
467
|
+
//# sourceMappingURL=chart.js.map
|