@kind-ui/charts 0.1.1 → 0.3.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +683 -1
  3. package/dist/activity-rings.d.ts +2 -1
  4. package/dist/activity-rings.js +6 -3
  5. package/dist/animation.d.ts +11 -3
  6. package/dist/animation.js +18 -10
  7. package/dist/area-chart.d.ts +3 -1
  8. package/dist/area-chart.js +15 -10
  9. package/dist/area-series.d.ts +9 -1
  10. package/dist/area-series.js +35 -8
  11. package/dist/bar-chart.d.ts +10 -3
  12. package/dist/bar-chart.js +17 -4
  13. package/dist/bar-material.js +2 -3
  14. package/dist/bar-series.d.ts +11 -1
  15. package/dist/bar-series.js +65 -13
  16. package/dist/box-plot.js +2 -2
  17. package/dist/category-cells.d.ts +7 -1
  18. package/dist/category-cells.js +32 -2
  19. package/dist/chart-background-pattern.d.ts +23 -0
  20. package/dist/chart-background-pattern.js +31 -0
  21. package/dist/chart-context.d.ts +6 -2
  22. package/dist/chart-interaction.d.ts +94 -0
  23. package/dist/chart-interaction.js +224 -0
  24. package/dist/combo-chart.d.ts +8 -5
  25. package/dist/combo-chart.js +14 -12
  26. package/dist/configured-line-chart.d.ts +5 -1
  27. package/dist/configured-line-chart.js +8 -5
  28. package/dist/emphasis.d.ts +5 -2
  29. package/dist/emphasis.js +18 -4
  30. package/dist/fill-pattern.d.ts +25 -0
  31. package/dist/fill-pattern.js +23 -0
  32. package/dist/heatmap.d.ts +4 -2
  33. package/dist/heatmap.js +34 -12
  34. package/dist/histogram-chart.js +10 -3
  35. package/dist/histogram-material.js +1 -2
  36. package/dist/index.d.ts +12 -3
  37. package/dist/index.js +8 -1
  38. package/dist/legend.d.ts +2 -0
  39. package/dist/legend.js +28 -7
  40. package/dist/line-chart.d.ts +13 -3
  41. package/dist/line-chart.js +54 -34
  42. package/dist/line-dash.d.ts +8 -0
  43. package/dist/line-dash.js +18 -0
  44. package/dist/line-material.d.ts +1 -1
  45. package/dist/line-material.js +1 -1
  46. package/dist/line-series.d.ts +9 -1
  47. package/dist/line-series.js +37 -11
  48. package/dist/loading-cartesian-designs.d.ts +6 -0
  49. package/dist/loading-cartesian-designs.js +123 -0
  50. package/dist/loading-motion.d.ts +37 -0
  51. package/dist/loading-motion.js +88 -0
  52. package/dist/loading-polar-designs.d.ts +6 -0
  53. package/dist/loading-polar-designs.js +95 -0
  54. package/dist/loading-skeleton.d.ts +29 -0
  55. package/dist/loading-skeleton.js +143 -0
  56. package/dist/loading-standalone-designs.d.ts +5 -0
  57. package/dist/loading-standalone-designs.js +126 -0
  58. package/dist/percent-stack.d.ts +18 -0
  59. package/dist/percent-stack.js +33 -0
  60. package/dist/pie-chart.d.ts +7 -3
  61. package/dist/pie-chart.js +37 -6
  62. package/dist/pie-material.js +1 -1
  63. package/dist/pie-pin-identity.d.ts +4 -0
  64. package/dist/pie-pin-identity.js +10 -0
  65. package/dist/pie-series.d.ts +4 -0
  66. package/dist/pie-series.js +138 -15
  67. package/dist/pie-tooltip-pin.d.ts +4 -0
  68. package/dist/pie-tooltip-pin.js +52 -0
  69. package/dist/point-marker.d.ts +19 -0
  70. package/dist/point-marker.js +19 -0
  71. package/dist/polar-chart.d.ts +14 -5
  72. package/dist/polar-chart.js +41 -10
  73. package/dist/polar-material.js +1 -1
  74. package/dist/polar-series.js +68 -22
  75. package/dist/radar-interaction.js +10 -4
  76. package/dist/radial-category.d.ts +2 -0
  77. package/dist/reveal-clip.d.ts +16 -0
  78. package/dist/reveal-clip.js +18 -0
  79. package/dist/root.d.ts +5 -9
  80. package/dist/root.js +46 -11
  81. package/dist/sankey-chart.d.ts +5 -1
  82. package/dist/sankey-chart.js +64 -13
  83. package/dist/sankey-colors.d.ts +3 -0
  84. package/dist/sankey-finish.d.ts +1 -1
  85. package/dist/sankey-finish.js +1 -1
  86. package/dist/sankey-interaction.d.ts +8 -0
  87. package/dist/sankey-interaction.js +32 -0
  88. package/dist/sankey-legend.d.ts +2 -1
  89. package/dist/sankey-legend.js +18 -3
  90. package/dist/sankey-node-label.d.ts +22 -0
  91. package/dist/sankey-node-label.js +34 -0
  92. package/dist/scatter-chart.d.ts +5 -3
  93. package/dist/scatter-chart.js +14 -4
  94. package/dist/scatter-material.js +1 -1
  95. package/dist/scatter-series.js +5 -3
  96. package/dist/series-color.d.ts +10 -0
  97. package/dist/series-color.js +48 -0
  98. package/dist/series-interaction.d.ts +12 -0
  99. package/dist/series-interaction.js +82 -0
  100. package/dist/series-label.d.ts +7 -0
  101. package/dist/series-label.js +18 -0
  102. package/dist/series-paint.d.ts +8 -0
  103. package/dist/series-paint.js +27 -0
  104. package/dist/styles.css +151 -6
  105. package/dist/stylesheet-warning.d.ts +2 -0
  106. package/dist/stylesheet-warning.js +86 -0
  107. package/dist/surface-material.js +1 -1
  108. package/dist/tooltip-content.d.ts +8 -1
  109. package/dist/tooltip-content.js +15 -4
  110. package/dist/tooltip.d.ts +3 -1
  111. package/dist/tooltip.js +8 -5
  112. package/dist/types.d.ts +11 -2
  113. package/dist/waterfall-chart.d.ts +4 -0
  114. package/dist/waterfall-chart.js +7 -0
  115. package/package.json +2 -2
  116. package/dist/bar-paper.d.ts +0 -2
  117. package/dist/bar-paper.js +0 -6
