@x12i/charts 1.0.0 → 1.1.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/react.js ADDED
@@ -0,0 +1,1271 @@
1
+ "use client";
2
+ import {
3
+ AXIS_FONT_SIZE,
4
+ TICK_PADDING,
5
+ TICK_SIZE,
6
+ buildChartModel,
7
+ categoricalTones,
8
+ chartToCsv,
9
+ chartToTable,
10
+ createFormatters,
11
+ dashArrays,
12
+ describeModel,
13
+ describeTarget,
14
+ divergingColor,
15
+ formatMessage,
16
+ hitTest,
17
+ isNavKey,
18
+ isOpenMarker,
19
+ isRtlLocale,
20
+ layoutChart,
21
+ markerPath,
22
+ navigate,
23
+ pointInfo,
24
+ rampGradient,
25
+ resolveMessages,
26
+ resolveRenderer,
27
+ sequentialColor,
28
+ targetById,
29
+ themeColorsFromTokens,
30
+ toEChartsOption,
31
+ toneVarName,
32
+ tooltipFor
33
+ } from "./chunk-U3U6ZV7A.js";
34
+
35
+ // src/react/chart.tsx
36
+ import {
37
+ useCallback as useCallback3,
38
+ useId,
39
+ useMemo as useMemo4,
40
+ useRef as useRef4,
41
+ useState as useState4
42
+ } from "react";
43
+ import { Badge } from "@x12i/ui";
44
+
45
+ // src/react/config.tsx
46
+ import { createContext, useContext, useMemo } from "react";
47
+ import { jsx } from "react/jsx-runtime";
48
+ var ChartConfigContext = createContext({});
49
+ function mergeMessages(base, override) {
50
+ if (!base) return override;
51
+ if (!override) return base;
52
+ return { ...base, ...override, markNames: { ...base.markNames, ...override.markNames } };
53
+ }
54
+ function ChartConfigProvider({ children, locale, messages, direction, datasets, loadEcharts, target }) {
55
+ const parent = useContext(ChartConfigContext);
56
+ const value = useMemo(
57
+ () => ({
58
+ locale: locale ?? parent.locale,
59
+ direction: direction ?? parent.direction,
60
+ datasets: datasets ?? parent.datasets,
61
+ loadEcharts: loadEcharts ?? parent.loadEcharts,
62
+ target: target ?? parent.target,
63
+ messages: mergeMessages(parent.messages, messages)
64
+ }),
65
+ [parent, locale, messages, direction, datasets, loadEcharts, target]
66
+ );
67
+ return /* @__PURE__ */ jsx(ChartConfigContext.Provider, { value, children });
68
+ }
69
+ function useChartConfig() {
70
+ return useContext(ChartConfigContext);
71
+ }
72
+
73
+ // src/react/echarts-plot.tsx
74
+ import { useEffect, useRef, useState } from "react";
75
+ import { VisuallyHidden } from "@x12i/ui";
76
+
77
+ // src/react/states.tsx
78
+ import { Button, Spinner } from "@x12i/ui";
79
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
80
+ function ChartState({ kind, message, height, onRetry, retryLabel, overlay }) {
81
+ return /* @__PURE__ */ jsxs(
82
+ "div",
83
+ {
84
+ "data-kit": "chart-state",
85
+ "data-state": kind,
86
+ "data-overlay": overlay ? "true" : void 0,
87
+ role: kind === "error" ? "alert" : "status",
88
+ "aria-busy": kind === "loading" ? true : void 0,
89
+ style: { minBlockSize: height },
90
+ children: [
91
+ kind === "loading" ? /* @__PURE__ */ jsx2(Spinner, { size: "md" }) : null,
92
+ /* @__PURE__ */ jsx2("p", { "data-kit-slot": "state-message", children: message }),
93
+ kind === "error" && onRetry ? /* @__PURE__ */ jsx2(Button, { size: "sm", variant: "outline", tone: "neutral", onClick: onRetry, children: retryLabel }) : null
94
+ ]
95
+ }
96
+ );
97
+ }
98
+
99
+ // src/react/theme-colors.ts
100
+ import { cssVarName, themeNames } from "@x12i/tokens";
101
+ var SEQUENTIAL_STEPS = 9;
102
+ function themeAt(element) {
103
+ const attribute = element?.closest("[data-theme]")?.getAttribute("data-theme") ?? (typeof document === "undefined" ? null : document.documentElement.getAttribute("data-theme"));
104
+ return themeNames.includes(attribute ?? "") ? attribute : "light";
105
+ }
106
+ var toneNames = ["accent", "muted", "reference", "good", "warning", "critical", ...categoricalTones];
107
+ function readThemeColors(element) {
108
+ const fallback = themeColorsFromTokens(themeAt(element));
109
+ if (!element || typeof getComputedStyle !== "function") return fallback;
110
+ const style = getComputedStyle(element);
111
+ const read = (name, backup) => style.getPropertyValue(name).trim() || backup;
112
+ const tones = { ...fallback.tones };
113
+ for (const tone of toneNames) tones[tone] = read(toneVarName(tone), fallback.tones[tone]);
114
+ const ramp = (kind, backup) => Array.from(
115
+ { length: SEQUENTIAL_STEPS },
116
+ (_, i) => read(cssVarName(`color.${kind}.${i + 1}`), backup[i] ?? "")
117
+ );
118
+ return {
119
+ tones,
120
+ text: read(cssVarName("color.text.primary"), fallback.text),
121
+ textSecondary: read(cssVarName("color.text.secondary"), fallback.textSecondary),
122
+ axis: read(cssVarName("color.chart.axis"), fallback.axis),
123
+ grid: read(cssVarName("color.chart.grid"), fallback.grid),
124
+ surface: read(cssVarName("color.bg.surface"), fallback.surface),
125
+ border: read(cssVarName("color.border.default"), fallback.border),
126
+ accent: read(cssVarName("color.accent.default"), fallback.accent),
127
+ sequential: ramp("sequential", fallback.sequential),
128
+ diverging: ramp("diverging", fallback.diverging),
129
+ fontFamily: read(cssVarName("font.family.sans"), fallback.fontFamily)
130
+ };
131
+ }
132
+
133
+ // src/react/echarts-plot.tsx
134
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
135
+ function EChartsPlot({
136
+ model,
137
+ messageInput,
138
+ messages,
139
+ locale,
140
+ hidden,
141
+ width,
142
+ height,
143
+ loader,
144
+ animate,
145
+ name,
146
+ description,
147
+ idPrefix,
148
+ onUnavailable
149
+ }) {
150
+ const hostRef = useRef(null);
151
+ const instanceRef = useRef(null);
152
+ const loaderRef = useRef(loader);
153
+ const unavailableRef = useRef(onUnavailable);
154
+ const [status, setStatus] = useState("loading");
155
+ const [themeVersion, setThemeVersion] = useState(0);
156
+ useEffect(() => {
157
+ loaderRef.current = loader;
158
+ unavailableRef.current = onUnavailable;
159
+ });
160
+ useEffect(() => {
161
+ let cancelled = false;
162
+ const host = hostRef.current;
163
+ loaderRef.current().then((module) => {
164
+ if (cancelled || !host) return;
165
+ instanceRef.current = module.init(host, null, { renderer: "canvas" });
166
+ setStatus("ready");
167
+ }).catch(() => {
168
+ if (cancelled) return;
169
+ setStatus("unavailable");
170
+ unavailableRef.current("missing");
171
+ });
172
+ return () => {
173
+ cancelled = true;
174
+ instanceRef.current?.dispose();
175
+ instanceRef.current = null;
176
+ };
177
+ }, []);
178
+ useEffect(() => {
179
+ const host = hostRef.current;
180
+ if (!host || typeof MutationObserver === "undefined") return void 0;
181
+ const observer = new MutationObserver(() => setThemeVersion((version) => version + 1));
182
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
183
+ const scoped = host.closest("[data-theme]");
184
+ if (scoped && scoped !== document.documentElement) observer.observe(scoped, { attributes: true, attributeFilter: ["data-theme"] });
185
+ return () => observer.disconnect();
186
+ }, []);
187
+ useEffect(() => {
188
+ const instance = instanceRef.current;
189
+ if (status !== "ready" || !instance) return;
190
+ const option = toEChartsOption(model, {
191
+ colors: readThemeColors(hostRef.current),
192
+ locale,
193
+ messages: messageInput,
194
+ hidden,
195
+ animate
196
+ });
197
+ if (!option) {
198
+ unavailableRef.current("unsupported");
199
+ return;
200
+ }
201
+ instance.setOption(option, { notMerge: true });
202
+ }, [status, model, hidden, themeVersion, locale, messageInput, animate]);
203
+ useEffect(() => {
204
+ const instance = instanceRef.current;
205
+ if (status !== "ready" || !instance) return;
206
+ instance.resize({ ...width !== void 0 ? { width } : {}, height });
207
+ }, [status, width, height]);
208
+ const descriptionId = `${idPrefix}-description`;
209
+ return /* @__PURE__ */ jsxs2("div", { "data-kit": "chart-stage", "data-canvas": status, children: [
210
+ /* @__PURE__ */ jsx3(
211
+ "div",
212
+ {
213
+ ref: hostRef,
214
+ "data-kit": "chart-canvas",
215
+ role: "img",
216
+ "aria-label": name,
217
+ "aria-describedby": descriptionId,
218
+ style: { blockSize: height }
219
+ }
220
+ ),
221
+ status === "loading" ? /* @__PURE__ */ jsx3(ChartState, { kind: "loading", message: messages.canvasLoading, height, overlay: true }) : null,
222
+ /* @__PURE__ */ jsx3(VisuallyHidden, { id: descriptionId, children: description })
223
+ ] });
224
+ }
225
+
226
+ // src/react/echarts-loader.ts
227
+ var defaultEChartsLoader = () => import("echarts");
228
+
229
+ // src/react/hooks.ts
230
+ import { useCallback, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
231
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect2 : useLayoutEffect;
232
+ function useElementWidth(ref, explicit) {
233
+ const [width, setWidth] = useState2(void 0);
234
+ useIsomorphicLayoutEffect(() => {
235
+ const element = ref.current;
236
+ if (!element || explicit !== void 0) return void 0;
237
+ const read = () => {
238
+ const next = Math.round(element.getBoundingClientRect().width);
239
+ if (next > 0) setWidth((previous) => previous === next ? previous : next);
240
+ };
241
+ read();
242
+ if (typeof ResizeObserver === "undefined") return void 0;
243
+ const observer = new ResizeObserver(read);
244
+ observer.observe(element);
245
+ return () => observer.disconnect();
246
+ }, [ref, explicit]);
247
+ return explicit ?? width;
248
+ }
249
+ function directionAttribute(element) {
250
+ const value = element?.getAttribute("dir");
251
+ return value === "rtl" || value === "ltr" ? value : void 0;
252
+ }
253
+ function useDirection(ref, explicit, locale) {
254
+ const [fromPage, setFromPage] = useState2(void 0);
255
+ useIsomorphicLayoutEffect(() => {
256
+ if (explicit || typeof document === "undefined") return void 0;
257
+ const element = ref.current;
258
+ const read = () => {
259
+ const host2 = element?.parentElement?.closest("[dir]") ?? null;
260
+ setFromPage(directionAttribute(host2) ?? directionAttribute(document.documentElement));
261
+ };
262
+ read();
263
+ if (typeof MutationObserver === "undefined") return void 0;
264
+ const observer = new MutationObserver(read);
265
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["dir"] });
266
+ const host = element?.parentElement?.closest("[dir]");
267
+ if (host && host !== document.documentElement) observer.observe(host, { attributes: true, attributeFilter: ["dir"] });
268
+ return () => observer.disconnect();
269
+ }, [ref, explicit]);
270
+ const fromLocale = isRtlLocale(locale) ? "rtl" : void 0;
271
+ const direction = explicit ?? fromPage ?? fromLocale ?? "ltr";
272
+ const attribute = explicit ?? (fromPage ? void 0 : fromLocale);
273
+ return { direction, attribute };
274
+ }
275
+ function usePrefersReducedMotion() {
276
+ const [reduced, setReduced] = useState2(false);
277
+ useEffect2(() => {
278
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
279
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
280
+ const update = () => setReduced(query.matches);
281
+ update();
282
+ query.addEventListener?.("change", update);
283
+ return () => query.removeEventListener?.("change", update);
284
+ }, []);
285
+ return reduced;
286
+ }
287
+ function useHiddenIds(controlled, initial, onChange) {
288
+ const [internal, setInternal] = useState2(() => [...initial]);
289
+ const ids = controlled ?? internal;
290
+ const set = useMemo2(() => new Set(ids), [ids]);
291
+ const onChangeRef = useRef2(onChange);
292
+ useIsomorphicLayoutEffect(() => {
293
+ onChangeRef.current = onChange;
294
+ });
295
+ const update = useCallback(
296
+ (next) => {
297
+ if (controlled === void 0) setInternal(next);
298
+ onChangeRef.current?.(next);
299
+ },
300
+ [controlled]
301
+ );
302
+ return [set, update];
303
+ }
304
+ function safeId(id) {
305
+ return id.replace(/[^a-zA-Z0-9_-]/g, "");
306
+ }
307
+
308
+ // src/react/glyph.tsx
309
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
310
+ var GLYPH_WIDTH = 20;
311
+ var GLYPH_HEIGHT = 12;
312
+ var SWATCH_WIDTH = 14;
313
+ function PatternDefs({ prefix, indices }) {
314
+ if (indices.length === 0) return null;
315
+ const line = (d) => /* @__PURE__ */ jsx4("path", { d, "data-kit": "chart-pattern-line" });
316
+ const tiles = {
317
+ 0: { size: 6, body: line("M-1,1L1,-1M0,6L6,0M5,7L7,5") },
318
+ 1: { size: 6, body: line("M-1,5L1,7M0,0L6,6M5,-1L7,1") },
319
+ 2: { size: 6, body: /* @__PURE__ */ jsx4("circle", { cx: 3, cy: 3, r: 1, "data-kit": "chart-pattern-dot" }) },
320
+ 3: { size: 6, body: line("M0,3H6") },
321
+ 4: { size: 6, body: line("M3,0V6") },
322
+ 5: { size: 6, body: line("M0,3H6M3,0V6") },
323
+ 6: { size: 8, body: line("M-1,1L1,-1M0,8L8,0M7,9L9,7") },
324
+ 7: {
325
+ size: 6,
326
+ body: /* @__PURE__ */ jsxs3(Fragment, { children: [
327
+ /* @__PURE__ */ jsx4("rect", { x: 0, y: 0, width: 3, height: 3, "data-kit": "chart-pattern-dot" }),
328
+ /* @__PURE__ */ jsx4("rect", { x: 3, y: 3, width: 3, height: 3, "data-kit": "chart-pattern-dot" })
329
+ ] })
330
+ }
331
+ };
332
+ return /* @__PURE__ */ jsx4("defs", { children: indices.map((index) => {
333
+ const tile = tiles[index % 8];
334
+ return /* @__PURE__ */ jsx4("pattern", { id: `${prefix}-p${index}`, patternUnits: "userSpaceOnUse", width: tile.size, height: tile.size, children: tile.body }, index);
335
+ }) });
336
+ }
337
+ function Glyph({ tone, glyph, marker, dash, pattern, patternPrefix }) {
338
+ const middle = GLYPH_HEIGHT / 2;
339
+ const shape = marker;
340
+ const open = isOpenMarker(shape);
341
+ const overlay = (x, y, width, height) => pattern >= 0 ? /* @__PURE__ */ jsx4("rect", { x, y, width, height, fill: `url(#${patternPrefix}-p${pattern})`, "data-kit": "chart-pattern" }) : null;
342
+ return /* @__PURE__ */ jsxs3(
343
+ "svg",
344
+ {
345
+ viewBox: `0 0 ${GLYPH_WIDTH} ${GLYPH_HEIGHT}`,
346
+ width: GLYPH_WIDTH,
347
+ height: GLYPH_HEIGHT,
348
+ "aria-hidden": "true",
349
+ focusable: "false",
350
+ "data-kit": "chart-glyph",
351
+ "data-glyph": glyph,
352
+ "data-tone": tone,
353
+ children: [
354
+ glyph === "line" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
355
+ /* @__PURE__ */ jsx4("path", { d: `M0,${middle}H${GLYPH_WIDTH}`, "data-kit": "chart-glyph-line", ...dash ? { strokeDasharray: dash } : {} }),
356
+ /* @__PURE__ */ jsx4("path", { d: markerPath(shape, GLYPH_WIDTH / 2, middle, 3.5), "data-kit": "chart-glyph-marker", "data-open": open ? "true" : void 0 })
357
+ ] }) : null,
358
+ glyph === "area" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
359
+ /* @__PURE__ */ jsx4("rect", { x: 0, y: 2, width: GLYPH_WIDTH, height: GLYPH_HEIGHT - 2, "data-kit": "chart-glyph-area" }),
360
+ overlay(0, 2, GLYPH_WIDTH, GLYPH_HEIGHT - 2),
361
+ /* @__PURE__ */ jsx4("path", { d: `M0,2H${GLYPH_WIDTH}`, "data-kit": "chart-glyph-line" })
362
+ ] }) : null,
363
+ glyph === "bar" || glyph === "slice" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
364
+ /* @__PURE__ */ jsx4(
365
+ "rect",
366
+ {
367
+ x: (GLYPH_WIDTH - SWATCH_WIDTH) / 2,
368
+ y: 1,
369
+ width: SWATCH_WIDTH,
370
+ height: GLYPH_HEIGHT - 2,
371
+ "data-kit": "chart-glyph-fill"
372
+ }
373
+ ),
374
+ overlay((GLYPH_WIDTH - SWATCH_WIDTH) / 2, 1, SWATCH_WIDTH, GLYPH_HEIGHT - 2)
375
+ ] }) : null,
376
+ glyph === "point" ? /* @__PURE__ */ jsx4("path", { d: markerPath(shape, GLYPH_WIDTH / 2, middle, 4), "data-kit": "chart-glyph-marker", "data-open": open ? "true" : void 0 }) : null
377
+ ]
378
+ }
379
+ );
380
+ }
381
+
382
+ // src/react/legend.tsx
383
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
384
+ function ChartLegend({ entries, label, position, patternPrefix, onToggle }) {
385
+ if (entries.length === 0) return null;
386
+ return /* @__PURE__ */ jsx5("div", { role: "group", "aria-label": label, "data-kit": "chart-legend", "data-position": position, children: /* @__PURE__ */ jsx5("ul", { "data-kit-slot": "legend-list", children: entries.map((entry) => {
387
+ const glyph = /* @__PURE__ */ jsx5(
388
+ Glyph,
389
+ {
390
+ tone: entry.tone,
391
+ glyph: entry.glyph,
392
+ marker: entry.marker,
393
+ dash: entry.dash,
394
+ pattern: entry.pattern,
395
+ patternPrefix
396
+ }
397
+ );
398
+ return /* @__PURE__ */ jsx5("li", { "data-kit-slot": "legend-item", children: entry.toggle ? /* @__PURE__ */ jsxs4(
399
+ "button",
400
+ {
401
+ type: "button",
402
+ "data-kit": "chart-legend-button",
403
+ "data-hidden": entry.visible ? void 0 : "true",
404
+ "aria-pressed": entry.visible,
405
+ onClick: (event) => onToggle?.(entry, event),
406
+ children: [
407
+ glyph,
408
+ /* @__PURE__ */ jsx5("span", { "data-kit-slot": "legend-label", children: entry.label })
409
+ ]
410
+ }
411
+ ) : /* @__PURE__ */ jsxs4("span", { "data-kit": "chart-legend-key", children: [
412
+ glyph,
413
+ /* @__PURE__ */ jsx5("span", { "data-kit-slot": "legend-label", children: entry.label })
414
+ ] }) }, entry.id);
415
+ }) }) });
416
+ }
417
+ function ColorScaleLegend({ kind, min, max, label }) {
418
+ return /* @__PURE__ */ jsxs4("div", { role: "group", "aria-label": label, "data-kit": "chart-scale", "data-ramp": kind, children: [
419
+ /* @__PURE__ */ jsx5("span", { "data-kit-slot": "scale-min", children: min }),
420
+ /* @__PURE__ */ jsx5("span", { "data-kit-slot": "scale-ramp", "aria-hidden": "true", style: { backgroundImage: rampGradient(kind) } }),
421
+ /* @__PURE__ */ jsx5("span", { "data-kit-slot": "scale-max", children: max })
422
+ ] });
423
+ }
424
+
425
+ // src/react/plot.tsx
426
+ import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
427
+ import { VisuallyHidden as VisuallyHidden2 } from "@x12i/ui";
428
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
429
+ var RTL_SCRIPT = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/;
430
+ var ABOVE_OFFSET = -3;
431
+ var BELOW_OFFSET = 11;
432
+ var MIDDLE_OFFSET = 4;
433
+ var FOCUS_OUTSET = 2;
434
+ function textDirection(text, numeric) {
435
+ const rtl = !numeric && RTL_SCRIPT.test(text);
436
+ return { direction: rtl ? "rtl" : "ltr", flip: rtl };
437
+ }
438
+ function physicalAnchor(anchor, flip) {
439
+ if (!flip) return anchor;
440
+ return anchor === "start" ? "end" : anchor === "end" ? "start" : anchor;
441
+ }
442
+ function offsetFor(baseline) {
443
+ return baseline === "above" ? ABOVE_OFFSET : baseline === "below" ? BELOW_OFFSET : MIDDLE_OFFSET;
444
+ }
445
+ function Label({ label }) {
446
+ const { direction, flip } = textDirection(label.text, label.numeric);
447
+ return /* @__PURE__ */ jsx6(
448
+ "text",
449
+ {
450
+ x: label.x,
451
+ y: label.y + offsetFor(label.baseline),
452
+ textAnchor: physicalAnchor(label.anchor, flip),
453
+ direction,
454
+ "data-kit": "chart-label",
455
+ "data-label-kind": label.kind,
456
+ "data-tone": label.tone,
457
+ children: label.text
458
+ }
459
+ );
460
+ }
461
+ function Axis({ axis, plot }) {
462
+ const { line, orient } = axis;
463
+ const vertical = orient === "left" || orient === "right";
464
+ const sign = orient === "left" || orient === "top" ? -1 : 1;
465
+ const visibleTicks = axis.ticks.filter((tick) => tick.visible);
466
+ return /* @__PURE__ */ jsxs5("g", { "data-kit": "chart-axis", "data-axis": axis.id, "data-orient": orient, children: [
467
+ axis.id === "x" ? /* @__PURE__ */ jsx6("line", { x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2, "data-kit": "chart-axis-line" }) : null,
468
+ visibleTicks.map((tick) => {
469
+ const tickLine = vertical ? { x1: line.x1, y1: tick.position, x2: line.x1 + sign * TICK_SIZE, y2: tick.position } : { x1: tick.position, y1: line.y1, x2: tick.position, y2: line.y1 + sign * TICK_SIZE };
470
+ const gap = TICK_SIZE + TICK_PADDING;
471
+ let x;
472
+ let y;
473
+ if (vertical) {
474
+ x = line.x1 + sign * gap;
475
+ y = tick.position + MIDDLE_OFFSET;
476
+ } else {
477
+ x = tick.position;
478
+ y = line.y1 + sign * gap + (sign > 0 ? axis.labelAngle === 0 ? AXIS_FONT_SIZE * 0.8 : AXIS_FONT_SIZE * 0.35 : 0);
479
+ }
480
+ const text = String(tick.label);
481
+ const { direction, flip } = textDirection(text, axis.numeric);
482
+ const anchor = physicalAnchor(axis.labelAnchor, flip);
483
+ return /* @__PURE__ */ jsxs5("g", { "data-kit": "chart-tick", children: [
484
+ axis.grid ? vertical ? /* @__PURE__ */ jsx6(
485
+ "line",
486
+ {
487
+ x1: plot.x,
488
+ y1: tick.position,
489
+ x2: plot.x + plot.width,
490
+ y2: tick.position,
491
+ "data-kit": "chart-grid",
492
+ "data-zero": tick.value === 0 ? "true" : void 0
493
+ }
494
+ ) : /* @__PURE__ */ jsx6(
495
+ "line",
496
+ {
497
+ x1: tick.position,
498
+ y1: plot.y,
499
+ x2: tick.position,
500
+ y2: plot.y + plot.height,
501
+ "data-kit": "chart-grid",
502
+ "data-zero": tick.value === 0 ? "true" : void 0
503
+ }
504
+ ) : null,
505
+ /* @__PURE__ */ jsx6("line", { ...tickLine, "data-kit": "chart-tick-mark" }),
506
+ /* @__PURE__ */ jsx6(
507
+ "text",
508
+ {
509
+ x,
510
+ y,
511
+ textAnchor: anchor,
512
+ direction,
513
+ "data-kit": "chart-tick-label",
514
+ ...axis.labelAngle !== 0 && !vertical ? { transform: `rotate(${axis.labelAngle} ${x} ${y})` } : {},
515
+ children: text
516
+ }
517
+ )
518
+ ] }, String(tick.value));
519
+ }),
520
+ axis.title && axis.titlePosition ? /* @__PURE__ */ jsx6(
521
+ "text",
522
+ {
523
+ x: axis.titlePosition.x,
524
+ y: axis.titlePosition.y,
525
+ textAnchor: "middle",
526
+ direction: textDirection(axis.title, false).direction,
527
+ "data-kit": "chart-axis-title",
528
+ ...axis.titlePosition.rotate !== 0 ? { transform: `rotate(${axis.titlePosition.rotate} ${axis.titlePosition.x} ${axis.titlePosition.y})` } : {},
529
+ children: axis.title
530
+ }
531
+ ) : null
532
+ ] });
533
+ }
534
+ var KIND_ORDER = {
535
+ area: 0,
536
+ bar: 1,
537
+ cell: 1,
538
+ arc: 1,
539
+ connector: 2,
540
+ stem: 2,
541
+ line: 3,
542
+ point: 4
543
+ };
544
+ function cellFill(mark) {
545
+ const t = mark.t ?? 0;
546
+ return mark.ramp === "diverging" ? divergingColor(t) : sequentialColor(t);
547
+ }
548
+ function MarkView({ mark, active, patternPrefix }) {
549
+ const common = {
550
+ "data-tone": mark.tone,
551
+ "data-series": mark.styleIndex,
552
+ "data-muted": mark.muted ? "true" : void 0,
553
+ "data-highlighted": mark.highlighted ? "true" : void 0,
554
+ "data-breach": mark.breach ? "true" : void 0,
555
+ "data-active": active ? "true" : void 0,
556
+ "data-role": mark.role,
557
+ "data-mark-id": mark.id
558
+ };
559
+ const overlayFill = mark.pattern >= 0 ? `url(#${patternPrefix}-p${mark.pattern})` : void 0;
560
+ switch (mark.kind) {
561
+ case "bar":
562
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
563
+ /* @__PURE__ */ jsx6("rect", { x: mark.x, y: mark.y, width: mark.width, height: mark.height, "data-kit": "chart-bar", ...common }),
564
+ overlayFill ? /* @__PURE__ */ jsx6("rect", { x: mark.x, y: mark.y, width: mark.width, height: mark.height, fill: overlayFill, "data-kit": "chart-pattern" }) : null
565
+ ] });
566
+ case "cell":
567
+ return /* @__PURE__ */ jsx6(
568
+ "rect",
569
+ {
570
+ x: mark.x,
571
+ y: mark.y,
572
+ width: mark.width,
573
+ height: mark.height,
574
+ style: { fill: cellFill(mark) },
575
+ "data-kit": "chart-cell",
576
+ ...common
577
+ }
578
+ );
579
+ case "area":
580
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
581
+ /* @__PURE__ */ jsx6("path", { d: mark.d, "data-kit": "chart-area", ...common }),
582
+ overlayFill ? /* @__PURE__ */ jsx6("path", { d: mark.d, fill: overlayFill, "data-kit": "chart-pattern" }) : null
583
+ ] });
584
+ case "arc":
585
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
586
+ /* @__PURE__ */ jsx6("path", { d: mark.d, "data-kit": "chart-arc", ...common }),
587
+ overlayFill ? /* @__PURE__ */ jsx6("path", { d: mark.d, fill: overlayFill, "data-kit": "chart-pattern" }) : null
588
+ ] });
589
+ case "line":
590
+ return /* @__PURE__ */ jsx6("path", { d: mark.d, "data-kit": "chart-line", ...mark.dash ? { strokeDasharray: mark.dash } : {}, ...common });
591
+ case "stem":
592
+ case "connector":
593
+ return /* @__PURE__ */ jsx6(
594
+ "line",
595
+ {
596
+ x1: mark.x1,
597
+ y1: mark.y1,
598
+ x2: mark.x2,
599
+ y2: mark.y2,
600
+ "data-kit": mark.kind === "stem" ? "chart-stem" : "chart-connector",
601
+ ...common
602
+ }
603
+ );
604
+ case "point": {
605
+ const shape = mark.shape ?? "circle";
606
+ const open = isOpenMarker(shape) ? "true" : void 0;
607
+ if (shape === "circle") {
608
+ return /* @__PURE__ */ jsx6("circle", { cx: mark.cx, cy: mark.cy, r: mark.r, "data-kit": "chart-point", ...common });
609
+ }
610
+ return /* @__PURE__ */ jsx6("path", { d: markerPath(shape, mark.cx ?? 0, mark.cy ?? 0, mark.r ?? 4), "data-kit": "chart-point", "data-open": open, ...common });
611
+ }
612
+ default:
613
+ return null;
614
+ }
615
+ }
616
+ function AnnotationView({ annotation }) {
617
+ const common = {
618
+ "data-tone": annotation.tone,
619
+ "data-threshold": annotation.threshold ? "true" : void 0
620
+ };
621
+ return /* @__PURE__ */ jsxs5("g", { "data-kit": "chart-annotation", "data-annotation": annotation.kind, ...common, children: [
622
+ annotation.kind === "line" ? /* @__PURE__ */ jsx6("line", { x1: annotation.x1, y1: annotation.y1, x2: annotation.x2, y2: annotation.y2, "data-kit": "chart-annotation-line" }) : null,
623
+ annotation.kind === "band" || annotation.kind === "forecast" ? /* @__PURE__ */ jsx6(
624
+ "rect",
625
+ {
626
+ x: annotation.box?.x,
627
+ y: annotation.box?.y,
628
+ width: annotation.box?.width,
629
+ height: annotation.box?.height,
630
+ "data-kit": "chart-annotation-band",
631
+ "data-forecast": annotation.kind === "forecast" ? "true" : void 0
632
+ }
633
+ ) : null,
634
+ annotation.kind === "point" ? /* @__PURE__ */ jsx6("circle", { cx: annotation.cx, cy: annotation.cy, r: 5, "data-kit": "chart-annotation-point" }) : null,
635
+ annotation.label ? /* @__PURE__ */ jsx6(Label, { label: annotation.label }) : null
636
+ ] });
637
+ }
638
+ function FocusRing({ shape }) {
639
+ if (shape.kind === "circle") {
640
+ return /* @__PURE__ */ jsx6("circle", { cx: shape.cx, cy: shape.cy, r: shape.r, "data-kit": "chart-focus" });
641
+ }
642
+ if (shape.kind === "rect") {
643
+ return /* @__PURE__ */ jsx6(
644
+ "rect",
645
+ {
646
+ x: shape.x - FOCUS_OUTSET,
647
+ y: shape.y - FOCUS_OUTSET,
648
+ width: shape.width + FOCUS_OUTSET * 2,
649
+ height: shape.height + FOCUS_OUTSET * 2,
650
+ "data-kit": "chart-focus"
651
+ }
652
+ );
653
+ }
654
+ return /* @__PURE__ */ jsx6("path", { d: shape.d, transform: `translate(${shape.tx ?? 0} ${shape.ty ?? 0})`, "data-kit": "chart-focus" });
655
+ }
656
+ var EDGE_SHARE = 0.25;
657
+ function Tooltip({
658
+ content,
659
+ target,
660
+ layout,
661
+ patternPrefix
662
+ }) {
663
+ const left = target.x / layout.width * 100;
664
+ const top = target.y / layout.height * 100;
665
+ const align = left < EDGE_SHARE * 100 ? "start" : left > (1 - EDGE_SHARE) * 100 ? "end" : "center";
666
+ const placement = top < 40 ? "below" : "above";
667
+ return /* @__PURE__ */ jsxs5(
668
+ "div",
669
+ {
670
+ "data-kit": "chart-tooltip",
671
+ "data-placement": placement,
672
+ "data-align": align,
673
+ "aria-hidden": "true",
674
+ style: { left: `${left}%`, top: `${top}%` },
675
+ children: [
676
+ /* @__PURE__ */ jsx6("div", { "data-kit-slot": "tooltip-title", children: content.title }),
677
+ /* @__PURE__ */ jsx6("ul", { "data-kit-slot": "tooltip-rows", children: content.rows.map((row) => /* @__PURE__ */ jsxs5("li", { "data-kit-slot": "tooltip-row", "data-active": row.active ? "true" : void 0, children: [
678
+ /* @__PURE__ */ jsx6(Glyph, { tone: row.tone, glyph: row.glyph, marker: row.marker, dash: row.dash, pattern: row.pattern, patternPrefix }),
679
+ /* @__PURE__ */ jsx6("span", { "data-kit-slot": "tooltip-label", children: row.label }),
680
+ /* @__PURE__ */ jsx6("span", { "data-kit-slot": "tooltip-value", children: row.value }),
681
+ row.note ? /* @__PURE__ */ jsx6("span", { "data-kit-slot": "tooltip-note", children: row.note }) : null
682
+ ] }, row.id)) })
683
+ ]
684
+ }
685
+ );
686
+ }
687
+ function Plot({
688
+ model,
689
+ layout,
690
+ fmts,
691
+ name,
692
+ description,
693
+ idPrefix,
694
+ interactive,
695
+ tooltip,
696
+ animate,
697
+ onPointActivate,
698
+ onActivePointChange
699
+ }) {
700
+ const svgRef = useRef3(null);
701
+ const pointerDown = useRef3(false);
702
+ const [active, setActive] = useState3(null);
703
+ const [announcement, setAnnouncement] = useState3("");
704
+ const messages = fmts.messages;
705
+ const canInteract = interactive && layout.targets.length > 0;
706
+ const descriptionId = `${idPrefix}-description`;
707
+ const patternPrefix = `${idPrefix}-pattern`;
708
+ const target = active ? targetById(layout, active.id) : void 0;
709
+ const content = useMemo3(
710
+ () => target && tooltip ? tooltipFor(model, layout, target, fmts) : null,
711
+ [model, layout, fmts, target, tooltip]
712
+ );
713
+ const lastNotified = useRef3(null);
714
+ const select = useCallback2(
715
+ (next, source) => {
716
+ const nextId = next?.id ?? null;
717
+ if (!next) {
718
+ setActive(null);
719
+ setAnnouncement("");
720
+ } else {
721
+ setActive((previous) => previous && previous.id === next.id && previous.source === source ? previous : { id: next.id, source });
722
+ if (source === "keyboard") setAnnouncement(describeTarget(model, layout, next, fmts));
723
+ }
724
+ if (lastNotified.current !== nextId) {
725
+ lastNotified.current = nextId;
726
+ onActivePointChange?.(next ? pointInfo(model, next, fmts) : null);
727
+ }
728
+ },
729
+ [model, layout, fmts, onActivePointChange]
730
+ );
731
+ const pointerTarget = (event) => {
732
+ const svg = svgRef.current;
733
+ if (!svg) return null;
734
+ const rect = svg.getBoundingClientRect();
735
+ if (rect.width === 0 || rect.height === 0) return null;
736
+ const x = (event.clientX - rect.left) * (layout.width / rect.width);
737
+ const y = (event.clientY - rect.top) * (layout.height / rect.height);
738
+ return hitTest(layout, x, y);
739
+ };
740
+ const onPointerMove = (event) => {
741
+ if (!canInteract || event.pointerType === "touch") return;
742
+ const hit = pointerTarget(event);
743
+ if (hit) select(hit, "pointer");
744
+ else if (active?.source === "pointer") select(null, "pointer");
745
+ };
746
+ const onPointerDown = (event) => {
747
+ pointerDown.current = true;
748
+ if (!canInteract) return;
749
+ const hit = pointerTarget(event);
750
+ if (hit) select(hit, "pointer");
751
+ };
752
+ const onPointerUp = (event) => {
753
+ if (!canInteract || !onPointActivate) return;
754
+ const hit = pointerTarget(event);
755
+ const info = hit ? pointInfo(model, hit, fmts) : null;
756
+ if (info) onPointActivate(info, event);
757
+ };
758
+ const onStageLeave = (event) => {
759
+ if (event.pointerType === "touch") return;
760
+ if (event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget)) return;
761
+ if (active?.source === "pointer") select(null, "pointer");
762
+ };
763
+ const onFocus = () => {
764
+ if (!canInteract) return;
765
+ if (pointerDown.current) {
766
+ pointerDown.current = false;
767
+ return;
768
+ }
769
+ const start = (active && targetById(layout, active.id)) ?? layout.targets[0];
770
+ select(start, "keyboard");
771
+ };
772
+ const onBlur = (event) => {
773
+ pointerDown.current = false;
774
+ if (event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget)) return;
775
+ if (active) select(null, "keyboard");
776
+ };
777
+ const onKeyDown = (event) => {
778
+ if (!canInteract || event.altKey || event.ctrlKey || event.metaKey) return;
779
+ pointerDown.current = false;
780
+ if (isNavKey(event.key)) {
781
+ event.preventDefault();
782
+ const nextId = navigate(layout, active?.id ?? null, event.key);
783
+ select(targetById(layout, nextId), "keyboard");
784
+ return;
785
+ }
786
+ if (event.key === "Escape") {
787
+ if (active) {
788
+ event.preventDefault();
789
+ select(null, "keyboard");
790
+ }
791
+ return;
792
+ }
793
+ if ((event.key === "Enter" || event.key === " ") && target && onPointActivate) {
794
+ event.preventDefault();
795
+ const info = pointInfo(model, target, fmts);
796
+ if (info) onPointActivate(info, event);
797
+ }
798
+ };
799
+ const marks = useMemo3(
800
+ () => [...layout.marks].sort((a, b) => (KIND_ORDER[a.kind] ?? 0) - (KIND_ORDER[b.kind] ?? 0)),
801
+ [layout.marks]
802
+ );
803
+ const bands = layout.annotations.filter((annotation) => annotation.kind === "band" || annotation.kind === "forecast");
804
+ const notes = layout.annotations.filter((annotation) => annotation.kind !== "band" && annotation.kind !== "forecast");
805
+ const isActiveMark = (mark) => !!target && mark.point >= 0 && mark.point === target.point && (mark.series === target.series || mark.series === -1 && target.kind === "slice");
806
+ const keyboardActive = active?.source === "keyboard" ? target : void 0;
807
+ return /* @__PURE__ */ jsxs5("div", { "data-kit": "chart-stage", onPointerLeave: canInteract ? onStageLeave : void 0, children: [
808
+ /* @__PURE__ */ jsxs5(
809
+ "svg",
810
+ {
811
+ ref: svgRef,
812
+ viewBox: `0 0 ${layout.width} ${layout.height}`,
813
+ width: layout.width,
814
+ height: layout.height,
815
+ "data-kit": "chart-svg",
816
+ "data-mark": layout.mark,
817
+ "data-animate": animate ? "true" : void 0,
818
+ "data-interactive": canInteract ? "true" : void 0,
819
+ "data-activatable": canInteract && onPointActivate ? "true" : void 0,
820
+ focusable: canInteract ? "true" : "false",
821
+ ...canInteract ? {
822
+ role: "application",
823
+ tabIndex: 0,
824
+ "aria-roledescription": messages.chartRoleDescription,
825
+ onPointerMove,
826
+ onPointerDown,
827
+ onPointerUp,
828
+ onFocus,
829
+ onBlur,
830
+ onKeyDown
831
+ } : { role: "img" },
832
+ "aria-label": name,
833
+ "aria-describedby": descriptionId,
834
+ children: [
835
+ /* @__PURE__ */ jsx6(PatternDefs, { prefix: patternPrefix, indices: layout.patterns }),
836
+ /* @__PURE__ */ jsx6("g", { "data-kit": "chart-bands", "aria-hidden": "true", children: bands.map((annotation) => /* @__PURE__ */ jsx6(AnnotationView, { annotation }, annotation.id)) }),
837
+ /* @__PURE__ */ jsx6("g", { "data-kit": "chart-axes", "aria-hidden": "true", children: layout.axes.map((axis) => /* @__PURE__ */ jsx6(Axis, { axis, plot: layout.plot }, axis.id)) }),
838
+ /* @__PURE__ */ jsx6("g", { "data-kit": "chart-marks", "aria-hidden": "true", children: marks.map((mark) => /* @__PURE__ */ jsx6(MarkView, { mark, active: isActiveMark(mark), patternPrefix }, mark.id)) }),
839
+ /* @__PURE__ */ jsx6("g", { "data-kit": "chart-notes", "aria-hidden": "true", children: notes.map((annotation) => /* @__PURE__ */ jsx6(AnnotationView, { annotation }, annotation.id)) }),
840
+ /* @__PURE__ */ jsx6("g", { "data-kit": "chart-labels", "aria-hidden": "true", children: layout.labels.map((label) => /* @__PURE__ */ jsx6(Label, { label }, label.id)) }),
841
+ target && layout.crosshair !== "none" ? /* @__PURE__ */ jsx6("g", { "data-kit": "chart-crosshair", "aria-hidden": "true", children: layout.crosshair === "x" ? /* @__PURE__ */ jsx6("line", { x1: target.x, y1: layout.plot.y, x2: target.x, y2: layout.plot.y + layout.plot.height }) : /* @__PURE__ */ jsx6("line", { x1: layout.plot.x, y1: target.y, x2: layout.plot.x + layout.plot.width, y2: target.y }) }) : null,
842
+ keyboardActive ? /* @__PURE__ */ jsx6("g", { "aria-hidden": "true", children: /* @__PURE__ */ jsx6(FocusRing, { shape: keyboardActive.focus }) }) : null
843
+ ]
844
+ }
845
+ ),
846
+ content && target ? /* @__PURE__ */ jsx6(Tooltip, { content, target, layout, patternPrefix }) : null,
847
+ /* @__PURE__ */ jsx6(VisuallyHidden2, { id: descriptionId, children: description }),
848
+ /* @__PURE__ */ jsx6(VisuallyHidden2, { role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement })
849
+ ] });
850
+ }
851
+
852
+ // src/react/chart.tsx
853
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
854
+ var DEFAULT_WIDTH = 640;
855
+ var MIN_WIDTH = 120;
856
+ var DEFAULT_HEIGHT = 320;
857
+ var SPARKLINE_HEIGHT = 48;
858
+ var SPARKLINE_WIDTH = 160;
859
+ var GAUGE_HEIGHT = 220;
860
+ function defaultChartHeight(mark) {
861
+ if (mark === "sparkline") return SPARKLINE_HEIGHT;
862
+ if (mark === "gauge" || mark === "progress-ring") return GAUGE_HEIGHT;
863
+ return DEFAULT_HEIGHT;
864
+ }
865
+ function canvasLegend(model, hidden) {
866
+ if (model.slices.length > 0) {
867
+ return model.slices.map((slice) => ({
868
+ id: slice.id,
869
+ label: slice.label,
870
+ series: null,
871
+ tone: slice.style.tone,
872
+ marker: slice.style.marker,
873
+ dash: void 0,
874
+ glyph: "slice",
875
+ pattern: -1,
876
+ visible: !hidden.has(slice.id),
877
+ toggle: true
878
+ }));
879
+ }
880
+ return model.series.map((series, index) => {
881
+ const kind = model.mark === "combo" ? series.mark : model.mark;
882
+ const glyph = kind === "bar" || kind === "histogram" || kind === "waterfall" || kind === "lollipop" ? "bar" : kind === "area" ? "area" : kind === "scatter" || kind === "bubble" ? "point" : "line";
883
+ return {
884
+ id: series.id,
885
+ label: series.label,
886
+ series: index,
887
+ tone: series.style.tone,
888
+ marker: series.style.marker,
889
+ dash: dashArrays[series.style.dash],
890
+ glyph,
891
+ pattern: -1,
892
+ visible: !hidden.has(series.id),
893
+ toggle: true
894
+ };
895
+ });
896
+ }
897
+ function Chart(props) {
898
+ const { spec, className, style, id } = props;
899
+ const config = useChartConfig();
900
+ const locale = props.locale ?? config.locale;
901
+ const messageInput = useMemo4(() => mergeMessages(config.messages, props.messages), [config.messages, props.messages]);
902
+ const datasets = props.datasets ?? config.datasets;
903
+ const model = useMemo4(() => buildChartModel(spec, { datasets, messages: messageInput }), [spec, datasets, messageInput]);
904
+ const fmts = useMemo4(() => createFormatters(model, { locale, messages: messageInput }), [model, locale, messageInput]);
905
+ const messages = fmts.messages;
906
+ const rootRef = useRef4(null);
907
+ const explicitDirection = props.dir ?? config.direction ?? (spec.direction && spec.direction !== "auto" ? spec.direction : void 0);
908
+ const { direction, attribute } = useDirection(rootRef, explicitDirection, locale);
909
+ const measured = useElementWidth(rootRef, props.width);
910
+ const fallbackWidth = model.mark === "sparkline" ? SPARKLINE_WIDTH : DEFAULT_WIDTH;
911
+ const width = Math.max(MIN_WIDTH, measured ?? fallbackWidth);
912
+ const height = props.height ?? defaultChartHeight(model.mark);
913
+ const reducedMotion = usePrefersReducedMotion();
914
+ const idPrefix = `kit-chart-${safeId(useId())}`;
915
+ const initialHidden = props.defaultHiddenSeries ?? model.series.filter((series) => series.hidden).map((series) => series.id);
916
+ const [hidden, setHidden] = useHiddenIds(props.hiddenSeries, initialHidden, props.onHiddenSeriesChange);
917
+ const target = props.target ?? config.target ?? "screen";
918
+ const renderer = resolveRenderer({
919
+ renderer: props.renderer ?? spec.renderer,
920
+ mark: model.mark,
921
+ pointCount: model.pointCount,
922
+ target
923
+ });
924
+ const [canvasFailure, setCanvasFailure] = useState4(null);
925
+ const onCanvasUnavailable = useCallback3((reason) => setCanvasFailure(reason), []);
926
+ const canvas = renderer === "canvas" && canvasFailure === null && model.status === "ok";
927
+ const layout = useMemo4(
928
+ () => canvas || model.status !== "ok" ? null : layoutChart(model, { width, height, rtl: direction === "rtl", locale, messages: messageInput, hidden }),
929
+ [canvas, model, width, height, direction, locale, messageInput, hidden]
930
+ );
931
+ const description = useMemo4(
932
+ () => describeModel(model, { locale, messages: messageInput, hidden }),
933
+ [model, locale, messageInput, hidden]
934
+ );
935
+ const legendEntries = useMemo4(
936
+ () => layout ? layout.legend : canvas ? canvasLegend(model, hidden) : [],
937
+ [layout, canvas, model, hidden]
938
+ );
939
+ const legendSetting = spec.legend?.show ?? "auto";
940
+ const showLegend = legendSetting === true || legendSetting === "auto" && legendEntries.length > 1;
941
+ const onToggle = (entry, event) => {
942
+ const toggleable = legendEntries.filter((candidate) => candidate.toggle).map((candidate) => candidate.id);
943
+ const modifier = event.shiftKey || event.ctrlKey || event.metaKey;
944
+ if (spec.interactions?.legendIsolate && !modifier) {
945
+ const visible = toggleable.filter((candidate) => !hidden.has(candidate));
946
+ const isolated = visible.length === 1 && visible[0] === entry.id;
947
+ setHidden(isolated ? [] : toggleable.filter((candidate) => candidate !== entry.id));
948
+ return;
949
+ }
950
+ const next = new Set(hidden);
951
+ if (next.has(entry.id)) next.delete(entry.id);
952
+ else next.add(entry.id);
953
+ setHidden([...next]);
954
+ };
955
+ const markName = messages.markNames[model.mark] ?? model.mark;
956
+ const allHidden = layout ? layout.allHidden : canvas && legendEntries.length > 0 && legendEntries.every((entry) => !entry.visible);
957
+ const statusKind = props.loading ? "loading" : props.error ? "error" : model.status !== "ok" ? model.status : allHidden ? "all-hidden" : "ok";
958
+ let body;
959
+ if (props.loading) {
960
+ body = /* @__PURE__ */ jsx7(ChartState, { kind: "loading", message: messages.loading, height });
961
+ } else if (props.error) {
962
+ body = /* @__PURE__ */ jsx7(ChartState, { kind: "error", message: messages.error, height, onRetry: props.onRetry, retryLabel: messages.retry });
963
+ } else if (model.status === "missing-data") {
964
+ body = /* @__PURE__ */ jsx7(
965
+ ChartState,
966
+ {
967
+ kind: "missing-data",
968
+ message: formatMessage(messages.missingDataset, { ref: model.missingRef ?? "" }, locale),
969
+ height
970
+ }
971
+ );
972
+ } else if (model.status === "unsupported") {
973
+ body = /* @__PURE__ */ jsx7(ChartState, { kind: "unsupported", message: formatMessage(messages.unsupportedMark, { mark: markName }, locale), height });
974
+ } else if (model.status === "empty") {
975
+ body = /* @__PURE__ */ jsx7(ChartState, { kind: "empty", message: messages.empty, height });
976
+ } else if (canvas && allHidden) {
977
+ body = /* @__PURE__ */ jsx7("p", { "data-kit": "chart-note", role: "status", children: messages.allSeriesHidden });
978
+ } else if (canvas) {
979
+ body = /* @__PURE__ */ jsx7(
980
+ EChartsPlot,
981
+ {
982
+ model,
983
+ messages,
984
+ messageInput,
985
+ locale,
986
+ hidden,
987
+ width: props.width,
988
+ height,
989
+ loader: props.loadEcharts ?? config.loadEcharts ?? defaultEChartsLoader,
990
+ animate: !reducedMotion,
991
+ name: description.name,
992
+ description: description.text,
993
+ idPrefix,
994
+ onUnavailable: onCanvasUnavailable
995
+ }
996
+ );
997
+ } else if (layout) {
998
+ body = /* @__PURE__ */ jsxs6(Fragment3, { children: [
999
+ allHidden ? /* @__PURE__ */ jsx7("p", { "data-kit": "chart-note", role: "status", children: messages.allSeriesHidden }) : null,
1000
+ /* @__PURE__ */ jsx7(
1001
+ Plot,
1002
+ {
1003
+ model,
1004
+ layout,
1005
+ fmts,
1006
+ name: description.name,
1007
+ description: description.text,
1008
+ idPrefix,
1009
+ interactive: props.interactive ?? true,
1010
+ tooltip: props.tooltip ?? spec.interactions?.tooltip !== false,
1011
+ animate: !reducedMotion,
1012
+ onPointActivate: props.onPointActivate,
1013
+ onActivePointChange: props.onActivePointChange
1014
+ }
1015
+ )
1016
+ ] });
1017
+ } else {
1018
+ body = null;
1019
+ }
1020
+ const hasPlot = statusKind === "ok" || statusKind === "all-hidden";
1021
+ const legendPosition = spec.legend?.position ?? "bottom";
1022
+ const legend = hasPlot && showLegend ? /* @__PURE__ */ jsx7(
1023
+ ChartLegend,
1024
+ {
1025
+ entries: legendEntries,
1026
+ label: messages.legend,
1027
+ position: legendPosition,
1028
+ patternPrefix: `${idPrefix}-pattern`,
1029
+ onToggle
1030
+ }
1031
+ ) : null;
1032
+ const scale = layout?.colorScale;
1033
+ const colorScale = hasPlot && scale ? /* @__PURE__ */ jsx7(
1034
+ ColorScaleLegend,
1035
+ {
1036
+ kind: scale.kind,
1037
+ min: fmts.value(0)(scale.min),
1038
+ max: fmts.value(0)(scale.max),
1039
+ label: spec.value?.title ?? spec.color?.title ?? messages.columnValue
1040
+ }
1041
+ ) : null;
1042
+ return /* @__PURE__ */ jsxs6(
1043
+ "div",
1044
+ {
1045
+ ref: rootRef,
1046
+ id,
1047
+ className,
1048
+ style,
1049
+ dir: attribute,
1050
+ "data-kit": "chart",
1051
+ "data-mark": model.mark,
1052
+ "data-renderer": canvas ? "canvas" : "svg",
1053
+ "data-state": statusKind,
1054
+ children: [
1055
+ canvasFailure !== null && renderer === "canvas" ? /* @__PURE__ */ jsx7("p", { "data-kit": "chart-note", role: "status", children: canvasFailure === "missing" ? messages.canvasMissing : messages.canvasUnsupported }) : null,
1056
+ hasPlot && model.dualAxis ? /* @__PURE__ */ jsx7("div", { "data-kit": "chart-warning", children: /* @__PURE__ */ jsx7(Badge, { variant: "soft", tone: "warning", children: messages.dualAxisWarning }) }) : null,
1057
+ legendPosition === "top" ? legend : null,
1058
+ body,
1059
+ legendPosition === "bottom" ? legend : null,
1060
+ colorScale
1061
+ ]
1062
+ }
1063
+ );
1064
+ }
1065
+ function SvgChart(props) {
1066
+ return /* @__PURE__ */ jsx7(Chart, { ...props, renderer: "svg" });
1067
+ }
1068
+ function EChartsChart(props) {
1069
+ return /* @__PURE__ */ jsx7(Chart, { ...props, renderer: "canvas" });
1070
+ }
1071
+
1072
+ // src/react/frame.tsx
1073
+ import { createElement, useId as useId3, useMemo as useMemo6, useState as useState5 } from "react";
1074
+ import { Button as Button2 } from "@x12i/ui";
1075
+
1076
+ // src/react/data-table.tsx
1077
+ import { useId as useId2, useMemo as useMemo5 } from "react";
1078
+ import { VisuallyHidden as VisuallyHidden3 } from "@x12i/ui";
1079
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1080
+ function ChartDataTable({ spec, datasets, locale, messages, maxRows = 200, hidden = false, className, id }) {
1081
+ const config = useChartConfig();
1082
+ const merged = useMemo5(() => mergeMessages(config.messages, messages), [config.messages, messages]);
1083
+ const resolvedLocale = locale ?? config.locale;
1084
+ const resolvedDatasets = datasets ?? config.datasets;
1085
+ const table = useMemo5(
1086
+ () => chartToTable(spec, { datasets: resolvedDatasets, locale: resolvedLocale, messages: merged }),
1087
+ [spec, resolvedDatasets, resolvedLocale, merged]
1088
+ );
1089
+ const text = resolveMessages(merged);
1090
+ const captionId = `kit-chart-table-${safeId(useId2())}`;
1091
+ const rows = table.rows.slice(0, maxRows);
1092
+ const truncated = table.rows.length > rows.length;
1093
+ const content = /* @__PURE__ */ jsxs7(
1094
+ "div",
1095
+ {
1096
+ id,
1097
+ className,
1098
+ "data-kit": "chart-table",
1099
+ "data-hidden": hidden ? "true" : void 0,
1100
+ ...hidden ? {} : { role: "region", tabIndex: 0, "aria-labelledby": captionId },
1101
+ children: [
1102
+ /* @__PURE__ */ jsxs7("table", { "data-kit": "chart-table-element", children: [
1103
+ /* @__PURE__ */ jsx8("caption", { id: captionId, children: table.caption }),
1104
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsx8("tr", { children: table.columns.map((column) => /* @__PURE__ */ jsx8("th", { scope: "col", "data-numeric": column.numeric ? "true" : void 0, children: column.label }, column.key)) }) }),
1105
+ /* @__PURE__ */ jsx8("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx8("tr", { children: row.map((cell, cellIndex) => {
1106
+ const column = table.columns[cellIndex];
1107
+ const numeric = column?.numeric ? "true" : void 0;
1108
+ return cellIndex === 0 ? /* @__PURE__ */ jsx8("th", { scope: "row", "data-numeric": numeric, children: cell.text }, column?.key ?? cellIndex) : /* @__PURE__ */ jsx8("td", { "data-numeric": numeric, children: cell.text }, column?.key ?? cellIndex);
1109
+ }) }, rowIndex)) })
1110
+ ] }),
1111
+ truncated ? /* @__PURE__ */ jsx8("p", { "data-kit-slot": "table-note", children: formatMessage(text.tableTruncated, { shown: rows.length, total: table.rows.length }, resolvedLocale) }) : null
1112
+ ]
1113
+ }
1114
+ );
1115
+ return hidden ? /* @__PURE__ */ jsx8(VisuallyHidden3, { asChild: true, children: content }) : content;
1116
+ }
1117
+
1118
+ // src/react/icons.tsx
1119
+ import { jsx as jsx9 } from "react/jsx-runtime";
1120
+ function Icon({ children }) {
1121
+ return /* @__PURE__ */ jsx9("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": "true", focusable: "false", "data-kit": "chart-icon", children });
1122
+ }
1123
+ function TableIcon() {
1124
+ return /* @__PURE__ */ jsx9(Icon, { children: /* @__PURE__ */ jsx9("path", { d: "M2,3H14V13H2Z M2,6.5H14 M2,9.75H14 M6,3V13" }) });
1125
+ }
1126
+ function ChartIcon() {
1127
+ return /* @__PURE__ */ jsx9(Icon, { children: /* @__PURE__ */ jsx9("path", { d: "M2,2V14H14 M5,11V8 M8,11V5 M11,11V7" }) });
1128
+ }
1129
+ function DownloadIcon() {
1130
+ return /* @__PURE__ */ jsx9(Icon, { children: /* @__PURE__ */ jsx9("path", { d: "M8,2V10 M5,7L8,10L11,7 M3,13H13" }) });
1131
+ }
1132
+
1133
+ // src/react/frame.tsx
1134
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1135
+ function slug(title) {
1136
+ const base = title.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "");
1137
+ return base || "chart";
1138
+ }
1139
+ function downloadCsv(csv, fileName) {
1140
+ if (typeof document === "undefined" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return;
1141
+ const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
1142
+ const anchor = document.createElement("a");
1143
+ anchor.href = url;
1144
+ anchor.download = `${fileName}.csv`;
1145
+ anchor.hidden = true;
1146
+ document.body.append(anchor);
1147
+ anchor.click();
1148
+ anchor.remove();
1149
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1150
+ }
1151
+ function ChartFrame({
1152
+ headingLevel = 3,
1153
+ toolbar = true,
1154
+ actions,
1155
+ view: viewProp,
1156
+ defaultView = "chart",
1157
+ onViewChange,
1158
+ tableMaxRows,
1159
+ csvFileName,
1160
+ onDownload,
1161
+ variant = "plain",
1162
+ className,
1163
+ style,
1164
+ id,
1165
+ ...chartProps
1166
+ }) {
1167
+ const { spec } = chartProps;
1168
+ const config = useChartConfig();
1169
+ const locale = chartProps.locale ?? config.locale;
1170
+ const datasets = chartProps.datasets ?? config.datasets;
1171
+ const messageInput = useMemo6(() => mergeMessages(config.messages, chartProps.messages), [config.messages, chartProps.messages]);
1172
+ const messages = resolveMessages(messageInput);
1173
+ const [internalView, setInternalView] = useState5(defaultView);
1174
+ const view = viewProp ?? internalView;
1175
+ const uid = safeId(useId3());
1176
+ const titleId = `kit-chart-title-${uid}`;
1177
+ const bodyId = `kit-chart-body-${uid}`;
1178
+ const showTable = spec.a11y.table !== false;
1179
+ const setView = (next) => {
1180
+ if (viewProp === void 0) setInternalView(next);
1181
+ onViewChange?.(next);
1182
+ };
1183
+ const download = () => {
1184
+ const csv = chartToCsv(spec, { datasets, locale, messages: messageInput, bom: true });
1185
+ const name = csvFileName ?? slug(spec.title);
1186
+ if (onDownload) onDownload(csv, name);
1187
+ else downloadCsv(csv, name);
1188
+ };
1189
+ const tableVisible = showTable && view === "table";
1190
+ return /* @__PURE__ */ jsxs8("figure", { id, className, style, "data-kit": "chart-frame", "data-variant": variant, "data-view": tableVisible ? "table" : "chart", children: [
1191
+ /* @__PURE__ */ jsxs8("figcaption", { "data-kit-slot": "frame-caption", children: [
1192
+ createElement(`h${headingLevel}`, { id: titleId, "data-kit-slot": "frame-title" }, spec.title),
1193
+ spec.subtitle ? /* @__PURE__ */ jsx10("p", { "data-kit-slot": "frame-subtitle", children: spec.subtitle }) : null
1194
+ ] }),
1195
+ toolbar ? /* @__PURE__ */ jsxs8("div", { role: "group", "aria-label": messages.toolbar, "data-kit": "chart-toolbar", children: [
1196
+ showTable ? /* @__PURE__ */ jsxs8(
1197
+ Button2,
1198
+ {
1199
+ size: "sm",
1200
+ variant: "ghost",
1201
+ tone: "neutral",
1202
+ "aria-controls": bodyId,
1203
+ onClick: () => setView(tableVisible ? "chart" : "table"),
1204
+ children: [
1205
+ tableVisible ? /* @__PURE__ */ jsx10(ChartIcon, {}) : /* @__PURE__ */ jsx10(TableIcon, {}),
1206
+ tableVisible ? messages.viewAsChart : messages.viewAsTable
1207
+ ]
1208
+ }
1209
+ ) : null,
1210
+ /* @__PURE__ */ jsxs8(Button2, { size: "sm", variant: "ghost", tone: "neutral", onClick: download, children: [
1211
+ /* @__PURE__ */ jsx10(DownloadIcon, {}),
1212
+ messages.downloadCsv
1213
+ ] }),
1214
+ actions
1215
+ ] }) : null,
1216
+ /* @__PURE__ */ jsxs8("div", { id: bodyId, "data-kit-slot": "frame-body", children: [
1217
+ /* @__PURE__ */ jsx10("div", { "data-kit-slot": "frame-chart", hidden: tableVisible, children: /* @__PURE__ */ jsx10(Chart, { ...chartProps }) }),
1218
+ showTable ? /* @__PURE__ */ jsx10(
1219
+ ChartDataTable,
1220
+ {
1221
+ spec,
1222
+ datasets,
1223
+ locale,
1224
+ messages: messageInput,
1225
+ hidden: !tableVisible,
1226
+ ...tableMaxRows !== void 0 ? { maxRows: tableMaxRows } : {}
1227
+ }
1228
+ ) : null
1229
+ ] }),
1230
+ spec.source ? /* @__PURE__ */ jsx10("p", { "data-kit-slot": "frame-source", children: spec.source }) : null
1231
+ ] });
1232
+ }
1233
+ function ChartCard(props) {
1234
+ return /* @__PURE__ */ jsx10(ChartFrame, { ...props, variant: "card" });
1235
+ }
1236
+
1237
+ // src/react/grid.tsx
1238
+ import { jsx as jsx11 } from "react/jsx-runtime";
1239
+ function ChartGrid({ children, columns, label, className, style }) {
1240
+ return /* @__PURE__ */ jsx11(
1241
+ "div",
1242
+ {
1243
+ className,
1244
+ style: { ...columns ? { "--_columns": columns } : {}, ...style },
1245
+ "data-kit": "chart-grid-layout",
1246
+ "data-fixed": columns ? "true" : void 0,
1247
+ ...label ? { role: "group", "aria-label": label } : {},
1248
+ children
1249
+ }
1250
+ );
1251
+ }
1252
+ export {
1253
+ Chart,
1254
+ ChartCard,
1255
+ ChartConfigProvider,
1256
+ ChartDataTable,
1257
+ ChartFrame,
1258
+ ChartGrid,
1259
+ ChartLegend,
1260
+ ChartState,
1261
+ ColorScaleLegend,
1262
+ EChartsChart,
1263
+ SvgChart,
1264
+ defaultChartHeight,
1265
+ defaultEChartsLoader,
1266
+ mergeMessages,
1267
+ readThemeColors,
1268
+ themeAt,
1269
+ useChartConfig
1270
+ };
1271
+ //# sourceMappingURL=react.js.map