@x-plat/design-system 0.5.32 → 0.5.34

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.
@@ -2346,45 +2346,35 @@ var useChartAnimation = (containerRef, dataKey) => {
2346
2346
  prevDataKey.current = dataKey;
2347
2347
  if (prefersReducedMotion()) return;
2348
2348
  setAnimate(false);
2349
- requestAnimationFrame(() => setAnimate(true));
2349
+ requestAnimationFrame(() => {
2350
+ requestAnimationFrame(() => setAnimate(true));
2351
+ });
2350
2352
  }
2351
2353
  }, [dataKey]);
2352
2354
  return animate || prefersReducedMotion();
2353
2355
  };
2356
+ var TOOLTIP_OFFSET = 12;
2354
2357
  var useChartTooltip = (enabled) => {
2355
2358
  const [tooltip, setTooltip] = import_react6.default.useState({
2356
2359
  visible: false,
2357
- x: 0,
2358
- y: 0,
2360
+ clientX: 0,
2361
+ clientY: 0,
2359
2362
  content: ""
2360
2363
  });
2361
2364
  const containerRef = import_react6.default.useRef(null);
2362
2365
  const rafRef = import_react6.default.useRef(0);
2363
2366
  const move = import_react6.default.useCallback((e) => {
2364
2367
  if (!enabled) return;
2365
- const clientX = e.clientX;
2366
- const clientY = e.clientY;
2368
+ const cx = e.clientX;
2369
+ const cy = e.clientY;
2367
2370
  cancelAnimationFrame(rafRef.current);
2368
2371
  rafRef.current = requestAnimationFrame(() => {
2369
- const rect = containerRef.current?.getBoundingClientRect();
2370
- if (!rect) return;
2371
- setTooltip((prev) => ({
2372
- ...prev,
2373
- x: clientX - rect.left,
2374
- y: clientY - rect.top - 12
2375
- }));
2372
+ setTooltip((prev) => ({ ...prev, clientX: cx, clientY: cy }));
2376
2373
  });
2377
2374
  }, [enabled]);
2378
2375
  const show = import_react6.default.useCallback((e, content) => {
2379
2376
  if (!enabled) return;
2380
- const rect = containerRef.current?.getBoundingClientRect();
2381
- if (!rect) return;
2382
- setTooltip({
2383
- visible: true,
2384
- x: e.clientX - rect.left,
2385
- y: e.clientY - rect.top - 12,
2386
- content
2387
- });
2377
+ setTooltip({ visible: true, clientX: e.clientX, clientY: e.clientY, content });
2388
2378
  }, [enabled]);
2389
2379
  const hide = import_react6.default.useCallback(() => {
2390
2380
  cancelAnimationFrame(rafRef.current);
@@ -2418,14 +2408,14 @@ var AxisLabels = import_react6.default.memo(({ labels, count, chartW, height })
2418
2408
  AxisLabels.displayName = "AxisLabels";
2419
2409
  var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2420
2410
  const [activeIndex, setActiveIndex] = import_react6.default.useState(null);
2421
- const [mouseX, setMouseX] = import_react6.default.useState(null);
2422
2411
  const handleMouseMove = import_react6.default.useCallback((e) => {
2423
2412
  const svg = e.currentTarget;
2424
2413
  const rect = svg.getBoundingClientRect();
2425
2414
  const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2426
- setMouseX(mx);
2427
2415
  if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
2428
2416
  const points = seriesPoints[0];
2417
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2418
+ const threshold = step / 2;
2429
2419
  let closest = 0;
2430
2420
  let minDist = Math.abs(points[0].x - mx);
2431
2421
  for (let i = 1; i < points.length; i++) {
@@ -2435,11 +2425,10 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2435
2425
  closest = i;
2436
2426
  }
2437
2427
  }
2438
- setActiveIndex(closest);
2428
+ setActiveIndex(minDist <= threshold ? closest : null);
2439
2429
  }, [seriesPoints]);
2440
2430
  const handleMouseLeave = import_react6.default.useCallback(() => {
2441
2431
  setActiveIndex(null);
2442
- setMouseX(null);
2443
2432
  }, []);
2444
2433
  const tooltipContent = import_react6.default.useMemo(() => {
2445
2434
  if (activeIndex === null) return "";
@@ -2448,7 +2437,13 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2448
2437
  return p ? `${key}: ${p.v}` : "";
2449
2438
  }).filter(Boolean).join(" / ");
2450
2439
  }, [activeIndex, entries, seriesPoints]);
2451
- return { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent };
2440
+ const getTooltipAt = import_react6.default.useCallback((idx) => {
2441
+ return entries.map(([key], di) => {
2442
+ const p = seriesPoints[di]?.[idx];
2443
+ return p ? `${key}: ${p.v}` : "";
2444
+ }).filter(Boolean).join(" / ");
2445
+ }, [entries, seriesPoints]);
2446
+ return { activeIndex, handleMouseMove, handleMouseLeave, tooltipContent, getTooltipAt };
2452
2447
  };
