@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +657 -865
  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-series.d.ts +11 -1
  14. package/dist/bar-series.js +65 -13
  15. package/dist/box-plot.js +2 -2
  16. package/dist/category-cells.d.ts +7 -1
  17. package/dist/category-cells.js +32 -2
  18. package/dist/chart-background-pattern.d.ts +23 -0
  19. package/dist/chart-background-pattern.js +31 -0
  20. package/dist/chart-context.d.ts +6 -2
  21. package/dist/chart-interaction.d.ts +94 -0
  22. package/dist/chart-interaction.js +224 -0
  23. package/dist/combo-chart.d.ts +8 -5
  24. package/dist/combo-chart.js +14 -12
  25. package/dist/configured-line-chart.d.ts +5 -1
  26. package/dist/configured-line-chart.js +8 -5
  27. package/dist/emphasis.d.ts +5 -2
  28. package/dist/emphasis.js +18 -4
  29. package/dist/fill-pattern.d.ts +25 -0
  30. package/dist/fill-pattern.js +23 -0
  31. package/dist/heatmap.d.ts +3 -1
  32. package/dist/heatmap.js +34 -12
  33. package/dist/histogram-chart.js +10 -3
  34. package/dist/index.d.ts +12 -3
  35. package/dist/index.js +8 -1
  36. package/dist/legend.d.ts +2 -0
  37. package/dist/legend.js +28 -7
  38. package/dist/line-chart.d.ts +13 -3
  39. package/dist/line-chart.js +54 -34
  40. package/dist/line-dash.d.ts +8 -0
  41. package/dist/line-dash.js +18 -0
  42. package/dist/line-series.d.ts +9 -1
  43. package/dist/line-series.js +38 -11
  44. package/dist/loading-cartesian-designs.d.ts +6 -0
  45. package/dist/loading-cartesian-designs.js +123 -0
  46. package/dist/loading-motion.d.ts +37 -0
  47. package/dist/loading-motion.js +88 -0
  48. package/dist/loading-polar-designs.d.ts +6 -0
  49. package/dist/loading-polar-designs.js +95 -0
  50. package/dist/loading-skeleton.d.ts +29 -0
  51. package/dist/loading-skeleton.js +143 -0
  52. package/dist/loading-standalone-designs.d.ts +5 -0
  53. package/dist/loading-standalone-designs.js +126 -0
  54. package/dist/percent-stack.d.ts +18 -0
  55. package/dist/percent-stack.js +33 -0
  56. package/dist/pie-chart.d.ts +7 -3
  57. package/dist/pie-chart.js +37 -6
  58. package/dist/pie-pin-identity.d.ts +4 -0
  59. package/dist/pie-pin-identity.js +10 -0
  60. package/dist/pie-series.d.ts +4 -0
  61. package/dist/pie-series.js +137 -14
  62. package/dist/pie-tooltip-pin.d.ts +4 -0
  63. package/dist/pie-tooltip-pin.js +52 -0
  64. package/dist/point-marker.d.ts +19 -0
  65. package/dist/point-marker.js +19 -0
  66. package/dist/polar-chart.d.ts +14 -5
  67. package/dist/polar-chart.js +41 -10
  68. package/dist/polar-series.js +68 -22
  69. package/dist/radar-interaction.js +10 -4
  70. package/dist/radial-category.d.ts +2 -0
  71. package/dist/reveal-clip.d.ts +16 -0
  72. package/dist/reveal-clip.js +18 -0
  73. package/dist/root.d.ts +5 -9
  74. package/dist/root.js +46 -11
  75. package/dist/sankey-chart.d.ts +5 -1
  76. package/dist/sankey-chart.js +64 -13
  77. package/dist/sankey-colors.d.ts +3 -0
  78. package/dist/sankey-interaction.d.ts +8 -0
  79. package/dist/sankey-interaction.js +32 -0
  80. package/dist/sankey-legend.d.ts +2 -1
  81. package/dist/sankey-legend.js +18 -3
  82. package/dist/sankey-node-label.d.ts +22 -0
  83. package/dist/sankey-node-label.js +34 -0
  84. package/dist/scatter-chart.d.ts +5 -3
  85. package/dist/scatter-chart.js +14 -4
  86. package/dist/scatter-series.js +5 -3
  87. package/dist/series-color.d.ts +10 -0
  88. package/dist/series-color.js +48 -0
  89. package/dist/series-interaction.d.ts +12 -0
  90. package/dist/series-interaction.js +82 -0
  91. package/dist/series-label.d.ts +7 -0
  92. package/dist/series-label.js +18 -0
  93. package/dist/series-paint.d.ts +8 -0
  94. package/dist/series-paint.js +27 -0
  95. package/dist/styles.css +151 -1
  96. package/dist/stylesheet-warning.d.ts +2 -0
  97. package/dist/stylesheet-warning.js +86 -0
  98. package/dist/tooltip-content.d.ts +8 -1
  99. package/dist/tooltip-content.js +15 -4
  100. package/dist/tooltip.d.ts +3 -1
  101. package/dist/tooltip.js +8 -5
  102. package/dist/types.d.ts +11 -2
  103. package/dist/waterfall-chart.d.ts +4 -0
  104. package/dist/waterfall-chart.js +7 -0
  105. package/package.json +2 -2
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { LoadingPulseFlow, LoadingPulseMark } from "./loading-motion.js";
3
+ // Authored clustered, diagonal, paired-hotspot and striped illustrations.
4
+ const heatmapPatterns = [
5
+ [
6
+ 9, 9, 7, 2, 1, 1, 1, 2, 9, 8, 6, 2, 1, 1, 2, 1, 7, 6, 4, 2, 1, 2, 1, 1, 2, 2, 1, 1, 2, 1, 1, 1,
7
+ 1, 1, 1, 2, 1, 1, 2, 1,
8
+ ],
9
+ [
10
+ 9, 7, 2, 1, 1, 1, 1, 1, 2, 8, 9, 6, 1, 1, 1, 1, 1, 1, 3, 9, 8, 3, 1, 1, 1, 1, 1, 1, 5, 9, 7, 2,
11
+ 1, 1, 1, 1, 1, 2, 7, 9,
12
+ ],
13
+ [
14
+ 1, 2, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 2, 5, 3, 1, 1, 4, 7, 5, 6, 9, 8, 2, 2, 8, 9, 8,
15
+ 4, 8, 6, 1, 1, 6, 8, 5,
16
+ ],
17
+ [
18
+ 1, 8, 9, 1, 2, 1, 8, 9, 2, 7, 9, 1, 1, 1, 9, 7, 1, 9, 8, 2, 1, 2, 7, 9, 1, 8, 9, 1, 1, 1, 9, 8,
19
+ 2, 9, 7, 1, 2, 1, 8, 9,
20
+ ],
21
+ ];
22
+ // Each flow width is conserved at its node junctions. These are decorative authored layouts.
23
+ const sankeyProfiles = [
24
+ {
25
+ nodes: [
26
+ [16, 20, 16, 60],
27
+ [16, 100, 16, 50],
28
+ [16, 170, 16, 50],
29
+ [298, 35, 24, 90],
30
+ [298, 145, 24, 70],
31
+ [608, 20, 16, 50],
32
+ [608, 95, 16, 40],
33
+ [608, 155, 16, 70],
34
+ ],
35
+ flows: [
36
+ [32, 20, 298, 35, 60],
37
+ [32, 100, 298, 95, 30],
38
+ [32, 130, 298, 145, 20],
39
+ [32, 170, 298, 165, 50],
40
+ [322, 35, 608, 20, 50],
41
+ [322, 85, 608, 95, 40],
42
+ [322, 145, 608, 155, 70],
43
+ ],
44
+ },
45
+ {
46
+ nodes: [
47
+ [16, 20, 16, 100],
48
+ [16, 140, 16, 80],
49
+ [298, 20, 24, 50],
50
+ [298, 90, 24, 60],
51
+ [298, 170, 24, 70],
52
+ [608, 25, 16, 100],
53
+ [608, 145, 16, 80],
54
+ ],
55
+ flows: [
56
+ [32, 20, 298, 20, 50],
57
+ [32, 70, 298, 90, 50],
58
+ [32, 140, 298, 140, 10],
59
+ [32, 150, 298, 170, 70],
60
+ [322, 20, 608, 25, 50],
61
+ [322, 90, 608, 75, 50],
62
+ [322, 140, 608, 145, 10],
63
+ [322, 170, 608, 155, 70],
64
+ ],
65
+ },
66
+ {
67
+ nodes: [
68
+ [16, 32, 16, 176],
69
+ [298, 12, 24, 64],
70
+ [298, 96, 24, 46],
71
+ [298, 162, 24, 66],
72
+ [608, 20, 16, 110],
73
+ [608, 154, 16, 66],
74
+ ],
75
+ flows: [
76
+ [32, 32, 298, 12, 64],
77
+ [32, 96, 298, 96, 46],
78
+ [32, 142, 298, 162, 66],
79
+ [322, 12, 608, 20, 64],
80
+ [322, 96, 608, 84, 46],
81
+ [322, 162, 608, 154, 66],
82
+ ],
83
+ },
84
+ {
85
+ nodes: [
86
+ [16, 12, 16, 30],
87
+ [16, 58, 16, 40],
88
+ [16, 114, 16, 50],
89
+ [16, 174, 16, 60],
90
+ [608, 20, 16, 60],
91
+ [608, 102, 16, 55],
92
+ [608, 174, 16, 65],
93
+ ],
94
+ flows: [
95
+ [32, 12, 608, 20, 30],
96
+ [32, 58, 608, 50, 30],
97
+ [32, 88, 608, 102, 10],
98
+ [32, 114, 608, 112, 45],
99
+ [32, 159, 608, 174, 5],
100
+ [32, 174, 608, 179, 60],
101
+ ],
102
+ },
103
+ ];
104
+ /** Fixed illustrations; no user cells, flows, values, or native marks are sampled. */
105
+ export function StandaloneLoadingDesign({ family, seed, }) {
106
+ const profile = seed % 4;
107
+ if (family === "heatmap") {
108
+ return (_jsx("g", { fill: "currentColor", "data-profile": profile, children: heatmapPatterns[profile]?.map((intensity, index) => {
109
+ const row = Math.floor(index / 8);
110
+ const column = index % 8;
111
+ return (_jsx(LoadingPulseMark, { delay: ((row + column) / 11) * (0.55 / 0.8), span: 0.25 / 0.8, children: _jsx("rect", { "data-motion": "heatmap-cell", x: 16 + column * 77, y: 10 + row * 45, width: "69", height: "37", rx: "3", opacity: 0.2 + (intensity / 9) * 0.8 }) }, `${row}-${column}`));
112
+ }) }));
113
+ }
114
+ const design = sankeyProfiles[profile];
115
+ if (!design)
116
+ return null;
117
+ const band = ([x0, y0, x1, y1, width]) => {
118
+ const middle = (x0 + x1) / 2;
119
+ return `M ${x0} ${y0} C ${middle} ${y0} ${middle} ${y1} ${x1} ${y1} L ${x1} ${y1 + width} C ${middle} ${y1 + width} ${middle} ${y0 + width} ${x0} ${y0 + width} Z`;
120
+ };
121
+ const flow = ([x0, y0, x1, y1, width]) => {
122
+ const middle = (x0 + x1) / 2;
123
+ return `M ${x0} ${y0 + width / 2} C ${middle} ${y0 + width / 2} ${middle} ${y1 + width / 2} ${x1} ${y1 + width / 2}`;
124
+ };
125
+ return (_jsxs("g", { fill: "currentColor", "data-profile": profile, children: [design.flows.map((item) => (_jsxs(LoadingPulseFlow, { d: flow(item), width: item[4], delay: (item[0] / 640) * 0.55, children: [_jsx("path", { d: band(item), opacity: "0.5" }), _jsx("path", { "data-motion": "sankey-flow", d: flow(item), fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", opacity: "0.55" })] }, item.join("-")))), design.nodes.map(([x, y, width, height]) => (_jsx(LoadingPulseMark, { delay: (x / 640) * 0.55, span: 0.45, children: _jsx("rect", { x: x, y: y, width: width, height: height, rx: "2" }) }, `${x}-${y}`)))] }));
126
+ }
@@ -0,0 +1,18 @@
1
+ import type { TooltipContentProps } from "recharts";
2
+ type Entry = TooltipContentProps["payload"][number];
3
+ /** Return a fraction for selected entries; undefined retains ordinary formatting. */
4
+ export type NormalizedValue = (entry: Entry) => number | undefined;
5
+ export type PercentStackOptions = {
6
+ /** Raw members of this entry's native expand stack, including its own value.
7
+ * Return undefined for other series/axes. Match native hidden-series membership.
8
+ */
9
+ values: (entry: Entry) => readonly (number | null | undefined)[] | undefined;
10
+ };
11
+ /** Formats an existing fraction; never normalizes chart data or changes domains. */
12
+ export declare function formatPercent(value: number): string;
13
+ /** Formatting only for native stackOffset="expand" scalar stacks. */
14
+ export declare function createPercentStack({ values }: PercentStackOptions): {
15
+ tickFormatter: typeof formatPercent;
16
+ normalizedValue: NormalizedValue;
17
+ };
18
+ export {};
@@ -0,0 +1,33 @@
1
+ /** Formats an existing fraction; never normalizes chart data or changes domains. */
2
+ export function formatPercent(value) {
3
+ return `${Number((value * 100).toFixed(1))}%`;
4
+ }
5
+ /** Formatting only for native stackOffset="expand" scalar stacks. */
6
+ export function createPercentStack({ values }) {
7
+ const normalizedValue = (entry) => {
8
+ if (typeof entry.value !== "number" || !Number.isFinite(entry.value))
9
+ return undefined;
10
+ const members = values(entry);
11
+ if (!members || members.length === 0)
12
+ return undefined;
13
+ let total = 0;
14
+ let allZero = true;
15
+ for (const member of members) {
16
+ if (member == null)
17
+ continue;
18
+ if (!Number.isFinite(member))
19
+ return undefined;
20
+ total += member;
21
+ if (member !== 0)
22
+ allZero = false;
23
+ }
24
+ if (!Number.isFinite(total))
25
+ return undefined;
26
+ // Expand uses a signed sum. A cancelling sum has no normalized share.
27
+ if (total === 0)
28
+ return allZero && entry.value === 0 ? 0 : undefined;
29
+ const fraction = entry.value / total;
30
+ return Number.isFinite(fraction * 100) ? fraction : undefined;
31
+ };
32
+ return { tickFormatter: formatPercent, normalizedValue };
33
+ }
@@ -1,9 +1,13 @@
1
1
  import { type ComponentProps } from "react";
2
2
  import { PieChart as EnginePieChart } from "recharts";
3
- import { type LineAnimation } from "./animation.js";
4
- export type PieAnimation = LineAnimation;
3
+ import { type BaseAnimation } from "./animation.js";
4
+ export type PieAnimation = BaseAnimation;
5
5
  export type PieChartProps = ComponentProps<typeof EnginePieChart> & {
6
+ /** Initial tooltip category; one direct categoryKey PieSeries with explicit data only. */
7
+ defaultPinnedCategory?: string | undefined;
6
8
  animate?: boolean | PieAnimation | undefined;
9
+ loading?: boolean | undefined;
10
+ loadingLabel?: string | undefined;
7
11
  /** Entrance sweep only; native start/end angles and data order are unchanged. */
8
12
  animationDirection?: "clockwise" | "anticlockwise" | undefined;
9
13
  };
@@ -14,4 +18,4 @@ export declare const PieMotion: import("react").Context<{
14
18
  direction: "clockwise" | "anticlockwise";
15
19
  }>;
16
20
  /** Native polar composition, shared interaction, and optional Motion-owned sector entrance. */
17
- export declare function PieChart({ animate, animationDirection, children, ...props }: PieChartProps): import("react").JSX.Element;
21
+ export declare function PieChart({ animate, loading, loadingLabel, animationDirection, children, defaultPinnedCategory, ...props }: PieChartProps): import("react").JSX.Element;
package/dist/pie-chart.js CHANGED
@@ -1,10 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { animate as animateValue } from "motion/react";
4
- import { createContext, useCallback, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
4
+ import { createContext, memo, useCallback, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
5
5
  import { PieChart as EnginePieChart } from "recharts";
6
6
  import { MotionContext } from "./animation.js";
7
+ import { useChart } from "./chart-context.js";
7
8
  import { LineChartFrame } from "./line-chart.js";
9
+ import { PolarLoadingDesign } from "./loading-polar-designs.js";
10
+ import { PieTooltipPin, pinnedPieIndex } from "./pie-tooltip-pin.js";
8
11
  export const PieMotion = createContext({
9
12
  reveal: false,
10
13
  options: {},
@@ -21,11 +24,33 @@ const snapshot = () => window.matchMedia(query).matches;
21
24
  const serverSnapshot = () => true;
22
25
  const defaultHover = { type: "spring", stiffness: 210, damping: 28, mass: 0.8 };
23
26
  /** Native polar composition, shared interaction, and optional Motion-owned sector entrance. */
24
- export function PieChart({ animate = false, animationDirection = "clockwise", children, ...props }) {
27
+ export function PieChart({ animate = false, loading, loadingLabel, animationDirection = "clockwise", children, defaultPinnedCategory, ...props }) {
28
+ const { visibleSeries } = useChart();
29
+ const [initialCategory] = useState(defaultPinnedCategory);
30
+ const [pinCleared, setPinCleared] = useState(false);
31
+ const clearPin = useCallback(() => setPinCleared(true), []);
32
+ const pinIndex = initialCategory === undefined || pinCleared
33
+ ? undefined
34
+ : visibleSeries !== undefined && !visibleSeries.includes(initialCategory)
35
+ ? undefined
36
+ : pinnedPieIndex(children, initialCategory, visibleSeries);
37
+ useLayoutEffect(() => {
38
+ if (initialCategory !== undefined && pinIndex === undefined)
39
+ clearPin();
40
+ }, [initialCategory, pinIndex, clearPin]);
41
+ const chart = (_jsx(PieChartPlot, { ...props, animate: animate, loading: loading, loadingLabel: loadingLabel, animationDirection: animationDirection, children: children }));
42
+ return (_jsx(PieTooltipPin, { value: pinIndex, children: initialCategory === undefined ? (chart) : (_jsx("div", { style: { display: "contents" }, onPointerMoveCapture: clearPin, onPointerDownCapture: clearPin, onFocusCapture: clearPin, onKeyDownCapture: clearPin, children: chart })) }));
43
+ }
44
+ // Pin dismissal changes Tooltip context only. Keep the native plot's providers and
45
+ // geometry inputs intact while a pressed sector is awaiting its native click.
46
+ const PieChartPlot = memo(function PieChartPlot({ animate = false, loading, loadingLabel, animationDirection = "clockwise", children, ...props }) {
25
47
  const reduced = useSyncExternalStore(subscribe, snapshot, serverSnapshot);
26
48
  const [interacted, setInteracted] = useState(false);
27
- const finish = useCallback(() => setInteracted(true), []);
28
- const enabled = animate !== false && !reduced;
49
+ const finish = useCallback(() => {
50
+ if (!loading)
51
+ setInteracted(true);
52
+ }, [loading]);
53
+ const enabled = animate !== false && !reduced && !loading;
29
54
  const interrupt = useCallback(() => {
30
55
  if (enabled)
31
56
  finish();
@@ -42,6 +67,12 @@ export function PieChart({ animate = false, animationDirection = "clockwise", ch
42
67
  const duration = options.revealDurationMs ?? 1000;
43
68
  const easing = options.revealEasing ?? "easeOut";
44
69
  const started = useRef(false);
70
+ useLayoutEffect(() => {
71
+ if (loading) {
72
+ setInteracted(false);
73
+ started.current = false;
74
+ }
75
+ }, [loading]);
45
76
  const previous = useRef([duration, easing, animationDirection]);
46
77
  useLayoutEffect(() => {
47
78
  const inputs = [duration, easing, animationDirection];
@@ -64,5 +95,5 @@ export function PieChart({ animate = false, animationDirection = "clockwise", ch
64
95
  });
65
96
  return () => controls.stop();
66
97
  }, [reveal, duration, easing, finish]);
67
- return (_jsx(MotionContext, { value: { enabled, transition: options.hoverTransition ?? defaultHover }, children: _jsx(PieMotion, { value: { reveal, options, progress, direction: animationDirection }, children: _jsx(LineChartFrame, { chartProps: props, engine: EnginePieChart, motionEnabled: enabled, interrupt: interrupt, children: children }) }) }));
68
- }
98
+ return (_jsx(MotionContext, { value: { enabled, transition: options.hoverTransition ?? defaultHover }, children: _jsx(PieMotion, { value: { reveal, options, progress, direction: animationDirection }, children: _jsx(LineChartFrame, { chartProps: props, engine: EnginePieChart, loading: loading, loadingLabel: loadingLabel, loadingSkeleton: "pie", loadingDesign: (seed) => _jsx(PolarLoadingDesign, { family: "pie", seed: seed }), loadingAnimation: { ...options, direction: animationDirection }, motionEnabled: enabled, interrupt: interrupt, children: children }) }) }));
99
+ });
@@ -0,0 +1,4 @@
1
+ type PiePinComponentKind = "series" | "tooltip";
2
+ export declare function registerPiePinComponent(component: object, kind: PiePinComponentKind): void;
3
+ export declare function piePinComponentKind(component: unknown): PiePinComponentKind | undefined;
4
+ export {};
@@ -0,0 +1,10 @@
1
+ // Canonical exports register themselves; resolving children never imports their renderers.
2
+ const components = new WeakMap();
3
+ export function registerPiePinComponent(component, kind) {
4
+ components.set(component, kind);
5
+ }
6
+ export function piePinComponentKind(component) {
7
+ return typeof component === "function" || (typeof component === "object" && component !== null)
8
+ ? components.get(component)
9
+ : undefined;
10
+ }
@@ -5,8 +5,12 @@ import { type PieMaterial } from "./pie-material.js";
5
5
  export type PieSeriesProps<DataPoint = unknown, Value = unknown> = Omit<ComponentProps<typeof Pie<DataPoint, Value>>, "isAnimationActive"> & {
6
6
  /** Opt-in category colors from Root.config; requires explicit series data. */
7
7
  categoryKey?: CategoryKey<DataPoint> | undefined;
8
+ /** Explicitly share this Pie's category identities and actions with Root/Legend. */
9
+ interactionBinding?: "root";
8
10
  /** Finish on default native sectors; custom shapes, filters and CSS transforms keep ownership. */
9
11
  material?: PieMaterial | undefined;
12
+ /** Categories receiving glow instead of material; requires categoryKey and explicit data. Unknown IDs are ignored. */
13
+ glowCategories?: readonly string[] | undefined;
10
14
  /** Stable sector identity; defaults to the native nameKey value. */
11
15
  emphasisKey?: ((payload: unknown) => string | number | undefined) | undefined;
12
16
  };
@@ -1,13 +1,18 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Children, Fragment, isValidElement, use, useCallback, useId, useLayoutEffect, useRef, useState, } from "react";
3
+ import { Children, cloneElement, Fragment, isValidElement, memo, use, useCallback, useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
4
  import { Cell, Pie, Sector } from "recharts";
5
- import { categoryCells } from "./category-cells.js";
5
+ import { categoryCells, filterCategoryRows } from "./category-cells.js";
6
6
  import { useChart } from "./chart-context.js";
7
+ import { useChartInteraction, useInteractionFocus } from "./chart-interaction.js";
7
8
  import { EmphasisMark } from "./emphasis.js";
8
9
  import { useChartKeyboard, useLineInteraction } from "./line-chart.js";
9
10
  import { PieMotion } from "./pie-chart.js";
10
11
  import { PieMaterialFilter } from "./pie-material.js";
12
+ import { registerPiePinComponent } from "./pie-pin-identity.js";
13
+ // Native Pie keys its animation subtree by props identity even with animation disabled.
14
+ // Pointer/pin context updates must not replace an unchanged pressed native sector.
15
+ const StablePie = memo(Pie);
11
16
  // Recharts also supports Cell props as data when neither the chart nor Pie supplies rows.
12
17
  function cellProps(children) {
13
18
  const result = [];
@@ -30,11 +35,45 @@ function sameCells(previous, next) {
30
35
  Object.keys(props).every((key) => Object.is(old[key], props[key])));
31
36
  }));
32
37
  }
38
+ // Capture original rendered identity before an inner Cell handler can commit new rows.
39
+ function captureCellIdentity(children, keyAt, identities) {
40
+ let index = 0;
41
+ function capture(parts) {
42
+ return Children.map(parts, (child) => {
43
+ if (!isValidElement(child))
44
+ return child;
45
+ if (child.type === Fragment)
46
+ return cloneElement(child, {}, capture(child.props.children));
47
+ if (child.type !== Cell)
48
+ return child;
49
+ const key = keyAt(index++);
50
+ if (key === undefined)
51
+ return child;
52
+ return cloneElement(child, {
53
+ onClick: (event) => {
54
+ identities.set(event.nativeEvent, key);
55
+ child.props.onClick?.(event);
56
+ },
57
+ });
58
+ });
59
+ }
60
+ return capture(children);
61
+ }
33
62
  // One chart-level angular window reveals unchanged native sectors.
34
- function EntranceSector({ material, emphasisKey, scope, enabled, seriesStartAngle, seriesEndAngle, ...props }) {
63
+ function EntranceSector({ material, emphasisKey, scope, enabled, interactionKey, seriesStartAngle, seriesEndAngle, ...props }) {
35
64
  const keyboard = useChartKeyboard();
65
+ const interaction = useChartInteraction();
36
66
  const { reveal, progress, direction } = use(PieMotion);
37
- const semantic = emphasisKey ? emphasisKey(props.payload) : props.name;
67
+ const semantic = interactionKey
68
+ ? interactionKey(props.index ?? -1)
69
+ : emphasisKey
70
+ ? emphasisKey(props.payload)
71
+ : props.name;
72
+ const interactive = interactionKey !== undefined &&
73
+ interaction.interactive &&
74
+ interaction.markActivation &&
75
+ interaction.eligible.includes(String(semantic));
76
+ const focusRef = useInteractionFocus(String(semantic), interactive);
38
77
  const generatedId = useId();
39
78
  const sourceId = `kind-ui-pie-${generatedId.replace(/[^a-zA-Z0-9_-]/g, "_")}-paint`;
40
79
  const maskId = `${sourceId}-alpha`;
@@ -151,25 +190,102 @@ function EntranceSector({ material, emphasisKey, scope, enabled, seriesStartAngl
151
190
  const revealStart = fromStart ? seriesStartAngle : seriesStartAngle + nativeSpan;
152
191
  const revealSpan = Math.abs(nativeSpan) * (clockwise ? -1 : 1);
153
192
  const { className, cornerRadius, onClick, onMouseDown, onMouseUp, onMouseMove, onMouseOver, onMouseOut, onMouseEnter, onMouseLeave, ...sector } = props;
154
- return (_jsx(EmphasisMark, { enabled: enabled &&
155
- (emphasisKey !== undefined ? semantic !== undefined : typeof semantic === "string"), target: { kind: "sector", key: String(semantic), scope, seriesKey: String(semantic) }, keyboardActive: keyboard && props.isActive, children: _jsxs("g", { ref: markGroup, children: [maskEntrance && (_jsx("defs", { children: _jsx("mask", { id: entranceId, maskUnits: "userSpaceOnUse", x: props.cx - props.outerRadius - margin, y: props.cy - props.outerRadius - margin, width: (props.outerRadius + margin) * 2, height: (props.outerRadius + margin) * 2, style: { maskType: "alpha" }, children: _jsx(Sector, { fill: "white", "data-kind-ui": "pie-entrance-window", "data-direction": direction, "data-progress": progress, cx: props.cx, cy: props.cy, innerRadius: 0, outerRadius: props.outerRadius + margin, startAngle: revealStart, endAngle: revealStart + revealSpan * progress }) }) })), _jsxs("g", { mask: maskEntrance ? `url(#${entranceId})` : undefined, children: [materialized && (_jsxs("defs", { "data-kind-ui": "pie-material", "data-material": material, pointerEvents: "none", children: [_jsx(PieMaterialFilter, { material: material, id: filterId, strokeWidth: Math.max(0, Number.isFinite(resolvedStroke) ? resolvedStroke : 0, paintStroke), cx: props.cx, cy: props.cy, radius: props.outerRadius, thickness: props.outerRadius - props.innerRadius, bounds: paintBounds }), inset && (_jsx("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: paintBounds?.x ?? Math.floor(props.cx - props.outerRadius - margin), y: paintBounds?.y ?? Math.floor(props.cy - props.outerRadius - margin), width: paintBounds?.width ?? Math.ceil(props.outerRadius * 2 + margin * 2) + 1, height: paintBounds?.height ?? Math.ceil(props.outerRadius * 2 + margin * 2) + 1, style: { maskType: "alpha" }, children: _jsx("use", { href: `#${sourceId}` }) }))] })), materialized && material === "glow" && (
193
+ return (_jsx(EmphasisMark, { ref: focusRef, "data-interaction-focus-key": interactive ? String(semantic) : undefined, persistent: interactionKey !== undefined, enabled: enabled &&
194
+ (emphasisKey !== undefined ? semantic !== undefined : typeof semantic === "string"), target: { kind: "sector", key: String(semantic), scope, seriesKey: String(semantic) }, keyboardActive: keyboard && props.isActive === true, role: interactive ? "button" : undefined, tabIndex: interactive ? 0 : undefined, "aria-label": interactive
195
+ ? `${interaction.mode === "focus" ? "Highlight" : "Toggle"} ${String(semantic)}`
196
+ : undefined, "aria-pressed": interactive
197
+ ? interaction.mode === "focus"
198
+ ? interaction.selected === semantic
199
+ : true
200
+ : undefined, onKeyDown: (event) => {
201
+ if (interactive &&
202
+ event.target === event.currentTarget &&
203
+ !event.defaultPrevented &&
204
+ (event.key === "Enter" || event.key === " ")) {
205
+ if (!event.repeat)
206
+ interaction.activate({ kind: "category", key: String(semantic) }, "mark", event);
207
+ event.preventDefault();
208
+ }
209
+ }, children: _jsxs("g", { ref: markGroup, children: [maskEntrance && (_jsx("defs", { children: _jsx("mask", { id: entranceId, maskUnits: "userSpaceOnUse", x: props.cx - props.outerRadius - margin, y: props.cy - props.outerRadius - margin, width: (props.outerRadius + margin) * 2, height: (props.outerRadius + margin) * 2, style: { maskType: "alpha" }, children: _jsx(Sector, { fill: "white", "data-kind-ui": "pie-entrance-window", "data-direction": direction, "data-progress": progress, cx: props.cx, cy: props.cy, innerRadius: 0, outerRadius: props.outerRadius + margin, startAngle: revealStart, endAngle: revealStart + revealSpan * progress }) }) })), _jsxs("g", { mask: maskEntrance ? `url(#${entranceId})` : undefined, children: [materialized && (_jsxs("defs", { "data-kind-ui": "pie-material", "data-material": material, pointerEvents: "none", children: [_jsx(PieMaterialFilter, { material: material, id: filterId, strokeWidth: Math.max(0, Number.isFinite(resolvedStroke) ? resolvedStroke : 0, paintStroke), cx: props.cx, cy: props.cy, radius: props.outerRadius, thickness: props.outerRadius - props.innerRadius, bounds: paintBounds }), inset && (_jsx("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: paintBounds?.x ?? Math.floor(props.cx - props.outerRadius - margin), y: paintBounds?.y ?? Math.floor(props.cy - props.outerRadius - margin), width: paintBounds?.width ?? Math.ceil(props.outerRadius * 2 + margin * 2) + 1, height: paintBounds?.height ?? Math.ceil(props.outerRadius * 2 + margin * 2) + 1, style: { maskType: "alpha" }, children: _jsx("use", { href: `#${sourceId}` }) }))] })), materialized && material === "glow" && (
156
210
  // biome-ignore lint/a11y/noAriaHiddenOnFocusable: SVG decoration is explicitly nonfocusable and ignores pointer events.
157
- _jsx("g", { pointerEvents: "none", focusable: "false", "aria-hidden": "true", "data-kind-ui": "pie-halo", style: { clipPath: paintClip }, transform: paintClip !== "none" ? clipTransform.forward : undefined, children: _jsx("g", { transform: paintClip !== "none" ? clipTransform.inverse : undefined, filter: `url(#${filterId})`, children: _jsx("use", { href: `#${sourceId}` }) }) })), _jsx("g", { ...(inset ? { filter: `url(#${filterId})` } : {}), ...(inset ? { mask: `url(#${maskId})` } : {}), children: _jsxs("g", { id: sourceId, children: [materialized && (_jsx("rect", { x: paintBounds?.x ?? props.cx - props.outerRadius - margin, y: paintBounds?.y ?? props.cy - props.outerRadius - margin, width: paintBounds?.width ?? props.outerRadius * 2 + margin * 2, height: paintBounds?.height ?? props.outerRadius * 2 + margin * 2, fill: "white", fillOpacity: 0, pointerEvents: "none" })), _jsx(Sector, { ...sector, ...(onClick ? { onClick } : {}), ...(onMouseDown ? { onMouseDown } : {}), ...(onMouseUp ? { onMouseUp } : {}), ...(onMouseMove ? { onMouseMove } : {}), ...(onMouseOver ? { onMouseOver } : {}), ...(onMouseOut ? { onMouseOut } : {}), ...(onMouseEnter ? { onMouseEnter } : {}), ...(onMouseLeave ? { onMouseLeave } : {}), ...(className !== undefined ? { className } : {}), ...(cornerRadius !== undefined ? { cornerRadius } : {}), "data-kind-ui": "pie-sector", "data-reveal": maskEntrance ? "on" : "off" })] }) })] })] }) }));
211
+ _jsx("g", { pointerEvents: "none", focusable: "false", "aria-hidden": "true", "data-kind-ui": "pie-halo", style: { clipPath: paintClip }, transform: paintClip !== "none" ? clipTransform.forward : undefined, children: _jsx("g", { transform: paintClip !== "none" ? clipTransform.inverse : undefined, filter: `url(#${filterId})`, children: _jsx("use", { href: `#${sourceId}` }) }) })), _jsx("g", { ...(inset ? { filter: `url(#${filterId})` } : {}), ...(inset ? { mask: `url(#${maskId})` } : {}), children: _jsxs("g", { id: sourceId, children: [materialized && (_jsx("rect", { x: paintBounds?.x ?? props.cx - props.outerRadius - margin, y: paintBounds?.y ?? props.cy - props.outerRadius - margin, width: paintBounds?.width ?? props.outerRadius * 2 + margin * 2, height: paintBounds?.height ?? props.outerRadius * 2 + margin * 2, fill: "white", fillOpacity: 0, pointerEvents: "none" })), _jsx(Sector, { ...sector, ...(onClick ? { onClick } : {}), ...(onMouseDown ? { onMouseDown } : {}), ...(onMouseUp ? { onMouseUp } : {}), ...(onMouseMove ? { onMouseMove } : {}), ...(onMouseOver ? { onMouseOver } : {}), ...(onMouseOut ? { onMouseOut } : {}), ...(onMouseEnter ? { onMouseEnter } : {}), ...(onMouseLeave ? { onMouseLeave } : {}), ...(className !== undefined ? { className } : {}), ...(cornerRadius !== undefined ? { cornerRadius } : {}), "data-kind-ui": "pie-sector", "data-sector-span": props.endAngle - props.startAngle, "data-reveal": maskEntrance ? "on" : "off" })] }) })] })] }) }));
158
212
  }
159
213
  /** A native Pie. Use innerRadius for donuts; category data, Cells and visibility are consumer-owned. */
160
214
  export function PieSeries(props) {
161
- const { material = "plain", emphasisKey, categoryKey, ...nativeProps } = props;
162
- const { config } = useChart();
163
- const children = categoryKey === undefined
164
- ? props.children
165
- : categoryCells(props.data, categoryKey, config, props.children, props.fill);
215
+ const { material = "plain", glowCategories, emphasisKey, categoryKey, interactionBinding, ...nativeProps } = props;
216
+ const interaction = useChartInteraction();
217
+ if (interactionBinding &&
218
+ (interaction.kind !== "category" || categoryKey === undefined || props.data === undefined))
219
+ throw new Error("PieSeries Root interaction binding requires category-kind Root, categoryKey and explicit data");
220
+ const filtered = useMemo(() => interactionBinding && props.data && categoryKey !== undefined
221
+ ? filterCategoryRows(props.data, categoryKey, interaction.visible, props.children)
222
+ : undefined, [interactionBinding, props.data, categoryKey, interaction.visible, props.children]);
223
+ const data = filtered?.data ?? props.data;
224
+ const originalChildren = filtered?.children ?? props.children;
225
+ const resolveInteractionKey = useCallback((index) => {
226
+ // Cell props can override native payload fields; native index still addresses our rendered rows.
227
+ const row = data?.[index];
228
+ if (row === undefined)
229
+ return undefined;
230
+ if (typeof categoryKey === "function")
231
+ return categoryKey(row);
232
+ if (categoryKey !== undefined &&
233
+ typeof row === "object" &&
234
+ row !== null &&
235
+ Object.hasOwn(row, categoryKey))
236
+ return String(row[categoryKey]);
237
+ throw new Error("PieSeries interaction requires a stable category key");
238
+ }, [categoryKey, data]);
239
+ const interactionKey = interactionBinding ? resolveInteractionKey : undefined;
240
+ const { config, paints } = useChart();
241
+ const children = useMemo(() => categoryKey === undefined
242
+ ? originalChildren
243
+ : categoryCells(data, categoryKey, config, paints, originalChildren, props.fill), [categoryKey, data, config, paints, originalChildren, props.fill]);
244
+ const identities = useRef(new WeakMap());
245
+ const boundChildren = useMemo(() => interactionKey && interaction.interactive && interaction.markActivation
246
+ ? captureCellIdentity(children, interactionKey, identities.current)
247
+ : children, [children, interactionKey, interaction.interactive, interaction.markActivation]);
248
+ const latestClick = useRef({ handler: props.onClick, interactionKey, interaction });
249
+ useLayoutEffect(() => {
250
+ latestClick.current = { handler: props.onClick, interactionKey, interaction };
251
+ });
252
+ const activate = useCallback((...args) => {
253
+ // Capture the pressed row before a consumer can synchronously reorder/filter the data.
254
+ const pressed = latestClick.current;
255
+ const key = identities.current.get(args[2].nativeEvent) ?? pressed.interactionKey?.(args[1]);
256
+ pressed.handler?.(...args);
257
+ if (key !== undefined)
258
+ latestClick.current.interaction.activate({ kind: "category", key }, "mark", args[2]);
259
+ }, []);
260
+ const onClick = interactionBinding && interaction.interactive && interaction.markActivation
261
+ ? activate
262
+ : props.onClick;
263
+ if (glowCategories !== undefined && categoryKey === undefined)
264
+ throw new Error("glowCategories requires categoryKey and explicit series data");
265
+ // Native sector indices align with explicit data, but membership uses the original
266
+ // row identity, before native Cell props can override payload fields.
267
+ const glowRows = useMemo(() => {
268
+ if (categoryKey === undefined || glowCategories === undefined)
269
+ return undefined;
270
+ const glowing = new Set(glowCategories);
271
+ return data?.map((row) => {
272
+ const key = typeof categoryKey === "function"
273
+ ? categoryKey(row)
274
+ : row !== null && typeof row === "object" && Object.hasOwn(row, categoryKey)
275
+ ? row[categoryKey]
276
+ : undefined;
277
+ return typeof key === "string" && glowing.has(key);
278
+ });
279
+ }, [data, categoryKey, glowCategories]);
166
280
  const seriesId = useId();
167
281
  const { invalidate, emphasisScope } = useLineInteraction();
168
282
  const scope = `${emphasisScope}/${seriesId}`;
169
- const sectorShape = useCallback((sector) => (_jsx(EntranceSector, { ...sector, material: material, scope: scope, emphasisKey: emphasisKey, enabled: props.activeShape === undefined && props.inactiveShape === undefined, seriesStartAngle: props.startAngle ?? 0, seriesEndAngle: props.endAngle ?? 360 })), [
283
+ const sectorShape = useCallback((sector) => (_jsx(EntranceSector, { ...sector, material: sector.index !== undefined && glowRows?.[sector.index] ? "glow" : material, interactionKey: interactionKey, scope: scope, emphasisKey: emphasisKey, enabled: props.activeShape === undefined && props.inactiveShape === undefined, seriesStartAngle: props.startAngle ?? 0, seriesEndAngle: props.endAngle ?? 360 })), [
170
284
  material,
285
+ glowRows,
171
286
  scope,
172
287
  emphasisKey,
288
+ interactionKey,
173
289
  props.activeShape,
174
290
  props.inactiveShape,
175
291
  props.startAngle,
@@ -193,6 +309,8 @@ export function PieSeries(props) {
193
309
  props.activeShape,
194
310
  props.inactiveShape,
195
311
  material,
312
+ categoryKey,
313
+ glowCategories,
196
314
  ];
197
315
  const cells = cellProps(props.children);
198
316
  const previousCells = useRef(cells);
@@ -204,5 +322,10 @@ export function PieSeries(props) {
204
322
  previousCells.current = cells;
205
323
  previous.current = inputs;
206
324
  });
207
- return (_jsx(Pie, { ...nativeProps, stroke: props.stroke ?? "none", shape: props.shape ?? sectorShape, isAnimationActive: false, children: children }));
325
+ return (_jsx(StablePie, { ...nativeProps, ...(data !== undefined ? { data } : {}), ...(onClick !== undefined ? { onClick } : {}), stroke: props.stroke ?? "none", shape: props.shape ?? sectorShape, ...(interactionBinding && props.shape === undefined && props.activeShape === undefined
326
+ ? { activeShape: sectorShape }
327
+ : {}), ...(interactionBinding && props.shape === undefined && props.inactiveShape === undefined
328
+ ? { inactiveShape: sectorShape }
329
+ : {}), isAnimationActive: false, children: boundChildren }));
208
330
  }
331
+ registerPiePinComponent(PieSeries, "series");
@@ -0,0 +1,4 @@
1
+ import { type ReactNode } from "react";
2
+ export declare const PieTooltipPin: import("react").Context<number | undefined>;
3
+ /** Resolve against current rows; the returned index is never retained as identity. */
4
+ export declare function pinnedPieIndex(children: ReactNode, category: string, visibleSeries?: readonly string[]): number | undefined;
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { Children, createContext, Fragment, isValidElement } from "react";
3
+ import { piePinComponentKind } from "./pie-pin-identity.js";
4
+ export const PieTooltipPin = createContext(undefined);
5
+ /** Resolve against current rows; the returned index is never retained as identity. */
6
+ export function pinnedPieIndex(children, category, visibleSeries) {
7
+ const series = [];
8
+ let tooltips = 0;
9
+ let unsupported = false;
10
+ function collect(nodes) {
11
+ Children.forEach(nodes, (child) => {
12
+ if (!isValidElement(child)) {
13
+ if (child !== null && child !== undefined && typeof child !== "boolean")
14
+ unsupported = true;
15
+ return;
16
+ }
17
+ if (child.type === Fragment)
18
+ collect(child.props.children);
19
+ else if (piePinComponentKind(child.type) === "series")
20
+ series.push(child.props);
21
+ else if (piePinComponentKind(child.type) === "tooltip")
22
+ tooltips += 1;
23
+ else
24
+ unsupported = true;
25
+ });
26
+ }
27
+ collect(children);
28
+ const candidate = series[0];
29
+ if (series.length !== 1 ||
30
+ tooltips !== 1 ||
31
+ unsupported ||
32
+ !candidate?.data ||
33
+ candidate.categoryKey === undefined)
34
+ throw new Error("defaultPinnedCategory requires one direct PieSeries with explicit data and categoryKey, and one direct Tooltip");
35
+ const { data, categoryKey, hide } = candidate;
36
+ if (hide)
37
+ return undefined;
38
+ const keys = data.map((row) => typeof categoryKey === "function"
39
+ ? categoryKey(row)
40
+ : row !== null && typeof row === "object" && Object.hasOwn(row, categoryKey)
41
+ ? row[categoryKey]
42
+ : undefined);
43
+ // Ambiguous identities are never silently resolved by position, including hidden rows.
44
+ if (keys.filter((key) => key === category).length !== 1)
45
+ return undefined;
46
+ // Only a bound Pie filters its native data. Unbound composition keeps its original indices.
47
+ const renderedKeys = candidate.interactionBinding === "root" && visibleSeries !== undefined
48
+ ? keys.filter((key) => typeof key === "string" && visibleSeries.includes(key))
49
+ : keys;
50
+ const index = renderedKeys.indexOf(category);
51
+ return index < 0 ? undefined : index;
52
+ }
@@ -0,0 +1,19 @@
1
+ import { type DotProps } from "recharts";
2
+ export type PointStyle = "default" | "border" | "colored-border";
3
+ export type PointMarkerProps = DotProps & {
4
+ variant?: PointStyle | undefined;
5
+ };
6
+ /** Paint only: native coordinates, radius, events and inspection remain owned by Recharts. */
7
+ export declare function markerPaint(variant: PointStyle, color: string | undefined): {
8
+ fill?: never;
9
+ stroke?: never;
10
+ strokeWidth?: never;
11
+ strokeDasharray?: never;
12
+ } | {
13
+ fill: string | undefined;
14
+ stroke: string | undefined;
15
+ strokeWidth: number;
16
+ strokeDasharray: string;
17
+ };
18
+ /** Native Dot renderer: variant paint wins engine paint; SVG style remains consumer-owned. */
19
+ export declare function PointMarker({ variant, ...props }: PointMarkerProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Dot } from "recharts";
4
+ /** Paint only: native coordinates, radius, events and inspection remain owned by Recharts. */
5
+ export function markerPaint(variant, color) {
6
+ if (variant === "default")
7
+ return {};
8
+ const surface = "var(--kind-ui-chart-marker-surface, var(--card, white))";
9
+ return {
10
+ fill: variant === "border" ? color : surface,
11
+ stroke: variant === "border" ? surface : color,
12
+ strokeWidth: 2,
13
+ strokeDasharray: "none",
14
+ };
15
+ }
16
+ /** Native Dot renderer: variant paint wins engine paint; SVG style remains consumer-owned. */
17
+ export function PointMarker({ variant = "default", ...props }) {
18
+ return (_jsx(Dot, { ...props, ...markerPaint(variant, props.stroke ?? props.fill), "data-kind-ui": "point-marker", "data-point-style": variant }));
19
+ }