@@ -5,25 +5,32 @@ import { animate, motion, useMotionValue } from "motion/react";
5
5
  import { memo, use, useId, useLayoutEffect, useRef, useState } from "react";
6
6
  import { DefaultZIndexes, Polygon, Radar, RadialBar, Sector, ZIndexLayer } from "recharts";
7
7
  import { ActiveMarker } from "./animation.js";
8
- import { categoryCells } from "./category-cells.js";
8
+ import { categoryCells, filterCategoryRows } from "./category-cells.js";
9
9
  import { useChart } from "./chart-context.js";
10
+ import { useChartInteraction, useInteractionFocus } from "./chart-interaction.js";
11
+ import { EmphasisMark } from "./emphasis.js";
10
12
  import { useLineInteraction } from "./line-chart.js";
11
13
  import { PolarMotion, RadarMotion, RadialMotion } from "./polar-chart.js";
12
14
  import { PolarMaterialFilter } from "./polar-material.js";
13
15
  import { RadarSelectionLayer, useRadarSelectionDot } from "./radar-interaction.js";
14
16
  import { RadialCategory } from "./radial-category.js";
17
+ import { SeriesInteractionLayer, useSeriesInteraction } from "./series-interaction.js";
15
18
  // Native Radar uses a props-identity animation key even with animation disabled.
16
19
  // Avoid replacing its polygon for unrelated frame state during a pointer press.
17
20
  const StableRadar = memo(Radar);
18
21
  const radarActiveDot = _jsx(ActiveMarker, {});
19
22
  function usePolarSeries(kind, props, geometry) {
20
- const { config, visibleSeries } = useChart();
23
+ const { config, paints, visibleSeries } = useChart();
24
+ const interactionOwner = useChartInteraction();
21
25
  const { registerSeries, invalidate } = useLineInteraction();
22
26
  const { reveal, options } = use(PolarMotion);
23
27
  const generatedId = useId();
24
28
  const id = props.id || generatedId;
25
29
  const key = props.seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined);
26
- const hide = props.hide === true || (visibleSeries !== undefined && !visibleSeries.includes(key ?? ""));
30
+ const hide = props.hide === true ||
31
+ (interactionOwner.kind === "series" &&
32
+ visibleSeries !== undefined &&
33
+ !visibleSeries.includes(key ?? ""));
27
34
  const previous = useRef([...geometry, hide]);
28
35
  useLayoutEffect(() => {
29
36
  const inputs = [...geometry, hide];
@@ -56,7 +63,7 @@ function usePolarSeries(kind, props, geometry) {
56
63
  }, [reveal, hide, opacity, duration, easing]);
57
64
  if (key === undefined && visibleSeries !== undefined)
58
65
  throw new Error(`${kind} requires seriesKey for controlled non-string dataKey`);
59
- const color = key !== undefined && Object.hasOwn(config, key) ? `var(--color-${key})` : undefined;
66
+ const color = key !== undefined && Object.hasOwn(config, key) ? paints[key] : undefined;
60
67
  return { id, hide, color, opacity };
61
68
  }
62
69
  // Recharts injects its original computed points and callback props into this renderer.
@@ -90,24 +97,50 @@ export function RadarSeries({ seriesKey, hide, stroke, fill, className, material
90
97
  props.baseLinePoints,
91
98
  props.isRange,
92
99
  ]);
100
+ const interaction = useSeriesInteraction(seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined), series.hide, hide === true, undefined, props.onClick);
93
101
  const selectionDot = useRadarSelectionDot(props.dot, seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined));
94
102
  const filterId = `kind-ui-polar-${useId().replace(/[^a-zA-Z0-9_-]/g, "_")}`;
95
103
  const materialized = material !== "plain" &&
96
104
  props.shape === undefined &&
97
105
  props.filter === undefined &&
98
106
  props.style?.filter === undefined;