2453
2448
  var LineChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
2454
2449
  const entries = import_react6.default.useMemo(() => Object.entries(data), [data]);
@@ -2469,33 +2464,19 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2469
2464
  ),
2470
2465
  [entries, count, chartW, chartH, maxVal]
2471
2466
  );
2472
- const lineRefs = import_react6.default.useRef([]);
2473
2467
  const clipRef = import_react6.default.useRef(null);
2474
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
2468
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2475
2469
  import_react6.default.useEffect(() => {
2476
- if (!animate) return;
2477
- lineRefs.current.forEach((el) => {
2478
- if (!el) return;
2479
- const len = el.getTotalLength();
2480
- el.style.strokeDasharray = `${len}`;
2481
- el.style.strokeDashoffset = `${len}`;
2482
- requestAnimationFrame(() => {
2483
- el.style.transition = "stroke-dashoffset 1200ms ease-out 200ms";
2484
- el.style.strokeDashoffset = "0";
2485
- });
2470
+ if (!animate || !clipRef.current) return;
2471
+ clipRef.current.setAttribute("width", "0");
2472
+ requestAnimationFrame(() => {
2473
+ if (clipRef.current) {
2474
+ clipRef.current.style.transition = "width 1200ms ease-out 200ms";
2475
+ clipRef.current.setAttribute("width", `${width}`);
2476
+ }
2486
2477
  });
2487
- if (clipRef.current) {
2488
- clipRef.current.setAttribute("width", "0");
2489
- requestAnimationFrame(() => {
2490
- if (clipRef.current) {
2491
- clipRef.current.style.transition = "width 1200ms ease-out 200ms";
2492
- clipRef.current.setAttribute("width", `${width}`);
2493
- }
2494
- });
2495
- }
2496
- }, [animate, seriesPoints, width]);
2497
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
2498
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
2478
+ }, [animate, width]);
2479
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2499
2480
  const lineClipId = "line-area-clip";
2500
2481
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
2501
2482
  "svg",
@@ -2504,7 +2485,26 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2504
2485
  className: "chart-svg",
2505
2486
  onMouseMove: (e) => {
2506
2487
  handleMouseMove(e);
2507
- onMove(e);
2488
+ const svg = e.currentTarget;
2489
+ const rect = svg.getBoundingClientRect();
2490
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2491
+ const points = seriesPoints[0];
2492
+ if (!points || points.length === 0) return;
2493
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2494
+ let closest = 0;
2495
+ let minDist = Math.abs(points[0].x - mx);
2496
+ for (let i = 1; i < points.length; i++) {
2497
+ const dist = Math.abs(points[i].x - mx);
2498
+ if (dist < minDist) {
2499
+ minDist = dist;
2500
+ closest = i;
2501
+ }
2502
+ }
2503
+ if (minDist <= step / 2) {
2504
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
2505
+ } else {
2506
+ onLeave();
2507
+ }
2508
2508
  },
