@x-plat/design-system 0.5.18 → 0.5.19

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.
@@ -126,6 +126,37 @@ var useChartSize = (ref) => {
126
126
  }, [ref]);
127
127
  return size;
128
128
  };
129
+ var prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
130
+ var useChartAnimation = (containerRef, dataKey) => {
131
+ const [animate, setAnimate] = import_react.default.useState(false);
132
+ const prevDataKey = import_react.default.useRef(dataKey);
133
+ const hasAnimated = import_react.default.useRef(false);
134
+ import_react.default.useEffect(() => {
135
+ if (prefersReducedMotion()) return;
136
+ const el = containerRef.current;
137
+ if (!el) return;
138
+ const observer = new IntersectionObserver(
139
+ ([entry]) => {
140
+ if (entry.isIntersecting && !hasAnimated.current) {
141
+ hasAnimated.current = true;
142
+ setAnimate(true);
143
+ }
144
+ },
145
+ { threshold: 0.1 }
146
+ );
147
+ observer.observe(el);
148
+ return () => observer.disconnect();
149
+ }, [containerRef]);
150
+ import_react.default.useEffect(() => {
151
+ if (dataKey !== prevDataKey.current) {
152
+ prevDataKey.current = dataKey;
153
+ if (prefersReducedMotion()) return;
154
+ setAnimate(false);
155
+ requestAnimationFrame(() => setAnimate(true));
156
+ }
157
+ }, [dataKey]);
158
+ return animate || prefersReducedMotion();
159
+ };
129
160
  var useChartTooltip = (enabled) => {
130
161
  const [tooltip, setTooltip] = import_react.default.useState({
131
162
  visible: false,
@@ -191,7 +222,7 @@ var AxisLabels = import_react.default.memo(({ labels, count, chartW, height }) =
191
222
  }) });
192
223
  });
193
224
  AxisLabels.displayName = "AxisLabels";
194
- var LineChart = import_react.default.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
225
+ var LineChart = import_react.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
195
226
  const entries = import_react.default.useMemo(() => Object.entries(data), [data]);
196
227
  const maxVal = import_react.default.useMemo(() => {
197
228
  const allValues = entries.flatMap(([, v]) => v);
@@ -211,18 +242,52 @@ var LineChart = import_react.default.memo(({ data, labels, width, height, onHove
211
242
  [entries, count, chartW, chartH, maxVal]
212
243
  );
213
244
  const showPoints = count <= 100;
245
+ const lineRefs = import_react.default.useRef([]);
246
+ import_react.default.useEffect(() => {
247
+ if (!animate) return;
248
+ lineRefs.current.forEach((el) => {
249
+ if (!el) return;
250
+ const len = el.getTotalLength();
251
+ el.style.strokeDasharray = `${len}`;
252
+ el.style.strokeDashoffset = `${len}`;
253
+ requestAnimationFrame(() => {
254
+ el.style.transition = "stroke-dashoffset 1200ms ease-out 200ms";
255
+ el.style.strokeDashoffset = "0";
256
+ });
257
+ });
258
+ }, [animate, seriesPoints]);
214
259
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
215
260
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GridLines, { width, height, chartH, maxVal }),
216
261
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxisLabels, { labels, count, chartW, height }),
217
262
  entries.map(([key], di) => {
218
263
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
219
264
  const color = palette[2];
265
+ const areaColor = palette[0];
220
266
  const points = seriesPoints[di];
267
+ const gradientId = `line-gradient-${di}`;
268
+ const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
269
+ const areaD = `M ${points[0].x},${points[0].y} ${points.map((p) => `L ${p.x},${p.y}`).join(" ")} L ${points[points.length - 1].x},${PADDING.top + chartH} L ${points[0].x},${PADDING.top + chartH} Z`;
221
270
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
272
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
274
+ ] }) }),
275
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
276
+ "path",
277
+ {
278
+ d: areaD,
279
+ fill: `url(#${gradientId})`,
280
+ className: "chart-area",
281
+ style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
282
+ }
283
+ ),
222
284
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
223
285
  "polyline",
224
286
  {
225
- points: points.map((p) => `${p.x},${p.y}`).join(" "),
287
+ ref: (el) => {
288
+ lineRefs.current[di] = el;
289
+ },
290
+ points: polyPoints,
226
291
  fill: "none",
227
292
  stroke: color,
228
293
  strokeWidth: "2"
@@ -247,7 +312,7 @@ var LineChart = import_react.default.memo(({ data, labels, width, height, onHove
247
312
  ] });
248
313
  });