99
- return (_jsxs(ZIndexLayer, { zIndex: props.zIndex ?? DefaultZIndexes.area, children: [materialized && (_jsxs(_Fragment, { children: [_jsx(PolarMaterialFilter, { material: material, id: filterId }), _jsx("style", { children: `.${filterId} .recharts-radar-polygon > .recharts-polygon { filter: url(#${filterId}); }` })] })), _jsx(RadarSelectionLayer, { seriesKey: seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined), hidden: series.hide, children: _jsx(motion.g, { "data-kind-ui": "radar-reveal", initial: false, children: _jsx(StableRadar, { activeDot: radarActiveDot, ...props, ...(props.shape === undefined ? { shape: renderRadarEntrance } : {}), ...(selectionDot !== undefined ? { dot: selectionDot } : {}), id: series.id, hide: series.hide, isAnimationActive: false, zIndex: 0, ...(stroke !== undefined
100
- ? { stroke }
101
- : series.color !== undefined
102
- ? { stroke: series.color }
103
- : {}), ...(fill !== undefined
104
- ? { fill }
105
- : series.color !== undefined
106
- ? { fill: series.color }
107
- : {}), className: ["kind-ui-radar-series", filterId, className].filter(Boolean).join(" ") }) }) })] }));
107
+ return (_jsxs(ZIndexLayer, { zIndex: props.zIndex ?? DefaultZIndexes.area, children: [materialized && (_jsxs(_Fragment, { children: [_jsx(PolarMaterialFilter, { material: material, id: filterId }), _jsx("style", { children: `.${filterId} .recharts-radar-polygon > .recharts-polygon { filter: url(#${filterId}); }` })] })), _jsx(SeriesInteractionLayer, { seriesKey: seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined), hidden: series.hide, children: _jsx(RadarSelectionLayer, { seriesKey: seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined), hidden: series.hide, children: _jsx(motion.g, { "data-kind-ui": "radar-reveal", initial: false, children: _jsx(StableRadar, { activeDot: radarActiveDot, ...props, ...(interaction.onClick !== undefined ? { onClick: interaction.onClick } : {}), ...(props.shape === undefined ? { shape: renderRadarEntrance } : {}), ...(selectionDot !== undefined ? { dot: selectionDot } : {}), id: series.id, hide: series.hide, isAnimationActive: false, zIndex: 0, ...(stroke !== undefined
108
+ ? { stroke }
109
+ : series.color !== undefined
110
+ ? { stroke: series.color }
111
+ : {}), ...(fill !== undefined
112
+ ? { fill }
113
+ : series.color !== undefined
114
+ ? { fill: series.color }
115
+ : {}), className: ["kind-ui-radar-series", filterId, className].filter(Boolean).join(" ") }) }) }) })] }));
108
116
  }
109
117
  // Recharts computes every arc; the entrance masks its paint without changing its path.