2509
2509
  onMouseLeave: () => {
2510
2510
  handleMouseLeave();
@@ -2527,26 +2527,10 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2527
2527
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
2528
2528
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
2529
2529
  ] }) }),
2530
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2531
- "path",
2532
- {
2533
- d: areaD,
2534
- fill: `url(#${gradientId})`,
2535
- clipPath: animate ? `url(#${lineClipId})` : void 0
2536
- }
2537
- ),
2538
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2539
- "polyline",
2540
- {
2541
- ref: (el) => {
2542
- lineRefs.current[di] = el;
2543
- },
2544
- points: polyPoints,
2545
- fill: "none",
2546
- stroke: color,
2547
- strokeWidth: "2"
2548
- }
2549
- ),
2530
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { clipPath: animate ? `url(#${lineClipId})` : void 0, children: [
2531
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("path", { d: areaD, fill: `url(#${gradientId})` }),
2532
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("polyline", { points: polyPoints, fill: "none", stroke: color, strokeWidth: "2" })
2533
+ ] }),
2550
2534
  activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2551
2535
  "circle",
2552
2536
  {
@@ -2559,21 +2543,16 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2559
2543
  )
2560
2544
  ] }, di);
2561
2545
  }),
2562
- guideX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2546
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2563
2547
  "line",
2564
2548
  {
2565
- x1: guideX,
2549
+ x1: activeX,
2566
2550
  y1: PADDING.top,
2567
- x2: guideX,
2551
+ x2: activeX,
2568
2552
  y2: PADDING.top + chartH,
2569
2553
  className: "chart-crosshair"
2570
2554
  }
2571
2555
  ),
2572
- activeIndex !== null && activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-crosshair-label", children: [
2573
- labels[activeIndex],
2574
- " \u2014 ",
2575
- tooltipContent
2576
- ] }) }),
2577
2556
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2578
2557
  "rect",
2579
2558
  {
@@ -2609,33 +2588,19 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2609
2588
  ),
2610
2589
  [entries, count, chartW, chartH, maxVal]
2611
2590
  );
2612
- const lineRefs = import_react6.default.useRef([]);
2613
2591
  const curveClipRef = import_react6.default.useRef(null);
2614
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
2592
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2615
2593
  import_react6.default.useEffect(() => {
2616
- if (!animate) return;
2617
- lineRefs.current.forEach((el) => {
2618
- if (!el) return;
2619
- const len = el.getTotalLength();
2620
- el.style.strokeDasharray = `${len}`;
2621
- el.style.strokeDashoffset = `${len}`;
2622
- requestAnimationFrame(() => {
2623
- el.style.transition = "stroke-dashoffset 1200ms ease-out 200ms";
2624
- el.style.strokeDashoffset = "0";
2625
- });
2594
+ if (!animate || !curveClipRef.current) return;
2595
+ curveClipRef.current.setAttribute("width", "0");
2596
+ requestAnimationFrame(() => {
2597
+ if (curveClipRef.current) {
2598
+ curveClipRef.current.style.transition = "width 1200ms ease-out 200ms";
2599
+ curveClipRef.current.setAttribute("width", `${width}`);
2600
+ }
2626
2601
  });
2627
- if (curveClipRef.current) {
2628
- curveClipRef.current.setAttribute("width", "0");
2629
- requestAnimationFrame(() => {
2630
- if (curveClipRef.current) {
2631
- curveClipRef.current.style.transition = "width 1200ms ease-out 200ms";
2632
- curveClipRef.current.setAttribute("width", `${width}`);
2633
- }
2634
- });
2635
- }
2636
- }, [animate, seriesPoints, width]);
2637
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
2638
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
2602
+ }, [animate, width]);
2603
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2639
2604
  const curveClipId = "curve-area-clip";