249
314
  LineChart.displayName = "LineChart";
250
- var CurveChart = import_react.default.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
315
+ var CurveChart = import_react.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
251
316
  const entries = import_react.default.useMemo(() => Object.entries(data), [data]);
252
317
  const maxVal = import_react.default.useMemo(() => {
253
318
  const allValues = entries.flatMap(([, v]) => v);
@@ -267,6 +332,20 @@ var CurveChart = import_react.default.memo(({ data, labels, width, height, onHov
267
332
  [entries, count, chartW, chartH, maxVal]
268
333
  );
269
334
  const showPoints = count <= 100;
335
+ const lineRefs = import_react.default.useRef([]);
336
+ import_react.default.useEffect(() => {
337
+ if (!animate) return;
338
+ lineRefs.current.forEach((el) => {
339
+ if (!el) return;
340
+ const len = el.getTotalLength();
341
+ el.style.strokeDasharray = `${len}`;
342
+ el.style.strokeDashoffset = `${len}`;
343
+ requestAnimationFrame(() => {
344
+ el.style.transition = "stroke-dashoffset 1200ms ease-out 200ms";
345
+ el.style.strokeDashoffset = "0";
346
+ });
347
+ });
348
+ }, [animate, seriesPoints]);
270
349
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
271
350
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GridLines, { width, height, chartH, maxVal }),
272
351
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxisLabels, { labels, count, chartW, height }),
@@ -283,8 +362,27 @@ var CurveChart = import_react.default.memo(({ data, labels, width, height, onHov
283
362
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
284
363
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
285
364
  ] }) }),
286
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: areaPath, fill: `url(#${gradientId})` }),
287
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: linePath, fill: "none", stroke: color, strokeWidth: "2" }),
365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
366
+ "path",
367
+ {
368
+ d: areaPath,
369
+ fill: `url(#${gradientId})`,
370
+ className: "chart-area",
371
+ style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
372
+ }
373
+ ),
374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
375
+ "path",
376
+ {
377
+ ref: (el) => {
378
+ lineRefs.current[di] = el;
379
+ },
380
+ d: linePath,
381
+ fill: "none",
382
+ stroke: color,
383
+ strokeWidth: "2"
384
+ }
385
+ ),
288
386
  showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
289
387
  "circle",
290
388
  {
@@ -304,7 +402,7 @@ var CurveChart = import_react.default.memo(({ data, labels, width, height, onHov
304
402
  ] });
305
403
  });
306
404
  CurveChart.displayName = "CurveChart";
307
- var BarChart = import_react.default.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
405
+ var BarChart = import_react.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
308
406
  const entries = import_react.default.useMemo(() => Object.entries(data), [data]);