110
118
  function RadialEntranceSector(props) {
119
+ const categories = use(RadialCategory);
120
+ const interaction = useChartInteraction();
121
+ const key = categories?.bound ? categories.key(props.payload) : undefined;
122
+ const content = useRadialSector(props);
123
+ const interactive = key !== undefined &&
124
+ interaction.interactive &&
125
+ interaction.markActivation &&
126
+ interaction.eligible.includes(key);
127
+ const focusRef = useInteractionFocus(key, interactive);
128
+ return key === undefined ? (content) : (_jsx(EmphasisMark, { ref: focusRef, "data-interaction-focus-key": interactive ? key : undefined, target: { kind: "category", key, scope: "radial", seriesKey: key }, role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, "aria-label": interactive ? `${interaction.mode === "focus" ? "Highlight" : "Toggle"} ${key}` : undefined, "aria-pressed": interactive
129
+ ? interaction.mode === "focus"
130
+ ? interaction.selected === key
131
+ : true
132
+ : undefined, onKeyDown: (event) => {
133
+ if (interactive &&
134
+ event.target === event.currentTarget &&
135
+ !event.defaultPrevented &&
136
+ (event.key === "Enter" || event.key === " ")) {
137
+ if (!event.repeat)
138
+ interaction.activate({ kind: "category", key }, "mark", event);
139
+ event.preventDefault();
140
+ }
141
+ }, children: content }));
142
+ }
143
+ function useRadialSector(props) {
111
144
  const { reveal, progress, direction } = use(RadialMotion);
112
145
  const id = `kind-ui-radial-entrance-${useId().replace(/[^a-zA-Z0-9_-]/g, "_")}`;
113
146
  const group = useRef(null);
@@ -139,9 +172,13 @@ function RadialEntranceSector(props) {
139
172
  /** Series identity is independent from native category payload/Cell identity. */
140
173
  export function RadialBarSeries({ seriesKey, hide, fill, className, material = "plain", ...props }) {
141
174
  const categories = use(RadialCategory);
142
- const { config } = useChart();
175
+ const { config, paints } = useChart();
176
+ const owner = useChartInteraction();
177
+ const filtered = categories?.bound
178
+ ? filterCategoryRows(categories.originalData, categories.key, owner.visible, props.children)
179
+ : undefined;
143
180
  const children = categories
144
- ? categoryCells(categories.data, categories.key, config, props.children, fill)
181
+ ? categoryCells(categories.data, categories.key, config, paints, filtered?.children ?? props.children, fill)
145
182
  : props.children;
146
183
  const series = usePolarSeries("RadialBarSeries", { ...props, seriesKey, hide }, [
147
184
  props.dataKey,
@@ -153,17 +190,26 @@ export function RadialBarSeries({ seriesKey, hide, fill, className, material = "
153
190
  props.maxBarSize,
154
191
  props.minPointSize,
155
192
  ]);
193
+ const interaction = useSeriesInteraction(seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined), series.hide, hide === true, undefined, props.onClick);
156
194
  const filterId = `kind-ui-polar-${useId().replace(/[^a-zA-Z0-9_-]/g, "_")}`;
157
195
  const materialized = material !== "plain" &&
158
196
  (props.shape === undefined || typeof props.shape === "boolean") &&
159
197
  (props.activeShape === undefined || typeof props.activeShape === "boolean") &&
160
198
  props.filter === undefined &&
161
199
  props.style?.filter === undefined;
162
- return (_jsxs(ZIndexLayer, { zIndex: props.zIndex ?? DefaultZIndexes.bar, children: [materialized && _jsx(PolarMaterialFilter, { material: material, id: filterId }), _jsx(motion.g, { "data-kind-ui": "radial-bar-reveal", initial: false, children: _jsx(RadialBar, { ...props, ...(props.shape === undefined ? { shape: RadialEntranceSector } : {}), ...(props.background === true
163
- ? { background: { fill: "var(--kind-ui-radial-track, #f1f1f1)" } }
164
- : {}), ...(materialized ? { filter: `url(#${filterId})` } : {}), id: series.id, hide: series.hide, isAnimationActive: false, zIndex: 0, ...(fill !== undefined
165
- ? { fill }
166
- : series.color !== undefined
167
- ? { fill: series.color }
168
- : {}), className: ["kind-ui-radial-bar-series", className].filter(Boolean).join(" "), children: children }) })] }));
200
+ const onClick = categories?.bound
201
+ ? (...args) => {
202
+ props.onClick?.(...args);
203
+ owner.activate({ kind: "category", key: categories.key(args[0].payload) }, "mark", args[2]);
204
+ }
205
+ : interaction.onClick;
206
+ return (_jsxs(ZIndexLayer, { zIndex: props.zIndex ?? DefaultZIndexes.bar, children: [materialized && _jsx(PolarMaterialFilter, { material: material, id: filterId }), _jsx(SeriesInteractionLayer, { seriesKey: seriesKey ?? (typeof props.dataKey === "string" ? props.dataKey : undefined), hidden: series.hide, children: _jsx(motion.g, { "data-kind-ui": "radial-bar-reveal", initial: false, children: _jsx(RadialBar, { ...props, ...(onClick !== undefined ? { onClick } : {}), ...(props.shape === undefined ? { shape: RadialEntranceSector } : {}), ...(categories?.bound && props.shape === undefined && props.activeShape === undefined
207
+ ? { activeShape: RadialEntranceSector }
208
+ : {}), ...(props.background === true
209
+ ? { background: { fill: "var(--kind-ui-radial-track, #f1f1f1)" } }
210
+ : {}), ...(materialized ? { filter: `url(#${filterId})` } : {}), id: series.id, hide: series.hide, isAnimationActive: false, zIndex: 0, ...(fill !== undefined
211
+ ? { fill }
212
+ : series.color !== undefined
213
+ ? { fill: series.color }
214
+ : {}), className: ["kind-ui-radial-bar-series", className].filter(Boolean).join(" "), children: children }) }) })] }));
169
215
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { cloneElement, createContext, isValidElement, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useState, } from "react";
4
4
  import { Dot } from "recharts";
5
5
  import { useChart } from "./chart-context.js";
6
- import { useEmphasisActions } from "./emphasis.js";
6
+ import { useChartInteraction } from "./chart-interaction.js";
7
7
  import { useLineInteraction } from "./line-chart.js";
8
8
  const Context = createContext(null);
9
9
  const Mode = createContext(false);
@@ -11,8 +11,11 @@ const Mode = createContext(false);
11
11
  export function RadarSelectionProvider({ selection = "none", selectedSeries, onSelectedSeriesChange, children, }) {
12
12
  if (selectedSeries !== undefined && onSelectedSeriesChange === undefined)
13
13
  throw new Error("RadarChart requires onSelectedSeriesChange for controlled selectedSeries");
14
- const { enabled: emphasisEnabled } = useEmphasisActions();
15
- const enabled = selection === "series" && emphasisEnabled;
14
+ const interaction = useChartInteraction();
15
+ if (interaction.mode === "focus" &&
16
+ (selection !== "none" || selectedSeries !== undefined || onSelectedSeriesChange !== undefined))
17
+ throw new Error("RadarChart selection controls conflict with Root focus; choose one selection owner");
18
+ const enabled = selection === "series";
16
19
  const [internal, setInternal] = useState(null);
17
20
  const [series, setSeries] = useState(() => new Map());
18
21
  const register = useCallback((owner, key, visible) => {
@@ -51,7 +54,10 @@ export function RadarSelectionProvider({ selection = "none", selectedSeries, onS
51
54
  const value = useMemo(() => ({ enabled, selected, register, toggle }), [enabled, selected, register, toggle]);
52
55
  return (_jsx(Mode, { value: enabled, children: _jsx(Context, { value: value, children: _jsx("div", { style: { display: "contents" }, onKeyDown: (event) => {
53
56
  if (event.key === "Escape" && !event.defaultPrevented && enabled && requested !== null)
54
- change(null);
57
+ queueMicrotask(() => {
58
+ if (!event.defaultPrevented)
59
+ change(null);
60
+ });
55
61
  }, children: children }) }) }));
56
62
  }
57
63
  /** An extra paint/control layer preserves native shape, data, refs and event handlers. */
@@ -1,4 +1,6 @@
1
1
  export declare const RadialCategory: import("react").Context<{
2
2
  data: readonly unknown[];
3
+ originalData: readonly unknown[];
4
+ bound: boolean;
3
5
  key: (row: unknown) => string;
4
6
  } | null>;
@@ -0,0 +1,16 @@
1
+ import { type Transition } from "motion/react";
2
+ export type RevealDirection = "left-to-right" | "right-to-left" | "center-out" | "edges-in";
3
+ type Options = {
4
+ revealDirection?: RevealDirection;
5
+ revealDurationMs?: number;
6
+ revealEasing?: Transition["ease"];
7
+ };
8
+ /** Entrance paint only: percentage rectangles never transform native series geometry. */
9
+ export declare function RevealClip({ id, family, combo, options, finish, }: {
10
+ id: string;
11
+ family?: "line" | "area";
12
+ combo?: boolean;
13
+ options: Options;
14
+ finish?: () => void;
15
+ }): import("react").JSX.Element;
16
+ export {};
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { motion } from "motion/react";
4
+ /** Entrance paint only: percentage rectangles never transform native series geometry. */
5
+ export function RevealClip({ id, family, combo = false, options, finish, }) {
6
+ const direction = options.revealDirection ?? "left-to-right";
7
+ const split = direction === "edges-in";
8
+ const initialX = direction === "right-to-left" ? "100%" : direction === "center-out" ? "50%" : "0%";
9
+ const transition = {
10
+ duration: Math.max(0, options.revealDurationMs ?? 1000) / 1000,
11
+ ease: options.revealEasing ?? [0.25, 0.1, 0.25, 1],
12
+ };
13
+ return (_jsx("defs", { children: _jsxs("clipPath", { id: id, clipPathUnits: "userSpaceOnUse", "data-reveal-direction": direction, children: [_jsx(motion.rect, { ...(combo
14
+ ? { "data-combo-reveal": family }
15
+ : family === "area"
16
+ ? { "data-area-reveal": "" }
17
+ : {}), y: 0, height: "100%", initial: { attrX: initialX, width: "0%" }, animate: { attrX: "0%", width: split ? "50%" : "100%" }, ...(finish ? { onAnimationComplete: finish } : {}), transition: transition }), split && (_jsx(motion.rect, { y: 0, height: "100%", initial: { attrX: "100%", width: "0%" }, animate: { attrX: "50%", width: "50%" }, transition: transition }))] }) }));
18
+ }
package/dist/root.d.ts CHANGED
@@ -1,14 +1,10 @@
1
- import type { ComponentPropsWithRef } from "react";
1
+ import { type ComponentPropsWithRef } from "react";
2
+ import { type ChartInteractionConfig, type ChartVisibilityProps } from "./chart-interaction.js";
2
3
  import type { SeriesConfig } from "./types.js";
3
4
  export type RootProps = ComponentPropsWithRef<"div"> & {
4
5
  config: SeriesConfig;
5
6
  emphasis?: "auto" | "none";
6
- } & ({
7
- visibleSeries?: undefined;
8
- onVisibleSeriesChange?: never;
9
- } | {
10
- visibleSeries: readonly string[];
11
- onVisibleSeriesChange?: (next: string[]) => void;
12
- });
7
+ interaction?: ChartInteractionConfig;
8
+ } & ChartVisibilityProps;
13
9
  /** Scopes presentation metadata and CSS colors; the consumer owns chart geometry and state. */
14
- export declare function Root({ config, emphasis, visibleSeries, onVisibleSeriesChange, style, children, ...props }: RootProps): import("react").JSX.Element;
10
+ export declare function Root({ config, emphasis, interaction, defaultVisibleSeries, visibleSeries, onVisibleSeriesChange, style, children, ...props }: RootProps): import("react").JSX.Element;
package/dist/root.js CHANGED
@@ -1,29 +1,64 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
3
4
  import { ChartContext } from "./chart-context.js";
5
+ import { ChartInteractionProvider, ChartInteractionStatus, useChartInteraction, } from "./chart-interaction.js";
4
6
  import { EmphasisProvider, useEmphasisActions } from "./emphasis.js";
7
+ import { colorResourceId, colorStopToken, resolveSeriesColor, } from "./series-color.js";
8
+ import { resolveSeriesConfig } from "./series-label.js";
9
+ import { SeriesColorDefinitions } from "./series-paint.js";
10
+ import { StylesheetWarning } from "./stylesheet-warning.js";
5
11
  /** Scopes presentation metadata and CSS colors; the consumer owns chart geometry and state. */
6
- export function Root({ config, emphasis = "auto", visibleSeries, onVisibleSeriesChange, style, children, ...props }) {
7
- if (onVisibleSeriesChange && !visibleSeries)
8
- throw new Error("Root requires visibleSeries when onVisibleSeriesChange is provided");
12
+ export function Root({ config, emphasis = "auto", interaction, defaultVisibleSeries, visibleSeries, onVisibleSeriesChange, style, children, ...props }) {
13
+ if (onVisibleSeriesChange && visibleSeries === undefined && defaultVisibleSeries === undefined)
14
+ throw new Error("Root requires visibleSeries or defaultVisibleSeries when onVisibleSeriesChange is provided");
15
+ if (visibleSeries !== undefined && defaultVisibleSeries !== undefined)
16
+ throw new Error("Root visibility cannot be both controlled and defaulted");
17
+ const id = useId();
9
18
  const colors = {};
19
+ const paints = {};
20
+ const colorStops = {};
10
21
  for (const [key, item] of Object.entries(config)) {
11
22
  if (!/^[a-zA-Z][\w-]*$/.test(key))
12
23
  throw new Error(`Chart series key "${key}" must start with a letter and contain only letters, numbers, underscores or hyphens`);
13
- colors[`--color-${key}`] = item.color;
24
+ const resolved = resolveSeriesColor(item.color, key);
25
+ colors[`--color-${key}`] = resolved.colors[0];
26
+ for (const [index, color] of resolved.colors.entries())
27
+ colors[colorStopToken(key, index)] = color;
28
+ colorStops[key] = resolved;
29
+ const gradientId = colorResourceId(id, key);
30
+ const gradient = resolved.colors.length > 1;
31
+ paints[key] = gradient ? `url(#${gradientId})` : `var(--color-${key})`;
32
+ colors[colorStopToken(key, "gradient")] = gradient
33
+ ? `linear-gradient(to right in srgb, ${resolved.offsets.map((offset, index) => `var(${colorStopToken(key, index)}) ${offset * 100}%`).join(", ")})`
34
+ : `var(--color-${key})`;
14
35
  }
15
36
  const value = {
16
- config,
37
+ config: resolveSeriesConfig(config),
38
+ paints,
39
+ colorId: id,
40
+ colorStops,
17
41
  ...(visibleSeries ? { visibleSeries } : {}),
18
42
  ...(onVisibleSeriesChange ? { onVisibleSeriesChange } : {}),
19
43
  };
20
- return (_jsx(ChartContext, { value: value, children: _jsx(EmphasisProvider, { enabled: emphasis === "auto", children: _jsx(RootFrame, { ...props, style: { ...colors, ...style }, children: children }) }) }));
44
+ return (_jsx(ChartInteractionProvider, { config: value.config, ...(interaction ? { interaction } : {}), ...(visibleSeries !== undefined
45
+ ? { visibleSeries, ...(onVisibleSeriesChange ? { onVisibleSeriesChange } : {}) }
46
+ : defaultVisibleSeries !== undefined
47
+ ? { defaultVisibleSeries, ...(onVisibleSeriesChange ? { onVisibleSeriesChange } : {}) }
48
+ : {}), children: (visible, change) => (_jsx(ChartContext, { value: {
49
+ ...value,
50
+ ...(visible !== undefined ? { visibleSeries: visible } : {}),
51
+ ...(change ? { onVisibleSeriesChange: change } : {}),
52
+ }, children: _jsx(EmphasisProvider, { enabled: emphasis === "auto", children: _jsxs(RootFrame, { ...props, style: { ...colors, ...style }, children: [process.env.NODE_ENV === "development" && _jsx(StylesheetWarning, {}), Object.values(colorStops).some((stops) => stops.colors.length > 1) && (_jsx("svg", { "aria-hidden": "true", focusable: "false", width: 0, height: 0, style: { position: "absolute" }, "data-kind-ui": "color-resources", children: _jsx(SeriesColorDefinitions, {}) })), children] }) }) })) }));
21
53
  }
22
54
  function RootFrame({ children, ...props }) {
23
55
  const { reset } = useEmphasisActions();
24
- return (_jsx("div", { ...props, "data-kind-ui": "chart", onKeyDownCapture: (event) => {
25
- if (event.key === "Escape")
56
+ const interaction = useChartInteraction();
57
+ return (
58
+ // biome-ignore lint/a11y/noStaticElementInteractions: Delegates Escape from descendants after consumer handlers; this frame is not a control.
59
+ _jsxs("div", { ...props, "data-kind-ui": "chart", onKeyDown: (event) => {
60
+ props.onKeyDown?.(event);
61
+ if (event.key === "Escape" && interaction.reset(event))
26
62
  reset();
27
- props.onKeyDownCapture?.(event);
28
- }, children: children }));
63
+ }, children: [children, _jsx(ChartInteractionStatus, {})] }));
29
64
  }
@@ -14,6 +14,8 @@ export type SankeyChartProps = Omit<NativeProps, "data" | "node" | "link" | "onC
14
14
  data: SankeyFlowData;
15
15
  /** Explicit node-ID metadata; enables matching default Kind node/source-link paint. */
16
16
  nodeConfig?: SankeyNodeConfig | undefined;
17
+ /** Explicit node-focus binding; Root must use kind node and mode focus. */
18
+ interactionBinding?: "root";
17
19
  node?: Exclude<NativeProps["node"], (props: NativeNodeProps) => ReactNode> | ((props: SankeyNodeProps) => ReactNode);
18
20
  link?: Exclude<NativeProps["link"], (props: NativeLinkProps) => ReactNode> | ((props: SankeyLinkProps) => ReturnType<NonNullable<Extract<NativeProps["link"], (props: NativeLinkProps) => ReactNode>>>);
19
21
  onClick?: IdentityEvent;
@@ -21,6 +23,8 @@ export type SankeyChartProps = Omit<NativeProps, "data" | "node" | "link" | "onC
21
23
  onMouseLeave?: IdentityEvent;
22
24
  /** Kind SankeyLink paint flows along unchanged paths; custom renderers retain ownership. */
23
25
  animate?: boolean | SankeyAnimation;
26
+ loading?: boolean | undefined;
27
+ loadingLabel?: string | undefined;
24
28
  empty?: ReactNode;
25
29
  };
26
30
  export declare const SankeyMotion: import("react").Context<{
@@ -29,5 +33,5 @@ export declare const SankeyMotion: import("react").Context<{
29
33
  width: number;
30
34
  }>;
31
35
  /** Recharts owns layout, renderers, labels and events. Zero flows remain in the data alternative. */
32
- export declare function SankeyChart({ data, nodeConfig, animate, empty, ...props }: SankeyChartProps): import("react").JSX.Element;
36
+ export declare function SankeyChart({ data, nodeConfig, interactionBinding, animate, loading, loadingLabel, empty, ...props }: SankeyChartProps): import("react").JSX.Element;
33
37
  export {};
@@ -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 (inputs.some((value, index) => (index !== 0 || started.current) && value !== previous.current[index]))
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: () => setInterrupted(true), onFocusCapture: () => setInterrupted(true), style: { position: "relative", width: "fit-content", height: "fit-content" }, children: [_jsxs(EngineSankey, { ...props, ...(nodeConfig
137
- ? {
138
- node: props.node ?? configuredNode,
139
- link: props.link ?? configuredLink,
140
- }
141
- : {}), data: drawable && links.length ? { nodes, links } : { nodes: [], links: [] }, children: [_jsx(NativeSize, { onSize: onSize }), props.children] }), !links.length ? (_jsx("div", { role: "status", style: { position: "absolute", inset: 0 }, children: empty })) : !drawable ? (_jsx("div", { role: "status", style: { position: "absolute", inset: 0 }, children: "Insufficient space for flows; use the data table" })) : null] }) }) }));
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
  }