2640
2605
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
2641
2606
  "svg",
@@ -2644,7 +2609,26 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2644
2609
  className: "chart-svg",
2645
2610
  onMouseMove: (e) => {
2646
2611
  handleMouseMove(e);
2647
- onMove(e);
2612
+ const svg = e.currentTarget;
2613
+ const rect = svg.getBoundingClientRect();
2614
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2615
+ const points = seriesPoints[0];
2616
+ if (!points || points.length === 0) return;
2617
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2618
+ let closest = 0;
2619
+ let minDist = Math.abs(points[0].x - mx);
2620
+ for (let i = 1; i < points.length; i++) {
2621
+ const dist = Math.abs(points[i].x - mx);
2622
+ if (dist < minDist) {
2623
+ minDist = dist;
2624
+ closest = i;
2625
+ }
2626
+ }
2627
+ if (minDist <= step / 2) {
2628
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
2629
+ } else {
2630
+ onLeave();
2631
+ }
2648
2632
  },
2649
2633
  onMouseLeave: () => {
2650
2634
  handleMouseLeave();
@@ -2667,26 +2651,10 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2667
2651
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
2668
2652
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
2669
2653
  ] }) }),
2670
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2671
- "path",
2672
- {
2673
- d: areaPath,
2674
- fill: `url(#${gradientId})`,
2675
- clipPath: animate ? `url(#${curveClipId})` : void 0
2676
- }
2677
- ),
2678
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2679
- "path",
2680
- {
2681
- ref: (el) => {
2682
- lineRefs.current[di] = el;
2683
- },
2684
- d: linePath,
2685
- fill: "none",
2686
- stroke: color,
2687
- strokeWidth: "2"
2688
- }
2689
- ),
2654
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { clipPath: animate ? `url(#${curveClipId})` : void 0, children: [
2655
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("path", { d: areaPath, fill: `url(#${gradientId})` }),
2656
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("path", { d: linePath, fill: "none", stroke: color, strokeWidth: "2" })
2657
+ ] }),
2690
2658
  activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2691
2659
  "circle",
2692
2660
  {
@@ -2699,21 +2667,16 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2699
2667
  )
2700
2668
  ] }, di);
2701
2669
  }),
2702
- guideX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2670
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2703
2671
  "line",
2704
2672
  {
2705
- x1: guideX,
2673
+ x1: activeX,
2706
2674
  y1: PADDING.top,
2707
- x2: guideX,
2675
+ x2: activeX,
2708
2676
  y2: PADDING.top + chartH,
2709
2677
  className: "chart-crosshair"
2710
2678
  }
2711
2679
  ),
2712
- activeIndex !== null && activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-crosshair-label", children: [
2713
- labels[activeIndex],
2714
- " \u2014 ",
2715
- tooltipContent
2716
- ] }) }),
2717
2680
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2718
2681
  "rect",
2719
2682
  {
@@ -2892,30 +2855,70 @@ var PieDonutChart = import_react6.default.memo(
2892
2855
  }
2893
2856
  );
2894
2857
  PieDonutChart.displayName = "PieDonutChart";
2895
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
2858
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
2896
2859
  const ref = import_react6.default.useRef(null);
