@kind-ui/charts 0.1.0 → 0.2.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/CHANGELOG.md +32 -0
- package/README.md +657 -865
- package/dist/activity-rings.d.ts +2 -1
- package/dist/activity-rings.js +6 -3
- package/dist/animation.d.ts +11 -3
- package/dist/animation.js +18 -10
- package/dist/area-chart.d.ts +3 -1
- package/dist/area-chart.js +15 -10
- package/dist/area-series.d.ts +9 -1
- package/dist/area-series.js +35 -8
- package/dist/bar-chart.d.ts +10 -3
- package/dist/bar-chart.js +17 -4
- package/dist/bar-series.d.ts +11 -1
- package/dist/bar-series.js +65 -13
- package/dist/box-plot.js +2 -2
- package/dist/category-cells.d.ts +7 -1
- package/dist/category-cells.js +32 -2
- package/dist/chart-background-pattern.d.ts +23 -0
- package/dist/chart-background-pattern.js +31 -0
- package/dist/chart-context.d.ts +6 -2
- package/dist/chart-interaction.d.ts +94 -0
- package/dist/chart-interaction.js +224 -0
- package/dist/combo-chart.d.ts +8 -5
- package/dist/combo-chart.js +14 -12
- package/dist/configured-line-chart.d.ts +5 -1
- package/dist/configured-line-chart.js +8 -5
- package/dist/emphasis.d.ts +5 -2
- package/dist/emphasis.js +18 -4
- package/dist/fill-pattern.d.ts +25 -0
- package/dist/fill-pattern.js +23 -0
- package/dist/heatmap.d.ts +3 -1
- package/dist/heatmap.js +34 -12
- package/dist/histogram-chart.js +10 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +8 -1
- package/dist/legend.d.ts +2 -0
- package/dist/legend.js +28 -7
- package/dist/line-chart.d.ts +13 -3
- package/dist/line-chart.js +54 -34
- package/dist/line-dash.d.ts +8 -0
- package/dist/line-dash.js +18 -0
- package/dist/line-series.d.ts +9 -1
- package/dist/line-series.js +38 -11
- package/dist/loading-cartesian-designs.d.ts +6 -0
- package/dist/loading-cartesian-designs.js +123 -0
- package/dist/loading-motion.d.ts +37 -0
- package/dist/loading-motion.js +88 -0
- package/dist/loading-polar-designs.d.ts +6 -0
- package/dist/loading-polar-designs.js +95 -0
- package/dist/loading-skeleton.d.ts +29 -0
- package/dist/loading-skeleton.js +143 -0
- package/dist/loading-standalone-designs.d.ts +5 -0
- package/dist/loading-standalone-designs.js +126 -0
- package/dist/percent-stack.d.ts +18 -0
- package/dist/percent-stack.js +33 -0
- package/dist/pie-chart.d.ts +7 -3
- package/dist/pie-chart.js +37 -6
- package/dist/pie-pin-identity.d.ts +4 -0
- package/dist/pie-pin-identity.js +10 -0
- package/dist/pie-series.d.ts +4 -0
- package/dist/pie-series.js +137 -14
- package/dist/pie-tooltip-pin.d.ts +4 -0
- package/dist/pie-tooltip-pin.js +52 -0
- package/dist/point-marker.d.ts +19 -0
- package/dist/point-marker.js +19 -0
- package/dist/polar-chart.d.ts +14 -5
- package/dist/polar-chart.js +41 -10
- package/dist/polar-series.js +68 -22
- package/dist/radar-interaction.js +10 -4
- package/dist/radial-category.d.ts +2 -0
- package/dist/reveal-clip.d.ts +16 -0
- package/dist/reveal-clip.js +18 -0
- package/dist/root.d.ts +5 -9
- package/dist/root.js +46 -11
- package/dist/sankey-chart.d.ts +5 -1
- package/dist/sankey-chart.js +64 -13
- package/dist/sankey-colors.d.ts +3 -0
- package/dist/sankey-interaction.d.ts +8 -0
- package/dist/sankey-interaction.js +32 -0
- package/dist/sankey-legend.d.ts +2 -1
- package/dist/sankey-legend.js +18 -3
- package/dist/sankey-node-label.d.ts +22 -0
- package/dist/sankey-node-label.js +34 -0
- package/dist/scatter-chart.d.ts +5 -3
- package/dist/scatter-chart.js +14 -4
- package/dist/scatter-series.js +5 -3
- package/dist/series-color.d.ts +10 -0
- package/dist/series-color.js +48 -0
- package/dist/series-interaction.d.ts +12 -0
- package/dist/series-interaction.js +82 -0
- package/dist/series-label.d.ts +7 -0
- package/dist/series-label.js +18 -0
- package/dist/series-paint.d.ts +8 -0
- package/dist/series-paint.js +27 -0
- package/dist/styles.css +151 -1
- package/dist/stylesheet-warning.d.ts +2 -0
- package/dist/stylesheet-warning.js +86 -0
- package/dist/tooltip-content.d.ts +8 -1
- package/dist/tooltip-content.js +15 -4
- package/dist/tooltip.d.ts +3 -1
- package/dist/tooltip.js +8 -5
- package/dist/types.d.ts +11 -2
- package/dist/waterfall-chart.d.ts +4 -0
- package/dist/waterfall-chart.js +7 -0
- package/package.json +2 -2
package/dist/sankey-chart.js
CHANGED
|
@@ -3,8 +3,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { animate as animateValue, useMotionValue } from "motion/react";
|
|
4
4
|
import { createContext, useCallback, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
5
5
|
import { Sankey as EngineSankey, useChartHeight, useChartWidth, } from "recharts";
|
|
6
|
+
import { useOptionalChartInteraction } from "./chart-interaction.js";
|
|
7
|
+
import { LoadingSkeletonSurface, LoadingStatus, useLoadingSeed } from "./loading-skeleton.js";
|
|
8
|
+
import { StandaloneLoadingDesign } from "./loading-standalone-designs.js";
|
|
6
9
|
import { SankeyColors } from "./sankey-colors.js";
|
|
7
10
|
import { prepareSankeyData } from "./sankey-data.js";
|
|
11
|
+
import { SankeyFocusMark } from "./sankey-interaction.js";
|
|
8
12
|
import { SankeyLink, SankeyNode, } from "./sankey-marks.js";
|
|
9
13
|
const configuredNode = (shape) => _jsx(SankeyNode, { ...shape });
|
|
10
14
|
const configuredLink = (shape) => _jsx(SankeyLink, { ...shape });
|
|
@@ -27,7 +31,25 @@ const snapshot = () => window.matchMedia(query).matches;
|
|
|
27
31
|
const serverSnapshot = () => true;
|
|
28
32
|
export const SankeyMotion = createContext({ reveal: false, progress: null, width: 1 });
|
|
29
33
|
/** Recharts owns layout, renderers, labels and events. Zero flows remain in the data alternative. */
|
|
30
|
-
export function SankeyChart({ data, nodeConfig, animate = false, empty = "No positive flows", ...props }) {
|
|
34
|
+
export function SankeyChart({ data, nodeConfig, interactionBinding, animate = false, loading, loadingLabel, empty = "No positive flows", ...props }) {
|
|
35
|
+
const interaction = useOptionalChartInteraction();
|
|
36
|
+
if (interactionBinding &&
|
|
37
|
+
(!interaction || interaction.kind !== "node" || interaction.mode !== "focus"))
|
|
38
|
+
throw new Error("SankeyChart Root interaction binding requires node focus");
|
|
39
|
+
const adjacent = new Map();
|
|
40
|
+
for (const link of data.links) {
|
|
41
|
+
const source = typeof link.source === "string" ? link.source : data.nodes[link.source]?.id;
|
|
42
|
+
const target = typeof link.target === "string" ? link.target : data.nodes[link.target]?.id;
|
|
43
|
+
if (source && target) {
|
|
44
|
+
const from = adjacent.get(source) ?? new Set();
|
|
45
|
+
from.add(target);
|
|
46
|
+
adjacent.set(source, from);
|
|
47
|
+
const to = adjacent.get(target) ?? new Set();
|
|
48
|
+
to.add(source);
|
|
49
|
+
adjacent.set(target, to);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const loadingSeed = useLoadingSeed(loading);
|
|
31
53
|
const duration = typeof animate === "object" ? (animate.revealDurationMs ?? 450) : 450;
|
|
32
54
|
if (!Number.isFinite(duration) || duration < 0)
|
|
33
55
|
throw new Error("Sankey revealDurationMs must be finite and nonnegative");
|
|
@@ -63,15 +85,21 @@ export function SankeyChart({ data, nodeConfig, animate = false, empty = "No pos
|
|
|
63
85
|
const [interrupted, setInterrupted] = useState(false);
|
|
64
86
|
const [size, setSize] = useState(null);
|
|
65
87
|
const measured = useRef(null);
|
|
88
|
+
const previousLoading = useRef(loading);
|
|
89
|
+
const completing = previousLoading.current === true && loading !== true;
|
|
66
90
|
const onSize = useCallback((width, height) => {
|
|
67
91
|
const prior = measured.current;
|
|
68
92
|
if (prior?.width === width && prior?.height === height)
|
|
69
93
|
return;
|
|
70
|
-
if (prior)
|
|
94
|
+
if (prior && !loading && !completing)
|
|
71
95
|
setInterrupted(true);
|
|
72
96
|
measured.current = { width, height };
|
|
73
97
|
setSize(measured.current);
|
|
74
|
-
}, []);
|
|
98
|
+
}, [loading, completing]);
|
|
99
|
+
useLayoutEffect(() => {
|
|
100
|
+
if (loading)
|
|
101
|
+
setInterrupted(false);
|
|
102
|
+
}, [loading]);
|
|
75
103
|
const inputs = [
|
|
76
104
|
duration,
|
|
77
105
|
data,
|
|
@@ -91,17 +119,20 @@ export function SankeyChart({ data, nodeConfig, animate = false, empty = "No pos
|
|
|
91
119
|
const started = useRef(false);
|
|
92
120
|
const previous = useRef(inputs);
|
|
93
121
|
useLayoutEffect(() => {
|
|
94
|
-
if (
|
|
122
|
+
if (!loading &&
|
|
123
|
+
!completing &&
|
|
124
|
+
inputs.some((value, index) => (index !== 0 || started.current) && value !== previous.current[index]))
|
|
95
125
|
setInterrupted(true);
|
|
96
126
|
previous.current = inputs;
|
|
127
|
+
previousLoading.current = loading;
|
|
97
128
|
});
|
|
98
|
-
const enabled = Boolean(animate) && !reduced;
|
|
129
|
+
const enabled = Boolean(animate) && !reduced && !loading;
|
|
99
130
|
const previousEnabled = useRef(enabled);
|
|
100
131
|
useLayoutEffect(() => {
|
|
101
|
-
if (previousEnabled.current && !enabled)
|
|
132
|
+
if (previousEnabled.current && !enabled && !loading)
|
|
102
133
|
setInterrupted(true);
|
|
103
134
|
previousEnabled.current = enabled;
|
|
104
|
-
}, [enabled]);
|
|
135
|
+
}, [enabled, loading]);
|
|
105
136
|
const reveal = enabled && !interrupted && size !== null;
|
|
106
137
|
const progress = useMotionValue(1);
|
|
107
138
|
useLayoutEffect(() => {
|
|
@@ -133,10 +164,30 @@ export function SankeyChart({ data, nodeConfig, animate = false, empty = "No pos
|
|
|
133
164
|
links.some((link) => link.value * minimumScale === 0))
|
|
134
165
|
throw new Error("Sankey flows exceed native renderer numeric limits at this size; use explicitly rescaled input units.");
|
|
135
166
|
}
|
|
136
|
-
return (_jsx(SankeyColors, { value: nodeConfig, children: _jsx(SankeyMotion, { value: { reveal, progress, width: size?.width ?? 1 }, children: _jsxs("div", { "data-kind-ui": "sankey", onPointerDownCapture: () =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
167
|
+
return (_jsx(SankeyColors, { value: nodeConfig, children: _jsx(SankeyMotion, { value: { reveal, progress, width: size?.width ?? 1 }, children: _jsxs("div", { "data-kind-ui": "sankey", onPointerDownCapture: () => {
|
|
168
|
+
if (!loading)
|
|
169
|
+
setInterrupted(true);
|
|
170
|
+
}, onFocusCapture: () => {
|
|
171
|
+
if (!loading)
|
|
172
|
+
setInterrupted(true);
|
|
173
|
+
}, style: { position: "relative", width: "fit-content", height: "fit-content" }, children: [_jsx("div", { "data-kind-ui": "sankey-content", "data-loading": loading || undefined, "aria-busy": loading, "aria-hidden": loading || undefined, inert: loading || undefined, style: { display: "contents" }, children: _jsxs(EngineSankey, { ...props, ...(nodeConfig
|
|
174
|
+
? {
|
|
175
|
+
node: props.node ?? configuredNode,
|
|
176
|
+
link: props.link ?? configuredLink,
|
|
177
|
+
}
|
|
178
|
+
: {}), onClick: (item, type, event) => {
|
|
179
|
+
props.onClick?.(item, type, event);
|
|
180
|
+
if (interactionBinding && type === "node")
|
|
181
|
+
interaction?.activate({ kind: "node", key: item.payload.id }, "mark", event);
|
|
182
|
+
}, ...(interactionBinding
|
|
183
|
+
? {
|
|
184
|
+
node: props.node ??
|
|
185
|
+
((shape) => (_jsx(SankeyFocusMark, { nodeKey: shape.payload.id, adjacent: adjacent, children: _jsx(SankeyNode, { ...shape }) }))),
|
|
186
|
+
link: props.link ??
|
|
187
|
+
((shape) => (_jsx(SankeyFocusMark, { endpoints: [
|
|
188
|
+
shape.payload.source.id,
|
|
189
|
+
shape.payload.target.id,
|
|
190
|
+
], adjacent: adjacent, children: _jsx(SankeyLink, { ...shape }) }))),
|
|
191
|
+
}
|
|
192
|
+
: {}), data: drawable && links.length ? { nodes, links } : { nodes: [], links: [] }, children: [_jsx(NativeSize, { onSize: onSize }), props.children] }) }), loading && (_jsx(LoadingSkeletonSurface, { family: "sankey", design: (seed) => _jsx(StandaloneLoadingDesign, { family: "sankey", seed: seed }), width: "100%", height: "100%", seed: loadingSeed, animation: { revealDurationMs: duration } })), _jsx(LoadingStatus, { loading: loading, label: loadingLabel }), !loading && !links.length ? (_jsx("div", { role: "status", style: { position: "absolute", inset: 0 }, children: empty })) : !loading && !drawable ? (_jsx("div", { role: "status", style: { position: "absolute", inset: 0 }, children: "Insufficient space for flows; use the data table" })) : null] }) }) }));
|
|
142
193
|
}
|
package/dist/sankey-colors.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
/** Sankey IDs are arbitrary nonempty strings, independent from Root CSS variable keys. */
|
|
2
3
|
export type SankeyNodeConfig = Readonly<Record<string, {
|
|
3
4
|
label: string;
|
|
4
5
|
color: string;
|
|
6
|
+
icon?: ReactNode;
|
|
5
7
|
}>>;
|
|
6
8
|
export declare const SankeyColors: import("react").Context<Readonly<Record<string, {
|
|
7
9
|
label: string;
|
|
8
10
|
color: string;
|
|
11
|
+
icon?: ReactNode;
|
|
9
12
|
}>> | undefined>;
|
|
10
13
|
export declare function sankeyColor(config: SankeyNodeConfig | undefined, id: string): string | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
/** Node-only focus includes incident links and endpoint nodes; no flow filtering. */
|
|
3
|
+
export declare function SankeyFocusMark({ nodeKey, endpoints, adjacent, children, }: {
|
|
4
|
+
nodeKey?: string;
|
|
5
|
+
endpoints?: readonly string[];
|
|
6
|
+
adjacent: ReadonlyMap<string, ReadonlySet<string>>;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useChartInteraction } from "./chart-interaction.js";
|
|
4
|
+
import { useEmphasis } from "./emphasis.js";
|
|
5
|
+
/** Node-only focus includes incident links and endpoint nodes; no flow filtering. */
|
|
6
|
+
export function SankeyFocusMark({ nodeKey, endpoints, adjacent, children, }) {
|
|
7
|
+
const interaction = useChartInteraction();
|
|
8
|
+
const key = nodeKey ?? endpoints?.[0] ?? "";
|
|
9
|
+
const emphasis = useEmphasis({ kind: "series", key, scope: "sankey", seriesKey: key });
|
|
10
|
+
const inspected = emphasis.active?.kind === "series" ? emphasis.active.key : null;
|
|
11
|
+
const focus = inspected ?? interaction.selected;
|
|
12
|
+
const related = focus === null ||
|
|
13
|
+
(nodeKey !== undefined
|
|
14
|
+
? nodeKey === focus || adjacent.get(focus)?.has(nodeKey)
|
|
15
|
+
: endpoints?.includes(focus));
|
|
16
|
+
const interactive = nodeKey !== undefined && interaction.markActivation && interaction.eligible.includes(nodeKey);
|
|
17
|
+
return (
|
|
18
|
+
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useAriaPropsSupportedByRole: Role, pressed state and handlers are enabled together for opted-in SVG controls.
|
|
19
|
+
_jsx("g", { "data-kind-ui": "sankey-focus-mark", "data-node": nodeKey, role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, "aria-label": interactive ? `Highlight ${nodeKey}` : undefined, "aria-pressed": interactive ? interaction.selected === nodeKey : undefined, onPointerEnter: (event) => {
|
|
20
|
+
if (event.pointerType !== "touch")
|
|
21
|
+
emphasis.enter("pointer");
|
|
22
|
+
}, onPointerLeave: () => emphasis.leave("pointer"), onPointerCancel: () => emphasis.leave("pointer"), onFocus: () => emphasis.enter("keyboard"), onBlur: () => emphasis.leave("keyboard"), onKeyDown: (event) => {
|
|
23
|
+
if (interactive &&
|
|
24
|
+
event.target === event.currentTarget &&
|
|
25
|
+
!event.defaultPrevented &&
|
|
26
|
+
(event.key === "Enter" || event.key === " ")) {
|
|
27
|
+
if (!event.repeat)
|
|
28
|
+
interaction.activate({ kind: "node", key: nodeKey }, "mark", event);
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
}
|
|
31
|
+
}, children: _jsx("g", { style: { opacity: related ? 1 : 0.28 }, children: children }) }));
|
|
32
|
+
}
|
package/dist/sankey-legend.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { SankeyNodeConfig } from "./sankey-colors.js";
|
|
|
3
3
|
export type SankeyLegendProps = Omit<ComponentPropsWithRef<"ul">, "children"> & {
|
|
4
4
|
/** Share this explicit reference with SankeyChart.nodeConfig. Config order owns legend order. */
|
|
5
5
|
config: SankeyNodeConfig;
|
|
6
|
+
interactionBinding?: "root";
|
|
6
7
|
children?: (item: {
|
|
7
8
|
key: string;
|
|
8
9
|
label: string;
|
|
@@ -11,4 +12,4 @@ export type SankeyLegendProps = Omit<ComponentPropsWithRef<"ul">, "children"> &
|
|
|
11
12
|
}) => ReactNode;
|
|
12
13
|
};
|
|
13
14
|
/** Node identity legend; solid links inherit source color, gradient links use both endpoints. */
|
|
14
|
-
export declare function SankeyLegend({ config, children, ...props }: SankeyLegendProps): import("react").JSX.Element;
|
|
15
|
+
export declare function SankeyLegend({ config, interactionBinding, children, ...props }: SankeyLegendProps): import("react").JSX.Element;
|
package/dist/sankey-legend.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useOptionalChartInteraction } from "./chart-interaction.js";
|
|
3
4
|
/** Node identity legend; solid links inherit source color, gradient links use both endpoints. */
|
|
4
|
-
export function SankeyLegend({ config, children, ...props }) {
|
|
5
|
-
|
|
5
|
+
export function SankeyLegend({ config, interactionBinding, children, ...props }) {
|
|
6
|
+
const interaction = useOptionalChartInteraction();
|
|
7
|
+
if (interactionBinding &&
|
|
8
|
+
(!interaction || interaction.kind !== "node" || interaction.mode !== "focus"))
|
|
9
|
+
throw new Error("SankeyLegend Root interaction binding requires node focus");
|
|
10
|
+
return (
|
|
11
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: Delegates clicks from native buttons, which already implement Enter and Space.
|
|
12
|
+
_jsx("ul", { "aria-label": "Chart legend", ...props, "data-kind-ui": "chart-legend", onClick: (event) => {
|
|
13
|
+
props.onClick?.(event);
|
|
14
|
+
const button = event.target.closest("button[data-node-key]");
|
|
15
|
+
if (interactionBinding &&
|
|
16
|
+
button?.dataset.nodeKey !== undefined &&
|
|
17
|
+
event.currentTarget.contains(button))
|
|
18
|
+
interaction?.activate({ kind: "node", key: button.dataset.nodeKey }, "legend", event);
|
|
19
|
+
}, children: Object.entries(config).map(([key, item]) => {
|
|
6
20
|
const marker = (_jsx("span", { "aria-hidden": "true", "data-kind-ui": "chart-indicator", style: { "--kind-ui-chart-indicator-color": item.color } }));
|
|
7
|
-
|
|
21
|
+
const content = children ? (children({ key, label: item.label, color: item.color, marker })) : (_jsxs(_Fragment, { children: [marker, item.label] }));
|
|
22
|
+
return (_jsx("li", { "data-kind-ui": "chart-legend-item", "data-node": key, children: interactionBinding && interaction?.eligible.includes(key) ? (_jsx("button", { type: "button", "aria-pressed": interaction.selected === key, "data-node-key": key, children: content })) : (_jsx("span", { children: content })) }, key));
|
|
8
23
|
}) }));
|
|
9
24
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from "react";
|
|
2
|
+
import type { SankeyNodeConfig } from "./sankey-colors.js";
|
|
3
|
+
import { type SankeyFlowData } from "./sankey-data.js";
|
|
4
|
+
import type { SankeyNodeProps } from "./sankey-marks.js";
|
|
5
|
+
export type SankeyNodeLabelProps = Omit<ComponentPropsWithRef<"text">, "position"> & {
|
|
6
|
+
/** Geometry from the native node callback; identity comes from payload.id, never index. */
|
|
7
|
+
node: Pick<SankeyNodeProps, "x" | "y" | "width" | "height" | "payload">;
|
|
8
|
+
data: SankeyFlowData;
|
|
9
|
+
/** Explicit metadata; icons are looked up by payload.id. Labels still use data names. */
|
|
10
|
+
nodeConfig?: SankeyNodeConfig;
|
|
11
|
+
/** Square SVG viewport size and gap in chart units; inside icons share the node clip. */
|
|
12
|
+
iconSize?: number;
|
|
13
|
+
iconGap?: number;
|
|
14
|
+
position?: "inside" | "outside";
|
|
15
|
+
/** Outside defaults to left for sinks and right otherwise. Reserve chart margins for text. */
|
|
16
|
+
side?: "left" | "right";
|
|
17
|
+
showValues?: boolean;
|
|
18
|
+
valueFormatter?: (value: number) => string;
|
|
19
|
+
offset?: number;
|
|
20
|
+
};
|
|
21
|
+
/** Explicit sibling of SankeyNode; preserves native renderer and consumer text ownership. */
|
|
22
|
+
export declare function SankeyNodeLabel({ node, data, nodeConfig, iconSize, iconGap, position, side, showValues, valueFormatter, offset, children, ...textProps }: SankeyNodeLabelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { prepareSankeyData } from "./sankey-data.js";
|
|
5
|
+
/** Explicit sibling of SankeyNode; preserves native renderer and consumer text ownership. */
|
|
6
|
+
export function SankeyNodeLabel({ node, data, nodeConfig, iconSize = 16, iconGap = 4, position = "outside", side, showValues = false, valueFormatter = String, offset = 8, children, ...textProps }) {
|
|
7
|
+
const clipId = `sankey-label-${useId().replace(/:/g, "")}`;
|
|
8
|
+
const validated = prepareSankeyData(data);
|
|
9
|
+
const index = validated.nodes.findIndex((item) => item.id === node.payload.id);
|
|
10
|
+
const item = validated.nodes[index];
|
|
11
|
+
if (!item)
|
|
12
|
+
throw new Error(`SankeyNodeLabel requires node id "${node.payload.id}" in data`);
|
|
13
|
+
if (![node.x, node.y, node.width, node.height, offset, iconSize, iconGap].every(Number.isFinite) ||
|
|
14
|
+
node.width < 0 ||
|
|
15
|
+
node.height < 0 ||
|
|
16
|
+
offset < 0 ||
|
|
17
|
+
iconSize < 0 ||
|
|
18
|
+
iconGap < 0)
|
|
19
|
+
throw new Error("SankeyNodeLabel requires finite geometry and nonnegative dimensions/offset/icon size/gap");
|
|
20
|
+
const incoming = validated.links.reduce((sum, link) => sum + (link.target === index ? link.value : 0), 0);
|
|
21
|
+
const outgoing = validated.links.reduce((sum, link) => sum + (link.source === index ? link.value : 0), 0);
|
|
22
|
+
const value = Math.max(incoming, outgoing);
|
|
23
|
+
const label = showValues ? `${item.name}: ${valueFormatter(value)}` : item.name;
|
|
24
|
+
const left = (side ?? (validated.links.some((link) => link.source === index) ? "right" : "left")) === "left";
|
|
25
|
+
const inside = position === "inside";
|
|
26
|
+
const icon = nodeConfig && Object.hasOwn(nodeConfig, item.id) ? nodeConfig[item.id]?.icon : undefined;
|
|
27
|
+
const hasIcon = icon !== undefined && icon !== null && typeof icon !== "boolean" && iconSize > 0;
|
|
28
|
+
const iconSpace = hasIcon ? iconSize + iconGap : 0;
|
|
29
|
+
const edge = left ? node.x - offset : node.x + node.width + offset;
|
|
30
|
+
// Inside uses a centered icon above centered text; outside puts the icon nearest the node.
|
|
31
|
+
const iconX = inside ? node.x + (node.width - iconSize) / 2 : left ? edge - iconSize : edge;
|
|
32
|
+
const centerY = node.y + node.height / 2;
|
|
33
|
+
return (_jsxs("g", { "data-kind-ui": "sankey-node-label", "data-node-id": item.id, children: [_jsx("title", { children: label }), inside && (_jsx("defs", { children: _jsx("clipPath", { id: clipId, children: _jsx("rect", { x: node.x, y: node.y, width: node.width, height: node.height }) }) })), _jsxs("g", { clipPath: inside ? `url(#${clipId})` : undefined, children: [hasIcon && (_jsx("svg", { "data-kind-ui": "sankey-node-icon", x: iconX, y: inside ? centerY - iconSize - iconGap / 2 : centerY - iconSize / 2, width: iconSize, height: iconSize, viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", pointerEvents: "none", overflow: "hidden", children: icon })), _jsx("text", { x: inside ? node.x + node.width / 2 : left ? edge - iconSpace : edge + iconSpace, y: inside && hasIcon ? centerY + iconGap / 2 + 6 : centerY, textAnchor: inside ? "middle" : left ? "end" : "start", dominantBaseline: "middle", pointerEvents: "none", fontSize: 12, ...textProps, children: children === undefined ? label : children })] })] }));
|
|
34
|
+
}
|
package/dist/scatter-chart.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { type MotionValue } from "motion/react";
|
|
2
2
|
import { type ComponentProps } from "react";
|
|
3
3
|
import { ScatterChart as EngineScatterChart } from "recharts";
|
|
4
|
-
import { type
|
|
5
|
-
export type ScatterAnimation =
|
|
4
|
+
import { type BaseAnimation } from "./animation.js";
|
|
5
|
+
export type ScatterAnimation = BaseAnimation;
|
|
6
6
|
export type ScatterChartProps = ComponentProps<typeof EngineScatterChart> & {
|
|
7
7
|
animate?: boolean | ScatterAnimation | undefined;
|
|
8
|
+
loading?: boolean | undefined;
|
|
9
|
+
loadingLabel?: string | undefined;
|
|
8
10
|
};
|
|
9
11
|
export declare const ScatterMotion: import("react").Context<{
|
|
10
12
|
reveal: boolean;
|
|
@@ -13,4 +15,4 @@ export declare const ScatterMotion: import("react").Context<{
|
|
|
13
15
|
progress: MotionValue<number> | null;
|
|
14
16
|
}>;
|
|
15
17
|
/** Native numeric geometry and item selection, with optional entrance fade and shared tooltip Motion. */
|
|
16
|
-
export declare function ScatterChart({ animate, children, ...props }: ScatterChartProps): import("react").JSX.Element;
|
|
18
|
+
export declare function ScatterChart({ animate, loading, loadingLabel, children, ...props }: ScatterChartProps): import("react").JSX.Element;
|
package/dist/scatter-chart.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createContext, useCallback, useLayoutEffect, useRef, useState, useSyncE
|
|
|
5
5
|
import { ScatterChart as EngineScatterChart } from "recharts";
|
|
6
6
|
import { MotionContext } from "./animation.js";
|
|
7
7
|
import { LineChartFrame } from "./line-chart.js";
|
|
8
|
+
import { CartesianLoadingDesign } from "./loading-cartesian-designs.js";
|
|
8
9
|
export const ScatterMotion = createContext({
|
|
9
10
|
reveal: false,
|
|
10
11
|
options: {},
|
|
@@ -21,11 +22,20 @@ const snapshot = () => window.matchMedia(query).matches;
|
|
|
21
22
|
const serverSnapshot = () => true;
|
|
22
23
|
const defaultHover = { type: "spring", stiffness: 210, damping: 28, mass: 0.8 };
|
|
23
24
|
/** Native numeric geometry and item selection, with optional entrance fade and shared tooltip Motion. */
|
|
24
|
-
export function ScatterChart({ animate = false, children, ...props }) {
|
|
25
|
+
export function ScatterChart({ animate = false, loading, loadingLabel, children, ...props }) {
|
|
25
26
|
const reduced = useSyncExternalStore(subscribe, snapshot, serverSnapshot);
|
|
26
27
|
const [interacted, setInteracted] = useState(false);
|
|
27
|
-
const enabled = animate !== false && !reduced;
|
|
28
|
-
const interact = useCallback(() =>
|
|
28
|
+
const enabled = animate !== false && !reduced && !loading;
|
|
29
|
+
const interact = useCallback(() => {
|
|
30
|
+
if (!loading)
|
|
31
|
+
setInteracted(true);
|
|
32
|
+
}, [loading]);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
if (loading) {
|
|
35
|
+
setInteracted(false);
|
|
36
|
+
started.current = false;
|
|
37
|
+
}
|
|
38
|
+
}, [loading]);
|
|
29
39
|
const finish = useCallback(() => {
|
|
30
40
|
if (enabled)
|
|
31
41
|
setInteracted(true);
|
|
@@ -60,5 +70,5 @@ export function ScatterChart({ animate = false, children, ...props }) {
|
|
|
60
70
|
});
|
|
61
71
|
return () => controls.stop();
|
|
62
72
|
}, [reveal, duration, easing, progress, interact]);
|
|
63
|
-
return (_jsx(MotionContext, { value: { enabled, transition: options.hoverTransition ?? defaultHover }, children: _jsx(ScatterMotion, { value: { reveal, options, finish, progress }, children: _jsx("div", { style: { display: "contents" }, onFocusCapture: interact, onPointerDownCapture: interact, onPointerMoveCapture: interact, onKeyDownCapture: interact, children: _jsx(LineChartFrame, { chartProps: props, engine: EngineScatterChart, motionEnabled: enabled, interrupt: finish, children: children }) }) }) }));
|
|
73
|
+
return (_jsx(MotionContext, { value: { enabled, transition: options.hoverTransition ?? defaultHover }, children: _jsx(ScatterMotion, { value: { reveal, options, finish, progress }, children: _jsx("div", { style: { display: "contents" }, onFocusCapture: interact, onPointerDownCapture: interact, onPointerMoveCapture: interact, onKeyDownCapture: interact, children: _jsx(LineChartFrame, { loading: loading, loadingLabel: loadingLabel, loadingSkeleton: "scatter", loadingDesign: (seed) => _jsx(CartesianLoadingDesign, { family: "scatter", seed: seed }), loadingAnimation: options, chartProps: props, engine: EngineScatterChart, motionEnabled: enabled, interrupt: finish, children: children }) }) }) }));
|
|
64
74
|
}
|
package/dist/scatter-series.js
CHANGED
|
@@ -7,6 +7,7 @@ import { useChart } from "./chart-context.js";
|
|
|
7
7
|
import { useLineInteraction } from "./line-chart.js";
|
|
8
8
|
import { ScatterMotion } from "./scatter-chart.js";
|
|
9
9
|
import { ScatterMaterialSymbol } from "./scatter-material.js";
|
|
10
|
+
import { SeriesInteractionLayer, useSeriesInteraction } from "./series-interaction.js";
|
|
10
11
|
// Delay by native screen X, rather than sorting or changing quantitative data.
|
|
11
12
|
function EntranceSymbol({ material, type, ...props }) {
|
|
12
13
|
const { progress } = use(ScatterMotion);
|
|
@@ -20,7 +21,7 @@ function EntranceSymbol({ material, type, ...props }) {
|
|
|
20
21
|
}
|
|
21
22
|
/** Native Scatter owns marks, Cells, labels, handlers, axis mapping and point payloads. */
|
|
22
23
|
export function ScatterSeries({ seriesKey, hide, fill, material = "plain", ...props }) {
|
|
23
|
-
const { config, visibleSeries } = useChart();
|
|
24
|
+
const { config, paints, visibleSeries } = useChart();
|
|
24
25
|
const { registerSeries, invalidate } = useLineInteraction();
|
|
25
26
|
const shapes = useMemo(() => {
|
|
26
27
|
function finish(option) {
|
|
@@ -69,6 +70,7 @@ export function ScatterSeries({ seriesKey, hide, fill, material = "plain", ...pr
|
|
|
69
70
|
}, [id, key, registerSeries]);
|
|
70
71
|
if (key === undefined && visibleSeries !== undefined)
|
|
71
72
|
throw new Error("ScatterSeries requires seriesKey for controlled non-string dataKey");
|
|
72
|
-
const
|
|
73
|
-
|
|
73
|
+
const interaction = useSeriesInteraction(key, hidden, hide === true, props.data, props.onClick);
|
|
74
|
+
const color = fill ?? (key && Object.hasOwn(config, key) ? paints[key] : undefined);
|
|
75
|
+
return (_jsx(ZIndexLayer, { zIndex: props.zIndex ?? DefaultZIndexes.scatter, children: _jsx(SeriesInteractionLayer, { seriesKey: key, hidden: hidden, children: _jsx("g", { "data-kind-ui": "scatter-fade", children: _jsx(Scatter, { ...props, ...(interaction.onClick !== undefined ? { onClick: interaction.onClick } : {}), ...(shapes.shape !== undefined ? { shape: shapes.shape } : {}), ...(shapes.activeShape !== undefined ? { activeShape: shapes.activeShape } : {}), id: id, hide: hidden, ...(color !== undefined ? { fill: color } : {}), zIndex: 0, isAnimationActive: false }) }) }) }));
|
|
74
76
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SeriesColor } from "./types.js";
|
|
2
|
+
export type ColorStops = {
|
|
3
|
+
offsets: number[];
|
|
4
|
+
colors: string[];
|
|
5
|
+
};
|
|
6
|
+
/** Preserve each theme's evenly spaced stops, even with unequal stop counts. */
|
|
7
|
+
export declare function resolveSeriesColor(color: SeriesColor, key: string): ColorStops;
|
|
8
|
+
export declare function colorResourceId(id: string, key: string): string;
|
|
9
|
+
/** Disjoint from legacy --color-<key>, whose valid keys include numeric suffixes. */
|
|
10
|
+
export declare function colorStopToken(key: string, index: number | "gradient"): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function stops(value, path) {
|
|
2
|
+
const values = typeof value === "string" ? [value] : value;
|
|
3
|
+
if (!Array.isArray(values) || values.length === 0)
|
|
4
|
+
throw new Error(`Series color ${path} requires a nonempty string or nonempty array of color strings`);
|
|
5
|
+
for (let index = 0; index < values.length; index++) {
|
|
6
|
+
if (typeof values[index] !== "string" || values[index].trim() === "")
|
|
7
|
+
throw new Error(`Series color ${path}${Array.isArray(value) ? `[${index}]` : ""} requires a nonempty color string`);
|
|
8
|
+
}
|
|
9
|
+
return values;
|
|
10
|
+
}
|
|
11
|
+
function at(colors, offset) {
|
|
12
|
+
if (colors.length === 1)
|
|
13
|
+
return colors[0];
|
|
14
|
+
const position = offset * (colors.length - 1);
|
|
15
|
+
const index = Math.floor(position);
|
|
16
|
+
const fraction = position - index;
|
|
17
|
+
if (fraction < 1e-10)
|
|
18
|
+
return colors[index];
|
|
19
|
+
return `color-mix(in srgb, ${colors[index]} ${(1 - fraction) * 100}%, ${colors[index + 1]} ${fraction * 100}%)`;
|
|
20
|
+
}
|
|
21
|
+
/** Preserve each theme's evenly spaced stops, even with unequal stop counts. */
|
|
22
|
+
export function resolveSeriesColor(color, key) {
|
|
23
|
+
const path = `config[${JSON.stringify(key)}].color`;
|
|
24
|
+
const themed = color !== null && typeof color === "object" && !Array.isArray(color);
|
|
25
|
+
if (themed &&
|
|
26
|
+
(!Object.hasOwn(color, "light") ||
|
|
27
|
+
!Object.hasOwn(color, "dark") ||
|
|
28
|
+
Object.keys(color).some((key) => key !== "light" && key !== "dark")))
|
|
29
|
+
throw new Error(`Series color ${path} requires both light and dark definitions and no other fields`);
|
|
30
|
+
const variants = color;
|
|
31
|
+
const light = stops(themed ? variants.light : color, themed ? `${path}.light` : path);
|
|
32
|
+
const dark = stops(themed ? variants.dark : color, themed ? `${path}.dark` : path);
|
|
33
|
+
const offsets = [
|
|
34
|
+
...new Set([light, dark].flatMap((values) => values.length === 1 ? [0] : values.map((_, index) => index / (values.length - 1)))),
|
|
35
|
+
].sort((a, b) => a - b);
|
|
36
|
+
return {
|
|
37
|
+
offsets,
|
|
38
|
+
colors: offsets.map((offset) => themed ? `light-dark(${at(light, offset)}, ${at(dark, offset)})` : at(light, offset)),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function colorResourceId(id, key) {
|
|
42
|
+
return `kind-ui-color-${Array.from(`${id}:${key}`, (char) => char.codePointAt(0)?.toString(16)).join("-")}`;
|
|
43
|
+
}
|
|
44
|
+
/** Disjoint from legacy --color-<key>, whose valid keys include numeric suffixes. */
|
|
45
|
+
export function colorStopToken(key, index) {
|
|
46
|
+
const encoded = Array.from(key, (char) => char.codePointAt(0)?.toString(16)).join("-");
|
|
47
|
+
return `--kind-ui-series-${encoded}-${index}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type KeyboardEventHandler, type ReactNode } from "react";
|
|
2
|
+
/** Native handlers retain their original data/index/event tuple and run first. */
|
|
3
|
+
export declare function useSeriesInteraction<Args extends unknown[]>(key: string | undefined, hidden: boolean, nativeHidden: boolean | undefined, seriesData: readonly unknown[] | undefined, handler: ((...args: Args) => void) | undefined): {
|
|
4
|
+
onClick: ((...args: Args) => void) | undefined;
|
|
5
|
+
};
|
|
6
|
+
/** One keyboard target per logical series, within its native ZIndex portal. */
|
|
7
|
+
export declare function SeriesInteractionLayer({ seriesKey, hidden, children, onKeyDown, }: {
|
|
8
|
+
seriesKey: string | undefined;
|
|
9
|
+
hidden: boolean;
|
|
10
|
+
onKeyDown?: KeyboardEventHandler<SVGElement> | undefined;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, } from "react";
|
|
4
|
+
import { useChart } from "./chart-context.js";
|
|
5
|
+
import { useChartInteraction, useInteractionAvailability, useInteractionRegistration, } from "./chart-interaction.js";
|
|
6
|
+
import { useEmphasis } from "./emphasis.js";
|
|
7
|
+
import { useChartKeyboard, useLineInteraction } from "./line-chart.js";
|
|
8
|
+
/** Native handlers retain their original data/index/event tuple and run first. */
|
|
9
|
+
export function useSeriesInteraction(key, hidden, nativeHidden = false, seriesData, handler) {
|
|
10
|
+
const interaction = useChartInteraction();
|
|
11
|
+
const { data } = useLineInteraction();
|
|
12
|
+
const { visibleSeries } = useChart();
|
|
13
|
+
const rootHidden = key !== undefined && visibleSeries !== undefined && !visibleSeries.includes(key);
|
|
14
|
+
// Some consumers mirror Root visibility into native hide. Their hidden IDs stay restorable.
|
|
15
|
+
useInteractionAvailability(interaction.kind === "series" ? key : undefined, nativeHidden && !rootHidden);
|
|
16
|
+
useInteractionRegistration(interaction.kind === "series" && key !== undefined && (seriesData ?? data)?.length ? [key] : []);
|
|
17
|
+
const interactive = key !== undefined &&
|
|
18
|
+
!hidden &&
|
|
19
|
+
interaction.kind === "series" &&
|
|
20
|
+
interaction.interactive &&
|
|
21
|
+
interaction.markActivation &&
|
|
22
|
+
interaction.eligible.includes(key);
|
|
23
|
+
const latest = useRef({ handler, interaction, interactive, key });
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
latest.current = { handler, interaction, interactive, key };
|
|
26
|
+
});
|
|
27
|
+
const onClick = useCallback((...args) => {
|
|
28
|
+
latest.current.handler?.(...args);
|
|
29
|
+
const current = latest.current;
|
|
30
|
+
const event = args[args.length - 1];
|
|
31
|
+
if (current.interactive && current.key !== undefined && event?.button === 0)
|
|
32
|
+
current.interaction.activate({ kind: "series", key: current.key }, "mark", event);
|
|
33
|
+
}, []);
|
|
34
|
+
// Native animation IDs include handler identity even when native animation is disabled.
|
|
35
|
+
return { onClick: interactive ? onClick : handler };
|
|
36
|
+
}
|
|
37
|
+
/** One keyboard target per logical series, within its native ZIndex portal. */
|
|
38
|
+
export function SeriesInteractionLayer({ seriesKey, hidden, children, onKeyDown, }) {
|
|
39
|
+
const interaction = useChartInteraction();
|
|
40
|
+
const { config } = useChart();
|
|
41
|
+
const enabled = seriesKey !== undefined && !hidden && interaction.kind === "series" && interaction.configured;
|
|
42
|
+
const emphasis = useEmphasis({ kind: "series", key: seriesKey ?? "", scope: "series", seriesKey }, enabled);
|
|
43
|
+
const keyboard = useChartKeyboard();
|
|
44
|
+
const interactive = enabled &&
|
|
45
|
+
interaction.interactive &&
|
|
46
|
+
interaction.markActivation &&
|
|
47
|
+
interaction.eligible.includes(seriesKey);
|
|
48
|
+
// Native axis inspection includes all series at this category, so keep their paint readable.
|
|
49
|
+
const nativeInspection = keyboard &&
|
|
50
|
+
typeof document !== "undefined" &&
|
|
51
|
+
document.activeElement?.getAttribute("role") === "application";
|
|
52
|
+
const factor = nativeInspection && emphasis.active === null ? 1 : emphasis.factor;
|
|
53
|
+
return (
|
|
54
|
+
// biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useAriaPropsSupportedByRole: Role, pressed state and handlers are enabled together for opted-in SVG controls.
|
|
55
|
+
_jsx("g", { "data-kind-ui": "series-interaction", "data-series": seriesKey, role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, "aria-label": interactive
|
|
56
|
+
? `${interaction.mode === "focus" ? "Highlight" : "Toggle"} ${config[seriesKey]?.label ?? seriesKey}`
|
|
57
|
+
: undefined, "aria-pressed": interactive
|
|
58
|
+
? interaction.mode === "focus"
|
|
59
|
+
? interaction.selected === seriesKey
|
|
60
|
+
: (interaction.visible?.includes(seriesKey) ?? true)
|
|
61
|
+
: undefined, onPointerEnter: (event) => {
|
|
62
|
+
if (event.pointerType !== "touch")
|
|
63
|
+
emphasis.enter("pointer");
|
|
64
|
+
}, onPointerMove: (event) => {
|
|
65
|
+
if (event.pointerType !== "touch")
|
|
66
|
+
emphasis.enter("pointer");
|
|
67
|
+
}, onPointerLeave: () => emphasis.leave("pointer"), onPointerCancel: () => emphasis.leave("pointer"), onFocus: (event) => {
|
|
68
|
+
if (event.target.matches(":focus-visible"))
|
|
69
|
+
emphasis.enter("keyboard");
|
|
70
|
+
}, onBlur: () => emphasis.leave("keyboard"), onKeyDown: (event) => {
|
|
71
|
+
if (event.target === event.currentTarget)
|
|
72
|
+
onKeyDown?.(event);
|
|
73
|
+
if (interactive &&
|
|
74
|
+
event.target === event.currentTarget &&
|
|
75
|
+
!event.defaultPrevented &&
|
|
76
|
+
(event.key === "Enter" || event.key === " ")) {
|
|
77
|
+
if (!event.repeat)
|
|
78
|
+
interaction.activate({ kind: "series", key: seriesKey }, "mark", event);
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
}
|
|
81
|
+
}, children: _jsx("g", { "data-kind-ui": "series-interaction-paint", style: { opacity: factor }, children: children }) }));
|
|
82
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SeriesConfig } from "./types.js";
|
|
2
|
+
/** Presentation only: never rewrite metadata identity, paint or consumer data. */
|
|
3
|
+
export declare function resolveSeriesLabel(key: string, label?: string): string;
|
|
4
|
+
export type ResolvedSeriesConfig = Readonly<Record<string, SeriesConfig[string] & {
|
|
5
|
+
label: string;
|
|
6
|
+
}>>;
|
|
7
|
+
export declare function resolveSeriesConfig(config: SeriesConfig): ResolvedSeriesConfig;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Presentation only: never rewrite metadata identity, paint or consumer data. */
|
|
2
|
+
export function resolveSeriesLabel(key, label) {
|
|
3
|
+
if (label !== undefined)
|
|
4
|
+
return label;
|
|
5
|
+
const words = key
|
|
6
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1 $2")
|
|
7
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
8
|
+
.replace(/[_-]+/g, " ")
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
12
|
+
}
|
|
13
|
+
export function resolveSeriesConfig(config) {
|
|
14
|
+
return Object.fromEntries(Object.entries(config).map(([key, item]) => [
|
|
15
|
+
key,
|
|
16
|
+
{ ...item, label: resolveSeriesLabel(key, item.label) },
|
|
17
|
+
]));
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Each native SVG owns its viewport-relative resources; metadata stays Root-owned. */
|
|
3
|
+
export declare function SeriesPaintBoundary({ children }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
}): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
6
|
+
export declare function SeriesColorDefinitions({ viewport }: {
|
|
7
|
+
viewport?: boolean;
|
|
8
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { use, useId, useMemo } from "react";
|
|
4
|
+
import { ChartContext } from "./chart-context.js";
|
|
5
|
+
import { colorResourceId, colorStopToken } from "./series-color.js";
|
|
6
|
+
/** Each native SVG owns its viewport-relative resources; metadata stays Root-owned. */
|
|
7
|
+
export function SeriesPaintBoundary({ children }) {
|
|
8
|
+
const chart = use(ChartContext);
|
|
9
|
+
const colorId = useId();
|
|
10
|
+
const colorStops = chart?.colorStops;
|
|
11
|
+
const paints = useMemo(() => Object.fromEntries(Object.entries(colorStops ?? {}).map(([key, stops]) => [
|
|
12
|
+
key,
|
|
13
|
+
stops.colors.length > 1
|
|
14
|
+
? `url(#${colorResourceId(colorId, key)})`
|
|
15
|
+
: `var(--color-${key})`,
|
|
16
|
+
])), [colorStops, colorId]);
|
|
17
|
+
const value = useMemo(() => (chart ? { ...chart, colorId, paints } : null), [chart, colorId, paints]);
|
|
18
|
+
return value ? _jsx(ChartContext, { value: value, children: children }) : children;
|
|
19
|
+
}
|
|
20
|
+
export function SeriesColorDefinitions({ viewport = false }) {
|
|
21
|
+
const chart = use(ChartContext);
|
|
22
|
+
if (!chart)
|
|
23
|
+
return null;
|
|
24
|
+
return (_jsx("defs", { "data-kind-ui": "series-color-definitions", children: Object.entries(chart.colorStops)
|
|
25
|
+
.filter(([, stops]) => stops.colors.length > 1)
|
|
26
|
+
.map(([key, stops]) => (_jsx("linearGradient", { id: colorResourceId(chart.colorId, key), x1: "0", y1: "0", x2: viewport ? "100%" : "1", y2: "0", gradientUnits: viewport ? "userSpaceOnUse" : "objectBoundingBox", children: stops.offsets.map((offset, index) => (_jsx("stop", { offset: offset, stopColor: `var(${colorStopToken(key, index)})` }, offset))) }, key))) }));
|
|
27
|
+
}
|