@@ -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;
@@ -1,4 +1,4 @@
1
- export type SankeyFinish = "plain" | "paper" | "clay" | "glow";
1
+ export type SankeyFinish = "plain" | "clay" | "glow";
2
2
  /** Sankey-local surface treatment. Never displace the quantitative silhouette. */
3
3
  export declare function SankeyFinishFilter({ id, finish, thickness, bounds, }: {
4
4
  id: string;
@@ -6,5 +6,5 @@ export function SankeyFinishFilter({ id, finish, thickness, bounds, }) {
6
6
  const relief = Math.min(4, thickness / 4);
7
7
  // A 0.6px blur disappeared in Chromium probes; retain a measured thin-flow halo.
8
8
  const halo = Math.min(1, Math.max(0.85, thickness / 4));
9
- return (_jsxs("filter", { id: id, filterUnits: "userSpaceOnUse", primitiveUnits: "userSpaceOnUse", ...bounds, colorInterpolationFilters: "sRGB", children: [_jsx("feComponentTransfer", { in: "SourceAlpha", result: "footprint", children: _jsx("feFuncA", { type: "linear", slope: 100000 }) }), finish === "clay" ? (_jsxs(_Fragment, { children: [_jsx("feOffset", { in: "footprint", dx: relief, dy: relief, result: "lower" }), _jsx("feComposite", { in: "footprint", in2: "lower", operator: "arithmetic", k2: 1, k3: -1, result: "top" }), _jsx("feGaussianBlur", { in: "top", stdDeviation: relief * 0.8, result: "softTop" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.48 }), _jsx("feComposite", { in2: "softTop", operator: "in", result: "light" }), _jsx("feOffset", { in: "footprint", dx: -relief, dy: -relief, result: "upper" }), _jsx("feComposite", { in: "footprint", in2: "upper", operator: "arithmetic", k2: 1, k3: -1, result: "bottom" }), _jsx("feGaussianBlur", { in: "bottom", stdDeviation: relief * 0.8, result: "softBottom" }), _jsx("feFlood", { floodColor: "#17212b", floodOpacity: 0.26 }), _jsx("feComposite", { in2: "softBottom", operator: "in", result: "shade" }), _jsx("feComposite", { in: "shade", in2: "SourceGraphic", operator: "atop", result: "shaded" }), _jsx("feComposite", { in: "light", in2: "shaded", operator: "atop", result: "body" })] })) : (_jsxs(_Fragment, { children: [_jsx("feMorphology", { in: "footprint", operator: "erode", radius: edge, result: "inside" }), _jsx("feComposite", { in: "footprint", in2: "inside", operator: "out", result: "edge" }), finish === "paper" ? (_jsxs(_Fragment, { children: [_jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "0.08 0.2", numOctaves: 1, seed: 11, result: "pencil" }), _jsx("feColorMatrix", { in: "pencil", type: "luminanceToAlpha", result: "pencilMask" }), _jsx("feComponentTransfer", { in: "pencilMask", result: "sketch", children: _jsx("feFuncA", { type: "linear", slope: 1.6, intercept: -0.15 }) }), _jsx("feComposite", { in: "edge", in2: "sketch", operator: "in", result: "sketchEdge" }), _jsx("feFlood", { floodColor: "#17212b", floodOpacity: 0.5 }), _jsx("feComposite", { in2: "sketchEdge", operator: "in", result: "contour" }), _jsx("feComposite", { in: "contour", in2: "SourceGraphic", operator: "atop", result: "body" })] })) : (_jsxs(_Fragment, { children: [_jsx("feGaussianBlur", { in: "edge", stdDeviation: edge, result: "softEdge" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.8 }), _jsx("feComposite", { in2: "softEdge", operator: "in", result: "light" }), _jsx("feComposite", { in: "light", in2: "SourceGraphic", operator: "atop", result: "body" })] }))] })), finish !== "glow" ? (_jsxs(_Fragment, { children: [_jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "0.65 0.3", numOctaves: 1, seed: 7, result: "fiber" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: finish === "paper" ? 0.14 : 0.025 }), _jsx("feComposite", { in2: "fiber", operator: "in", result: "grain" }), _jsx("feComposite", { in: "grain", in2: "body", operator: "atop" })] })) : (_jsxs(_Fragment, { children: [_jsx("feGaussianBlur", { in: "SourceAlpha", stdDeviation: halo, result: "blur" }), _jsx("feComposite", { in: "blur", in2: "footprint", operator: "out", result: "exterior" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.12 }), _jsx("feComposite", { in2: "exterior", operator: "in", result: "halo" }), _jsxs("feMerge", { children: [_jsx("feMergeNode", { in: "halo" }), _jsx("feMergeNode", { in: "body" })] })] }))] }));
9
+ return (_jsxs("filter", { id: id, filterUnits: "userSpaceOnUse", primitiveUnits: "userSpaceOnUse", ...bounds, colorInterpolationFilters: "sRGB", children: [_jsx("feComponentTransfer", { in: "SourceAlpha", result: "footprint", children: _jsx("feFuncA", { type: "linear", slope: 100000 }) }), finish === "clay" ? (_jsxs(_Fragment, { children: [_jsx("feOffset", { in: "footprint", dx: relief, dy: relief, result: "lower" }), _jsx("feComposite", { in: "footprint", in2: "lower", operator: "arithmetic", k2: 1, k3: -1, result: "top" }), _jsx("feGaussianBlur", { in: "top", stdDeviation: relief * 0.8, result: "softTop" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.48 }), _jsx("feComposite", { in2: "softTop", operator: "in", result: "light" }), _jsx("feOffset", { in: "footprint", dx: -relief, dy: -relief, result: "upper" }), _jsx("feComposite", { in: "footprint", in2: "upper", operator: "arithmetic", k2: 1, k3: -1, result: "bottom" }), _jsx("feGaussianBlur", { in: "bottom", stdDeviation: relief * 0.8, result: "softBottom" }), _jsx("feFlood", { floodColor: "#17212b", floodOpacity: 0.26 }), _jsx("feComposite", { in2: "softBottom", operator: "in", result: "shade" }), _jsx("feComposite", { in: "shade", in2: "SourceGraphic", operator: "atop", result: "shaded" }), _jsx("feComposite", { in: "light", in2: "shaded", operator: "atop", result: "body" })] })) : (_jsxs(_Fragment, { children: [_jsx("feMorphology", { in: "footprint", operator: "erode", radius: edge, result: "inside" }), _jsx("feComposite", { in: "footprint", in2: "inside", operator: "out", result: "edge" }), _jsx("feGaussianBlur", { in: "edge", stdDeviation: edge, result: "softEdge" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.8 }), _jsx("feComposite", { in2: "softEdge", operator: "in", result: "light" }), _jsx("feComposite", { in: "light", in2: "SourceGraphic", operator: "atop", result: "body" })] })), finish !== "glow" ? (_jsxs(_Fragment, { children: [_jsx("feTurbulence", { type: "fractalNoise", baseFrequency: "0.65 0.3", numOctaves: 1, seed: 7, result: "fiber" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.025 }), _jsx("feComposite", { in2: "fiber", operator: "in", result: "grain" }), _jsx("feComposite", { in: "grain", in2: "body", operator: "atop" })] })) : (_jsxs(_Fragment, { children: [_jsx("feGaussianBlur", { in: "SourceAlpha", stdDeviation: halo, result: "blur" }), _jsx("feComposite", { in: "blur", in2: "footprint", operator: "out", result: "exterior" }), _jsx("feFlood", { floodColor: "#fff", floodOpacity: 0.12 }), _jsx("feComposite", { in2: "exterior", operator: "in", result: "halo" }), _jsxs("feMerge", { children: [_jsx("feMergeNode", { in: "halo" }), _jsx("feMergeNode", { in: "body" })] })] }))] }));
10
10
  }
@@ -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
+ }
@@ -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;