2897
- const [adjustedX, setAdjustedX] = import_react6.default.useState(x);
2898
- import_react6.default.useEffect(() => {
2860
+ const [pos, setPos] = import_react6.default.useState({ left: 0, top: 0 });
2861
+ import_react6.default.useLayoutEffect(() => {
2899
2862
  const el = ref.current;
2900
2863
  if (!el) return;
2901
2864
  const w = el.offsetWidth;
2902
- const half = w / 2;
2903
- const margin = 8;
2904
- let nx = x;
2905
- if (x - half < margin) nx = half + margin;
2906
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
2907
- setAdjustedX(nx);
2908
- }, [x, containerWidth]);
2865
+ const h = el.offsetHeight;
2866
+ const vw = window.innerWidth;
2867
+ let left = clientX + TOOLTIP_OFFSET;
2868
+ let top = clientY - h - TOOLTIP_OFFSET;
2869
+ if (left + w > vw - 8) left = clientX - w - TOOLTIP_OFFSET;
2870
+ if (top < 8) top = clientY + TOOLTIP_OFFSET;
2871
+ if (left < 8) left = 8;
2872
+ setPos({ left, top });
2873
+ }, [clientX, clientY]);
2909
2874
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2910
2875
  "div",
2911
2876
  {
2912
2877
  ref,
2913
- className: "chart-tooltip",
2914
- style: { left: adjustedX, top: y },
2878
+ className: `chart-tooltip ${visible ? "chart-tooltip-show" : "chart-tooltip-hide"}`,
2879
+ style: { position: "fixed", left: pos.left, top: pos.top, zIndex: 1100 },
2915
2880
  children
2916
2881
  }
2917
2882
  );
2918
2883
  };
2884
+ var ChartLegend = ({ data, labels, type }) => {
2885
+ const entries = Object.entries(data);
2886
+ if (type === "pie" || type === "doughnut") {
2887
+ const values = entries.flatMap(([, v]) => v);
2888
+ const total = values.reduce((a, b) => a + b, 0) || 1;
2889
+ const firstKey = entries[0]?.[0] ?? "";
2890
+ const colorOffset = hashString(firstKey);
2891
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("div", { className: "chart-legend", children: values.map((v, i) => {
2892
+ const pct = Math.round(v / total * 100);
2893
+ const color = PIE_COLORS[(i + colorOffset) % PIE_COLORS.length];
2894
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-item", children: [
2895
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
2896
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-text", children: [
2897
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-label", children: labels[i] || `${i + 1}` }),
2898
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("span", { className: "chart-legend-value", children: [
2899
+ v.toLocaleString(),
2900
+ "(",
2901
+ pct,
2902
+ "%)"
2903
+ ] })
2904
+ ] })
2905
+ ] }, i);
2906
+ }) });
2907
+ }
2908
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("div", { className: "chart-legend", children: entries.map(([key], di) => {
2909
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
2910
+ const color = palette[2];
2911
+ const values = entries[di][1];
2912
+ const sum = values.reduce((a, b) => a + b, 0);
2913
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-item", children: [
2914
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
2915
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-text", children: [
2916
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-label", children: key }),
2917
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-value", children: sum.toLocaleString() })
2918
+ ] })
2919
+ ] }, di);
2920
+ }) });
2921
+ };
2919
2922
  var Chart = import_react6.default.memo((props) => {
2920
2923
  const { type, data, labels, tooltip: showTooltip = true } = props;
2921
2924
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -2931,7 +2934,8 @@ var Chart = import_react6.default.memo((props) => {
2931
2934
  ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2932
2935
  ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2933
2936
  ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
2934
- tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
2937
+ ready && (type === "bar" || type === "pie" || type === "doughnut") && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(ChartLegend, { data: stableData, labels: stableLabels, type }),
2938
+ tooltip.content && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(ChartTooltipPortal, { clientX: tooltip.clientX, clientY: tooltip.clientY, visible: tooltip.visible, children: tooltip.content })
2935
2939
  ] });
2936
2940
  });
2937
2941
  Chart.displayName = "Chart";
@@ -2000,9 +2000,9 @@
2000
2000
  opacity: 1;
2001
2001
  }
2002
2002
  .lib-xplat-chart .chart-crosshair {
2003
- stroke: var(--semantic-border-subtle);
2003
+ stroke: var(--semantic-border-strong);
2004
2004
  stroke-width: 1;
2005
- stroke-dasharray: 4 2;
2005
+ stroke-dasharray: 4 3;
2006
2006
  pointer-events: none;
2007
2007
  }