309
407
  const maxVal = import_react.default.useMemo(() => {
310
408
  const allValues = entries.flatMap(([, v]) => v);
@@ -317,6 +415,7 @@ var BarChart = import_react.default.memo(({ data, labels, width, height, onHover
317
415
  const groupW = chartW / count;
318
416
  const barGap = groupCount > 1 ? 2 : 0;
319
417
  const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
418
+ const baseline = PADDING.top + chartH;
320
419
  const bars = import_react.default.useMemo(
321
420
  () => entries.map(
322
421
  ([, values], di) => values.map((v, i) => {
@@ -342,12 +441,17 @@ var BarChart = import_react.default.memo(({ data, labels, width, height, onHover
342
441
  return bars[di].map((b, i) => {
343
442
  const r = Math.min(4, b.w / 2);
344
443
  const d = b.h <= r ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r} Q ${b.x} ${b.y} ${b.x + r} ${b.y} H ${b.x + b.w - r} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r} V ${b.y + b.h} Z`;
444
+ const delay = 100 + i * 80;
345
445
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
346
446
  "path",
347
447
  {
348
448
  d,
349
449
  fill: color,
350
- className: "chart-bar",
450
+ className: `chart-bar ${animate ? "chart-bar-animate" : ""}`,
451
+ style: animate ? {
452
+ transformOrigin: `${b.x + b.w / 2}px ${baseline}px`,
453
+ animationDelay: `${delay}ms`
454
+ } : void 0,
351
455
  onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
352
456
  onMouseMove: onMove,
353
457
  onMouseLeave: onLeave
@@ -360,7 +464,7 @@ var BarChart = import_react.default.memo(({ data, labels, width, height, onHover
360
464
  });
361
465
  BarChart.displayName = "BarChart";
362
466
  var PieDonutChart = import_react.default.memo(
363
- ({ data, labels, width, height, isDoughnut, onHover, onMove, onLeave }) => {
467
+ ({ data, labels, width, height, animate, isDoughnut, onHover, onMove, onLeave }) => {
364
468
  const entries = import_react.default.useMemo(() => Object.entries(data), [data]);
365
469
  const values = import_react.default.useMemo(() => entries.flatMap(([, v]) => v), [entries]);
366
470
  const total = import_react.default.useMemo(() => values.reduce((a, b) => a + b, 0) || 1, [values]);
@@ -400,20 +504,34 @@ var PieDonutChart = import_react.default.memo(
400
504
  return { d, lx, ly, v, pct, angle, label: labels[i] || `${i + 1}` };
401
505
  });
402
506
  }, [values, total, cx, cy, r, innerR, labels]);
403
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: `0 0 ${size} ${size}`, className: "chart-svg chart-pie", children: sliceData.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
404
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
405
- "path",
406
- {
407
- d: s.d,
408
- fill: PIE_COLORS[(i + colorOffset) % PIE_COLORS.length],
409
- className: "chart-slice",
410
- onMouseEnter: (e) => onHover(e, `${s.label}: ${s.v} (${s.pct}%)`),
411
- onMouseMove: onMove,
412
- onMouseLeave: onLeave
413
- }
414
- ),
415
- s.angle > 0.2 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("text", { x: s.lx, y: s.ly, className: "chart-pie-label", textAnchor: "middle", dominantBaseline: "central", children: s.v })
416
- ] }, i)) });
507
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: `0 0 ${size} ${size}`, className: "chart-svg chart-pie", children: sliceData.map((s, i) => {
508
+ const delay = i * 100;
509
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { className: animate ? "chart-slice-group-animate" : "", style: animate ? { animationDelay: `${delay}ms` } : void 0, children: [
510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
511
+ "path",
512
+ {
513
+ d: s.d,
514
+ fill: PIE_COLORS[(i + colorOffset) % PIE_COLORS.length],
515
+ className: "chart-slice",
516
+ onMouseEnter: (e) => onHover(e, `${s.label}: ${s.v} (${s.pct}%)`),
517
+ onMouseMove: onMove,
518
+ onMouseLeave: onLeave
519
+ }
520
+ ),
521
+ s.angle > 0.2 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
522
+ "text",
523
+ {
524
+ x: s.lx,
525
+ y: s.ly,
526
+ className: `chart-pie-label ${animate ? "chart-pie-label-animate" : ""}`,
527
+ style: animate ? { animationDelay: `${delay + 150}ms` } : void 0,
528
+ textAnchor: "middle",
529
+ dominantBaseline: "central",
530
+ children: s.v
531
+ }
532
+ )
533
+ ] }, i);
534
+ }) });
417
535
  }
418
536
  );
419
537
  PieDonutChart.displayName = "PieDonutChart";
@@ -447,13 +565,15 @@ var Chart = import_react.default.memo((props) => {
447
565
  const { width, height } = useChartSize(containerRef);
448
566
  const stableData = import_react.default.useMemo(() => data, [JSON.stringify(data)]);
449
567
  const stableLabels = import_react.default.useMemo(() => labels, [JSON.stringify(labels)]);
568
+ const dataKey = import_react.default.useMemo(() => JSON.stringify(labels), [labels]);
569
+ const animate = useChartAnimation(containerRef, dataKey);
450
570
  const ready = width > 0 && height > 0;
451
571
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lib-xplat-chart", ref: containerRef, children: [
452
- ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
453
- ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
454
- ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
455
- ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
456
- ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
572
+ ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LineChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
573
+ ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CurveChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
574
+ ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
575
+ ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
576
+ ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
457
577
  tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
458
578
  ] });
459
579
  });
@@ -25,6 +25,7 @@
25
25
  font-weight: 600;
26
26
  fill: var(--semantic-text-inverse);
27
27
  pointer-events: none;
28
+ opacity: 1;
28
29
  }
29
30
  .lib-xplat-chart .chart-pie {
30
31
  max-width: 300px;
@@ -34,10 +35,10 @@
34
35
  cursor: pointer;
35
36
  r: 0;
36
37
  opacity: 0;
37
- transition: r 0.15s, opacity 0.15s;
38
+ transition: r 0.15s ease-out, opacity 0.15s ease-out;
38
39
  }
39
40
  .lib-xplat-chart .chart-point:hover {
40
- r: 5;
41
+ r: 6;
41
42
  opacity: 1;
42
43
  }
43
44
  .lib-xplat-chart .chart-svg:hover .chart-point {
@@ -46,7 +47,7 @@
46
47
  }
47
48
  .lib-xplat-chart .chart-bar {
48
49
  cursor: pointer;
49
- transition: opacity 0.15s, filter 0.15s;
50
+ transition: opacity 0.15s ease-out, filter 0.15s ease-out;
50
51
  }
51
52
  .lib-xplat-chart .chart-bar:hover {
52
53
  opacity: 0.85;
@@ -57,15 +58,22 @@
57
58
  stroke: var(--semantic-surface-neutral-default);
58
59
  stroke-width: 2;
59
60
  transition:
60
- opacity 0.15s,
61
- filter 0.15s,
62
- transform 0.15s;
61
+ opacity 0.15s ease-out,
62
+ filter 0.15s ease-out,
63
+ transform 0.15s ease-out;
63
64
  transform-origin: center;
64
65
  }
66
+ .lib-xplat-chart .chart-svg:hover .chart-slice {
67
+ opacity: 0.5;
68
+ }
65
69
  .lib-xplat-chart .chart-slice:hover {
66
- opacity: 0.9;
70
+ opacity: 1 !important;
71
+ transform: scale(1.03);
67
72
  filter: brightness(1.05) drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
68
73
  }
74
+ .lib-xplat-chart .chart-area {
75
+ opacity: 1;
76
+ }
69
77
  .lib-xplat-chart .chart-tooltip {
70
78
  position: absolute;
71
79
  transform: translate(-50%, -100%);
@@ -78,4 +86,63 @@
78
86
  white-space: nowrap;
79
87
  pointer-events: none;
80
88
  z-index: 10;
89
+ animation: chart-tooltip-in 150ms ease-out;
90
+ }
91
+ .lib-xplat-chart .chart-bar-animate {
92
+ animation: chart-bar-grow 800ms ease-out both;
93
+ }
94
+ .lib-xplat-chart .chart-slice-group-animate {
95
+ animation: chart-slice-in 1000ms ease-out both;
96
+ }
97
+ .lib-xplat-chart .chart-pie-label-animate {
98
+ animation: chart-fade-in 150ms ease-out both;
99
+ }
100
+ .lib-xplat-chart .chart-area[style*=animationDelay] {
101
+ animation: chart-fade-in 800ms ease-out both;
102
+ }
103
+ @keyframes chart-bar-grow {
104
+ from {
105
+ transform: scaleY(0);
106
+ }
107
+ to {
108
+ transform: scaleY(1);
109
+ }
110
+ }
111
+ @keyframes chart-slice-in {
112
+ from {
113
+ opacity: 0;
114
+ transform: scale(0.8);
115
+ }
116
+ to {
117
+ opacity: 1;
118
+ transform: scale(1);
119
+ }
120
+ }
121
+ @keyframes chart-fade-in {
122
+ from {
123
+ opacity: 0;
124
+ }
125
+ to {
126
+ opacity: 1;
127
+ }
128
+ }
129
+ @keyframes chart-tooltip-in {
130
+ from {
131
+ opacity: 0;
132
+ transform: translate(-50%, -90%);
133
+ }
134
+ to {
135
+ opacity: 1;
136
+ transform: translate(-50%, -100%);
137
+ }
138
+ }
139
+ @media (prefers-reduced-motion: reduce) {
140
+ .lib-xplat-chart .chart-bar-animate,
141
+ .lib-xplat-chart .chart-slice-group-animate,
142
+ .lib-xplat-chart .chart-pie-label-animate,
143
+ .lib-xplat-chart .chart-area {
144
+ animation: none !important;
145
+ opacity: 1 !important;
146
+ transform: none !important;
147
+ }
81
148
  }
@@ -90,6 +90,37 @@ var useChartSize = (ref) => {
90
90
  }, [ref]);
91
91
  return size;
92
92
  };
93
+ var prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
94
+ var useChartAnimation = (containerRef, dataKey) => {
95
+ const [animate, setAnimate] = React.useState(false);
96
+ const prevDataKey = React.useRef(dataKey);
97
+ const hasAnimated = React.useRef(false);
98
+ React.useEffect(() => {
99
+ if (prefersReducedMotion()) return;
100
+ const el = containerRef.current;
101
+ if (!el) return;
102
+ const observer = new IntersectionObserver(
103
+ ([entry]) => {
104
+ if (entry.isIntersecting && !hasAnimated.current) {
105
+ hasAnimated.current = true;
106
+ setAnimate(true);
107
+ }
108
+ },
109
+ { threshold: 0.1 }
110
+ );
111
+ observer.observe(el);
112
+ return () => observer.disconnect();
113
+ }, [containerRef]);
114
+ React.useEffect(() => {
115
+ if (dataKey !== prevDataKey.current) {
116
+ prevDataKey.current = dataKey;
117
+ if (prefersReducedMotion()) return;
118
+ setAnimate(false);
119
+ requestAnimationFrame(() => setAnimate(true));
120
+ }
121
+ }, [dataKey]);
122
+ return animate || prefersReducedMotion();
123
+ };
93
124
  var useChartTooltip = (enabled) => {
94
125
  const [tooltip, setTooltip] = React.useState({
95
126
  visible: false,
@@ -155,7 +186,7 @@ var AxisLabels = React.memo(({ labels, count, chartW, height }) => {
155
186
  }) });
156
187
  });
157
188
  AxisLabels.displayName = "AxisLabels";
158
- var LineChart = React.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
189
+ var LineChart = React.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
159
190
  const entries = React.useMemo(() => Object.entries(data), [data]);
160
191
  const maxVal = React.useMemo(() => {
161
192
  const allValues = entries.flatMap(([, v]) => v);
@@ -175,18 +206,52 @@ var LineChart = React.memo(({ data, labels, width, height, onHover, onMove, onLe
175
206
  [entries, count, chartW, chartH, maxVal]
176
207
  );
177
208
  const showPoints = count <= 100;
209
+ const lineRefs = React.useRef([]);
210
+ React.useEffect(() => {
211
+ if (!animate) return;
212
+ lineRefs.current.forEach((el) => {
213
+ if (!el) return;
214
+ const len = el.getTotalLength();
215
+ el.style.strokeDasharray = `${len}`;
216
+ el.style.strokeDashoffset = `${len}`;
217
+ requestAnimationFrame(() => {
218
+ el.style.transition = "stroke-dashoffset 1200ms ease-out 200ms";
219
+ el.style.strokeDashoffset = "0";
220
+ });
221
+ });
222
+ }, [animate, seriesPoints]);
178
223
  return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
179
224
  /* @__PURE__ */ jsx(GridLines, { width, height, chartH, maxVal }),
180
225
  /* @__PURE__ */ jsx(AxisLabels, { labels, count, chartW, height }),
181
226
  entries.map(([key], di) => {
182
227
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
183
228
  const color = palette[2];
229
+ const areaColor = palette[0];
184
230
  const points = seriesPoints[di];
231
+ const gradientId = `line-gradient-${di}`;
232
+ const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
233
+ const areaD = `M ${points[0].x},${points[0].y} ${points.map((p) => `L ${p.x},${p.y}`).join(" ")} L ${points[points.length - 1].x},${PADDING.top + chartH} L ${points[0].x},${PADDING.top + chartH} Z`;
185
234
  return /* @__PURE__ */ jsxs("g", { children: [
235
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
236
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
237
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
238
+ ] }) }),
239
+ /* @__PURE__ */ jsx(
240
+ "path",
241
+ {
242
+ d: areaD,
243
+ fill: `url(#${gradientId})`,
244
+ className: "chart-area",
245
+ style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
246
+ }
247
+ ),
186
248
  /* @__PURE__ */ jsx(
187
249
  "polyline",
188
250
  {
189
- points: points.map((p) => `${p.x},${p.y}`).join(" "),
251
+ ref: (el) => {
252
+ lineRefs.current[di] = el;
253
+ },
254
+ points: polyPoints,
190
255
  fill: "none",
191
256
  stroke: color,
192
257
  strokeWidth: "2"
@@ -211,7 +276,7 @@ var LineChart = React.memo(({ data, labels, width, height, onHover, onMove, onLe
211
276
  ] });
212
277
  });
213
278
  LineChart.displayName = "LineChart";
214
- var CurveChart = React.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
279
+ var CurveChart = React.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
215
280
  const entries = React.useMemo(() => Object.entries(data), [data]);
216
281
  const maxVal = React.useMemo(() => {
217
282
  const allValues = entries.flatMap(([, v]) => v);
@@ -231,6 +296,20 @@ var CurveChart = React.memo(({ data, labels, width, height, onHover, onMove, onL
231
296
  [entries, count, chartW, chartH, maxVal]
232
297
  );
233
298
  const showPoints = count <= 100;
299
+ const lineRefs = React.useRef([]);
300
+ React.useEffect(() => {
301
+ if (!animate) return;
302
+ lineRefs.current.forEach((el) => {
303
+ if (!el) return;
304
+ const len = el.getTotalLength();
305
+ el.style.strokeDasharray = `${len}`;
306
+ el.style.strokeDashoffset = `${len}`;
307
+ requestAnimationFrame(() => {
308
+ el.style.transition = "stroke-dashoffset 1200ms ease-out 200ms";
309
+ el.style.strokeDashoffset = "0";
310
+ });
311
+ });
312
+ }, [animate, seriesPoints]);
234
313
  return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
235
314
  /* @__PURE__ */ jsx(GridLines, { width, height, chartH, maxVal }),
236
315
  /* @__PURE__ */ jsx(AxisLabels, { labels, count, chartW, height }),
@@ -247,8 +326,27 @@ var CurveChart = React.memo(({ data, labels, width, height, onHover, onMove, onL
247
326
  /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
248
327
  /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
249
328
  ] }) }),
250
- /* @__PURE__ */ jsx("path", { d: areaPath, fill: `url(#${gradientId})` }),
251
- /* @__PURE__ */ jsx("path", { d: linePath, fill: "none", stroke: color, strokeWidth: "2" }),
329
+ /* @__PURE__ */ jsx(
330
+ "path",
331
+ {
332
+ d: areaPath,
333
+ fill: `url(#${gradientId})`,
334
+ className: "chart-area",
335
+ style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
336
+ }
337
+ ),
338
+ /* @__PURE__ */ jsx(
339
+ "path",
340
+ {
341
+ ref: (el) => {
342
+ lineRefs.current[di] = el;
343
+ },
344
+ d: linePath,
345
+ fill: "none",
346
+ stroke: color,
347
+ strokeWidth: "2"
348
+ }
349
+ ),
252
350
  showPoints && points.map((p, i) => /* @__PURE__ */ jsx(
253
351
  "circle",
254
352
  {
@@ -268,7 +366,7 @@ var CurveChart = React.memo(({ data, labels, width, height, onHover, onMove, onL
268
366
  ] });
269
367
  });
270
368
  CurveChart.displayName = "CurveChart";
271
- var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
369
+ var BarChart = React.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
272
370
  const entries = React.useMemo(() => Object.entries(data), [data]);
273
371
  const maxVal = React.useMemo(() => {
274
372
  const allValues = entries.flatMap(([, v]) => v);
@@ -281,6 +379,7 @@ var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLea
281
379
  const groupW = chartW / count;
282
380
  const barGap = groupCount > 1 ? 2 : 0;
283
381
  const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
382
+ const baseline = PADDING.top + chartH;
284
383
  const bars = React.useMemo(
285
384
  () => entries.map(
286
385
  ([, values], di) => values.map((v, i) => {
@@ -306,12 +405,17 @@ var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLea
306
405
  return bars[di].map((b, i) => {
307
406
  const r = Math.min(4, b.w / 2);
308
407
  const d = b.h <= r ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r} Q ${b.x} ${b.y} ${b.x + r} ${b.y} H ${b.x + b.w - r} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r} V ${b.y + b.h} Z`;
408
+ const delay = 100 + i * 80;
309
409
  return /* @__PURE__ */ jsx(
310
410
  "path",
311
411
  {
312
412
  d,
313
413
  fill: color,
314
- className: "chart-bar",
414
+ className: `chart-bar ${animate ? "chart-bar-animate" : ""}`,
415
+ style: animate ? {
416
+ transformOrigin: `${b.x + b.w / 2}px ${baseline}px`,
417
+ animationDelay: `${delay}ms`
418
+ } : void 0,
315
419
  onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
316
420
  onMouseMove: onMove,
317
421
  onMouseLeave: onLeave
@@ -324,7 +428,7 @@ var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLea
324
428
  });
325
429
  BarChart.displayName = "BarChart";
326
430
  var PieDonutChart = React.memo(
327
- ({ data, labels, width, height, isDoughnut, onHover, onMove, onLeave }) => {
431
+ ({ data, labels, width, height, animate, isDoughnut, onHover, onMove, onLeave }) => {
328
432
  const entries = React.useMemo(() => Object.entries(data), [data]);
329
433
  const values = React.useMemo(() => entries.flatMap(([, v]) => v), [entries]);
330
434
  const total = React.useMemo(() => values.reduce((a, b) => a + b, 0) || 1, [values]);
@@ -364,20 +468,34 @@ var PieDonutChart = React.memo(
364
468
  return { d, lx, ly, v, pct, angle, label: labels[i] || `${i + 1}` };
365
469
  });
366
470
  }, [values, total, cx, cy, r, innerR, labels]);
367
- return /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${size} ${size}`, className: "chart-svg chart-pie", children: sliceData.map((s, i) => /* @__PURE__ */ jsxs("g", { children: [
368
- /* @__PURE__ */ jsx(
369
- "path",
370
- {
371
- d: s.d,
372
- fill: PIE_COLORS[(i + colorOffset) % PIE_COLORS.length],
373
- className: "chart-slice",
374
- onMouseEnter: (e) => onHover(e, `${s.label}: ${s.v} (${s.pct}%)`),
375
- onMouseMove: onMove,
376
- onMouseLeave: onLeave
377
- }
378
- ),
379
- s.angle > 0.2 && /* @__PURE__ */ jsx("text", { x: s.lx, y: s.ly, className: "chart-pie-label", textAnchor: "middle", dominantBaseline: "central", children: s.v })
380
- ] }, i)) });
471
+ return /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${size} ${size}`, className: "chart-svg chart-pie", children: sliceData.map((s, i) => {
472
+ const delay = i * 100;
473
+ return /* @__PURE__ */ jsxs("g", { className: animate ? "chart-slice-group-animate" : "", style: animate ? { animationDelay: `${delay}ms` } : void 0, children: [
474
+ /* @__PURE__ */ jsx(
475
+ "path",
476
+ {
477
+ d: s.d,
478
+ fill: PIE_COLORS[(i + colorOffset) % PIE_COLORS.length],
479
+ className: "chart-slice",
480
+ onMouseEnter: (e) => onHover(e, `${s.label}: ${s.v} (${s.pct}%)`),
481
+ onMouseMove: onMove,
482
+ onMouseLeave: onLeave
483
+ }
484
+ ),
485
+ s.angle > 0.2 && /* @__PURE__ */ jsx(
486
+ "text",
487
+ {
488
+ x: s.lx,
489
+ y: s.ly,
490
+ className: `chart-pie-label ${animate ? "chart-pie-label-animate" : ""}`,
491
+ style: animate ? { animationDelay: `${delay + 150}ms` } : void 0,
492
+ textAnchor: "middle",
493
+ dominantBaseline: "central",
494
+ children: s.v
495
+ }
496
+ )
497
+ ] }, i);
498
+ }) });
381
499
  }
382
500
  );
383
501
  PieDonutChart.displayName = "PieDonutChart";
@@ -411,13 +529,15 @@ var Chart = React.memo((props) => {
411
529
  const { width, height } = useChartSize(containerRef);
412
530
  const stableData = React.useMemo(() => data, [JSON.stringify(data)]);
413
531
  const stableLabels = React.useMemo(() => labels, [JSON.stringify(labels)]);
532
+ const dataKey = React.useMemo(() => JSON.stringify(labels), [labels]);
533
+ const animate = useChartAnimation(containerRef, dataKey);
414
534
  const ready = width > 0 && height > 0;
415
535
  return /* @__PURE__ */ jsxs("div", { className: "lib-xplat-chart", ref: containerRef, children: [
416
- ready && type === "line" && /* @__PURE__ */ jsx(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
417
- ready && type === "curve" && /* @__PURE__ */ jsx(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
418
- ready && type === "bar" && /* @__PURE__ */ jsx(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
419
- ready && type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
420
- ready && type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
536
+ ready && type === "line" && /* @__PURE__ */ jsx(LineChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
537
+ ready && type === "curve" && /* @__PURE__ */ jsx(CurveChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
538
+ ready && type === "bar" && /* @__PURE__ */ jsx(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
539
+ ready && type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
540
+ ready && type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
421
541
  tooltip.visible && /* @__PURE__ */ jsx(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
422
542
  ] });
423
543
  });