2008
2008
  .lib-xplat-chart .chart-point-active {
@@ -2018,18 +2018,24 @@
2018
2018
  overflow: visible;
2019
2019
  }
2020
2020
  .lib-xplat-chart .chart-tooltip {
2021
- position: absolute;
2022
- transform: translate(-50%, -100%);
2023
- padding: var(--spacing-space-2) var(--spacing-space-3);
2021
+ padding: var(--spacing-space-3);
2024
2022
  background-color: var(--semantic-surface-neutral-strong);
2025
2023
  color: var(--semantic-text-inverse);
2026
2024
  font-size: 12px;
2025
+ line-height: 18px;
2027
2026
  font-weight: 500;
2028
2027
  border-radius: var(--spacing-radius-md);
2029
- white-space: nowrap;
2028
+ max-width: 240px;
2030
2029
  pointer-events: none;
2031
- z-index: 10;
2032
- animation: chart-tooltip-in 150ms ease-out;
2030
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
2031
+ }
2032
+ .lib-xplat-chart .chart-tooltip.chart-tooltip-show {
2033
+ opacity: 1;
2034
+ animation: chart-tooltip-in 120ms ease-out;
2035
+ }
2036
+ .lib-xplat-chart .chart-tooltip.chart-tooltip-hide {
2037
+ opacity: 0;
2038
+ animation: chart-tooltip-out 80ms ease-in;
2033
2039
  }
2034
2040
  .lib-xplat-chart .chart-bar-animate {
2035
2041
  animation: chart-bar-grow 800ms ease-out both;
@@ -2041,6 +2047,38 @@
2041
2047
  .lib-xplat-chart .chart-area[style*=animationDelay] {
2042
2048
  animation: chart-fade-in 800ms ease-out both;
2043
2049
  }
2050
+ .lib-xplat-chart .chart-legend {
2051
+ display: flex;
2052
+ flex-wrap: wrap;
2053
+ justify-content: center;
2054
+ gap: var(--spacing-space-4);
2055
+ padding: var(--spacing-space-3) 0;
2056
+ }
2057
+ .lib-xplat-chart .chart-legend-item {
2058
+ display: flex;
2059
+ align-items: flex-start;
2060
+ gap: var(--spacing-space-2);
2061
+ }
2062
+ .lib-xplat-chart .chart-legend-dot {
2063
+ flex-shrink: 0;
2064
+ width: 10px;
2065
+ height: 10px;
2066
+ border-radius: 50%;
2067
+ margin-top: 3px;
2068
+ }
2069
+ .lib-xplat-chart .chart-legend-text {
2070
+ display: flex;
2071
+ flex-direction: column;
2072
+ }
2073
+ .lib-xplat-chart .chart-legend-label {
2074
+ font-size: 12px;
2075
+ color: var(--semantic-text-muted);
2076
+ }
2077
+ .lib-xplat-chart .chart-legend-value {
2078
+ font-size: 14px;
2079
+ font-weight: 600;
2080
+ color: var(--semantic-text-strong);
2081
+ }
2044
2082
  @keyframes chart-bar-grow {
2045
2083
  from {
2046
2084
  transform: scaleY(0);
@@ -2060,11 +2098,17 @@
2060
2098
  @keyframes chart-tooltip-in {
2061
2099
  from {
2062
2100
  opacity: 0;
2063
- transform: translate(-50%, -90%);
2064
2101
  }
2065
2102
  to {
2066
2103
  opacity: 1;
2067
- transform: translate(-50%, -100%);
2104
+ }
2105
+ }
2106
+ @keyframes chart-tooltip-out {
2107
+ from {
2108
+ opacity: 1;
2109
+ }
2110
+ to {
2111
+ opacity: 0;
2068
2112
  }
2069
2113
  }
2070
2114
  @media (prefers-reduced-motion: reduce) {