@x-plat/design-system 0.5.31 → 0.5.33

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.
@@ -2351,40 +2351,28 @@ var useChartAnimation = (containerRef, dataKey) => {
2351
2351
  }, [dataKey]);
2352
2352
  return animate || prefersReducedMotion();
2353
2353
  };
2354
+ var TOOLTIP_OFFSET = 12;
2354
2355
  var useChartTooltip = (enabled) => {
2355
2356
  const [tooltip, setTooltip] = import_react6.default.useState({
2356
2357
  visible: false,
2357
- x: 0,
2358
- y: 0,
2358
+ clientX: 0,
2359
+ clientY: 0,
2359
2360
  content: ""
2360
2361
  });
2361
2362
  const containerRef = import_react6.default.useRef(null);
2362
2363
  const rafRef = import_react6.default.useRef(0);
2363
2364
  const move = import_react6.default.useCallback((e) => {
2364
2365
  if (!enabled) return;
2365
- const clientX = e.clientX;
2366
- const clientY = e.clientY;
2366
+ const cx = e.clientX;
2367
+ const cy = e.clientY;
2367
2368
  cancelAnimationFrame(rafRef.current);
2368
2369
  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
- }));
2370
+ setTooltip((prev) => ({ ...prev, clientX: cx, clientY: cy }));
2376
2371
  });
2377
2372
  }, [enabled]);
2378
2373
  const show = import_react6.default.useCallback((e, content) => {
2379
2374
  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
- });
2375
+ setTooltip({ visible: true, clientX: e.clientX, clientY: e.clientY, content });
2388
2376
  }, [enabled]);
2389
2377
  const hide = import_react6.default.useCallback(() => {
2390
2378
  cancelAnimationFrame(rafRef.current);
@@ -2416,6 +2404,45 @@ var AxisLabels = import_react6.default.memo(({ labels, count, chartW, height })
2416
2404
  }) });
2417
2405
  });
2418
2406
  AxisLabels.displayName = "AxisLabels";
2407
+ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2408
+ const [activeIndex, setActiveIndex] = import_react6.default.useState(null);
2409
+ const handleMouseMove = import_react6.default.useCallback((e) => {
2410
+ const svg = e.currentTarget;
2411
+ const rect = svg.getBoundingClientRect();
2412
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2413
+ if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
2414
+ const points = seriesPoints[0];
2415
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2416
+ const threshold = step / 2;
2417
+ let closest = 0;
2418
+ let minDist = Math.abs(points[0].x - mx);
2419
+ for (let i = 1; i < points.length; i++) {
2420
+ const dist = Math.abs(points[i].x - mx);
2421
+ if (dist < minDist) {
2422
+ minDist = dist;
2423
+ closest = i;
2424
+ }
2425
+ }
2426
+ setActiveIndex(minDist <= threshold ? closest : null);
2427
+ }, [seriesPoints]);
2428
+ const handleMouseLeave = import_react6.default.useCallback(() => {
2429
+ setActiveIndex(null);
2430
+ }, []);
2431
+ const tooltipContent = import_react6.default.useMemo(() => {
2432
+ if (activeIndex === null) return "";
2433
+ return entries.map(([key], di) => {
2434
+ const p = seriesPoints[di]?.[activeIndex];
2435
+ return p ? `${key}: ${p.v}` : "";
2436
+ }).filter(Boolean).join(" / ");
2437
+ }, [activeIndex, entries, seriesPoints]);
2438
+ const getTooltipAt = import_react6.default.useCallback((idx) => {
2439
+ return entries.map(([key], di) => {
2440
+ const p = seriesPoints[di]?.[idx];
2441
+ return p ? `${key}: ${p.v}` : "";
2442
+ }).filter(Boolean).join(" / ");
2443
+ }, [entries, seriesPoints]);
2444
+ return { activeIndex, handleMouseMove, handleMouseLeave, tooltipContent, getTooltipAt };
2445
+ };
2419
2446
  var LineChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
2420
2447
  const entries = import_react6.default.useMemo(() => Object.entries(data), [data]);
2421
2448
  const maxVal = import_react6.default.useMemo(() => {
@@ -2435,8 +2462,9 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2435
2462
  ),
2436
2463
  [entries, count, chartW, chartH, maxVal]
2437
2464
  );
2438
- const showPoints = count <= 100;
2439
2465
  const lineRefs = import_react6.default.useRef([]);
2466
+ const clipRef = import_react6.default.useRef(null);
2467
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2440
2468
  import_react6.default.useEffect(() => {
2441
2469
  if (!animate) return;
2442
2470
  lineRefs.current.forEach((el) => {
@@ -2449,61 +2477,123 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2449
2477
  el.style.strokeDashoffset = "0";
2450
2478
  });
2451
2479
  });
2452
- }, [animate, seriesPoints]);
2453
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
2454
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
2455
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
2456
- entries.map(([key], di) => {
2457
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
2458
- const color = palette[2];
2459
- const areaColor = palette[0];
2460
- const points = seriesPoints[di];
2461
- const gradientId = `line-gradient-${di}`;
2462
- const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
2463
- 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`;
2464
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
2465
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2466
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
2467
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
2468
- ] }) }),
2469
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2470
- "path",
2480
+ if (clipRef.current) {
2481
+ clipRef.current.setAttribute("width", "0");
2482
+ requestAnimationFrame(() => {
2483
+ if (clipRef.current) {
2484
+ clipRef.current.style.transition = "width 1200ms ease-out 200ms";
2485
+ clipRef.current.setAttribute("width", `${width}`);
2486
+ }
2487
+ });
2488
+ }
2489
+ }, [animate, seriesPoints, width]);
2490
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2491
+ const lineClipId = "line-area-clip";
2492
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
2493
+ "svg",
2494
+ {
2495
+ viewBox: `0 0 ${width} ${height}`,
2496
+ className: "chart-svg",
2497
+ onMouseMove: (e) => {
2498
+ handleMouseMove(e);
2499
+ const svg = e.currentTarget;
2500
+ const rect = svg.getBoundingClientRect();
2501
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2502
+ const points = seriesPoints[0];
2503
+ if (!points || points.length === 0) return;
2504
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2505
+ let closest = 0;
2506
+ let minDist = Math.abs(points[0].x - mx);
2507
+ for (let i = 1; i < points.length; i++) {
2508
+ const dist = Math.abs(points[i].x - mx);
2509
+ if (dist < minDist) {
2510
+ minDist = dist;
2511
+ closest = i;
2512
+ }
2513
+ }
2514
+ if (minDist <= step / 2) {
2515
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
2516
+ } else {
2517
+ onLeave();
2518
+ }
2519
+ },
2520
+ onMouseLeave: () => {
2521
+ handleMouseLeave();
2522
+ onLeave();
2523
+ },
2524
+ children: [
2525
+ animate && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("clipPath", { id: lineClipId, children: /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("rect", { ref: clipRef, x: "0", y: "0", width: animate ? 0 : width, height }) }) }),
2526
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
2527
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
2528
+ entries.map(([key], di) => {
2529
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
2530
+ const color = palette[2];
2531
+ const areaColor = palette[0];
2532
+ const points = seriesPoints[di];
2533
+ const gradientId = `line-gradient-${di}`;
2534
+ const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
2535
+ 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`;
2536
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
2537
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2538
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
2539
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
2540
+ ] }) }),
2541
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2542
+ "path",
2543
+ {
2544
+ d: areaD,
2545
+ fill: `url(#${gradientId})`,
2546
+ clipPath: animate ? `url(#${lineClipId})` : void 0
2547
+ }
2548
+ ),
2549
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2550
+ "polyline",
2551
+ {
2552
+ ref: (el) => {
2553
+ lineRefs.current[di] = el;
2554
+ },
2555
+ points: polyPoints,
2556
+ fill: "none",
2557
+ stroke: color,
2558
+ strokeWidth: "2"
2559
+ }
2560
+ ),
2561
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2562
+ "circle",
2563
+ {
2564
+ cx: points[activeIndex].x,
2565
+ cy: points[activeIndex].y,
2566
+ r: "5",
2567
+ fill: color,
2568
+ className: "chart-point-active"
2569
+ }
2570
+ )
2571
+ ] }, di);
2572
+ }),
2573
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2574
+ "line",
2471
2575
  {
2472
- d: areaD,
2473
- fill: `url(#${gradientId})`,
2474
- className: "chart-area",
2475
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
2576
+ x1: activeX,
2577
+ y1: PADDING.top,
2578
+ x2: activeX,
2579
+ y2: PADDING.top + chartH,
2580
+ className: "chart-crosshair"
2476
2581
  }
2477
2582
  ),
2478
2583
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2479
- "polyline",
2584
+ "rect",
2480
2585
  {
2481
- ref: (el) => {
2482
- lineRefs.current[di] = el;
2483
- },
2484
- points: polyPoints,
2485
- fill: "none",
2486
- stroke: color,
2487
- strokeWidth: "2"
2586
+ x: PADDING.left,
2587
+ y: PADDING.top,
2588
+ width: chartW,
2589
+ height: chartH,
2590
+ fill: "transparent",
2591
+ style: { cursor: "crosshair" }
2488
2592
  }
2489
- ),
2490
- showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2491
- "circle",
2492
- {
2493
- cx: p.x,
2494
- cy: p.y,
2495
- r: "4",
2496
- fill: color,
2497
- className: "chart-point",
2498
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
2499
- onMouseMove: onMove,
2500
- onMouseLeave: onLeave
2501
- },
2502
- i
2503
- ))
2504
- ] }, di);
2505
- })
2506
- ] });
2593
+ )
2594
+ ]
2595
+ }
2596
+ );
2507
2597
  });
2508
2598
  LineChart.displayName = "LineChart";
2509
2599
  var CurveChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -2525,8 +2615,9 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2525
2615
  ),
2526
2616
  [entries, count, chartW, chartH, maxVal]
2527
2617
  );
2528
- const showPoints = count <= 100;
2529
2618
  const lineRefs = import_react6.default.useRef([]);
2619
+ const curveClipRef = import_react6.default.useRef(null);
2620
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2530
2621
  import_react6.default.useEffect(() => {
2531
2622
  if (!animate) return;
2532
2623
  lineRefs.current.forEach((el) => {
@@ -2539,61 +2630,123 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2539
2630
  el.style.strokeDashoffset = "0";
2540
2631
  });
2541
2632
  });
2542
- }, [animate, seriesPoints]);
2543
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
2544
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
2545
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
2546
- entries.map(([key], di) => {
2547
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
2548
- const color = palette[2];
2549
- const areaColor = palette[0];
2550
- const points = seriesPoints[di];
2551
- const gradientId = `curve-gradient-${di}`;
2552
- const linePath = toSmoothPath(points);
2553
- const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
2554
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
2555
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2556
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
2557
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
2558
- ] }) }),
2559
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2560
- "path",
2633
+ if (curveClipRef.current) {
2634
+ curveClipRef.current.setAttribute("width", "0");
2635
+ requestAnimationFrame(() => {
2636
+ if (curveClipRef.current) {
2637
+ curveClipRef.current.style.transition = "width 1200ms ease-out 200ms";
2638
+ curveClipRef.current.setAttribute("width", `${width}`);
2639
+ }
2640
+ });
2641
+ }
2642
+ }, [animate, seriesPoints, width]);
2643
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2644
+ const curveClipId = "curve-area-clip";
2645
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
2646
+ "svg",
2647
+ {
2648
+ viewBox: `0 0 ${width} ${height}`,
2649
+ className: "chart-svg",
2650
+ onMouseMove: (e) => {
2651
+ handleMouseMove(e);
2652
+ const svg = e.currentTarget;
2653
+ const rect = svg.getBoundingClientRect();
2654
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2655
+ const points = seriesPoints[0];
2656
+ if (!points || points.length === 0) return;
2657
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2658
+ let closest = 0;
2659
+ let minDist = Math.abs(points[0].x - mx);
2660
+ for (let i = 1; i < points.length; i++) {
2661
+ const dist = Math.abs(points[i].x - mx);
2662
+ if (dist < minDist) {
2663
+ minDist = dist;
2664
+ closest = i;
2665
+ }
2666
+ }
2667
+ if (minDist <= step / 2) {
2668
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
2669
+ } else {
2670
+ onLeave();
2671
+ }
2672
+ },
2673
+ onMouseLeave: () => {
2674
+ handleMouseLeave();
2675
+ onLeave();
2676
+ },
2677
+ children: [
2678
+ animate && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("clipPath", { id: curveClipId, children: /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("rect", { ref: curveClipRef, x: "0", y: "0", width: animate ? 0 : width, height }) }) }),
2679
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
2680
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
2681
+ entries.map(([key], di) => {
2682
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
2683
+ const color = palette[2];
2684
+ const areaColor = palette[0];
2685
+ const points = seriesPoints[di];
2686
+ const gradientId = `curve-gradient-${di}`;
2687
+ const linePath = toSmoothPath(points);
2688
+ const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
2689
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
2690
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
2691
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
2692
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
2693
+ ] }) }),
2694
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2695
+ "path",
2696
+ {
2697
+ d: areaPath,
2698
+ fill: `url(#${gradientId})`,
2699
+ clipPath: animate ? `url(#${curveClipId})` : void 0
2700
+ }
2701
+ ),
2702
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2703
+ "path",
2704
+ {
2705
+ ref: (el) => {
2706
+ lineRefs.current[di] = el;
2707
+ },
2708
+ d: linePath,
2709
+ fill: "none",
2710
+ stroke: color,
2711
+ strokeWidth: "2"
2712
+ }
2713
+ ),
2714
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2715
+ "circle",
2716
+ {
2717
+ cx: points[activeIndex].x,
2718
+ cy: points[activeIndex].y,
2719
+ r: "5",
2720
+ fill: color,
2721
+ className: "chart-point-active"
2722
+ }
2723
+ )
2724
+ ] }, di);
2725
+ }),
2726
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2727
+ "line",
2561
2728
  {
2562
- d: areaPath,
2563
- fill: `url(#${gradientId})`,
2564
- className: "chart-area",
2565
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
2729
+ x1: activeX,
2730
+ y1: PADDING.top,
2731
+ x2: activeX,
2732
+ y2: PADDING.top + chartH,
2733
+ className: "chart-crosshair"
2566
2734
  }
2567
2735
  ),
2568
2736
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2569
- "path",
2737
+ "rect",
2570
2738
  {
2571
- ref: (el) => {
2572
- lineRefs.current[di] = el;
2573
- },
2574
- d: linePath,
2575
- fill: "none",
2576
- stroke: color,
2577
- strokeWidth: "2"
2739
+ x: PADDING.left,
2740
+ y: PADDING.top,
2741
+ width: chartW,
2742
+ height: chartH,
2743
+ fill: "transparent",
2744
+ style: { cursor: "crosshair" }
2578
2745
  }
2579
- ),
2580
- showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2581
- "circle",
2582
- {
2583
- cx: p.x,
2584
- cy: p.y,
2585
- r: "4",
2586
- fill: color,
2587
- className: "chart-point",
2588
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
2589
- onMouseMove: onMove,
2590
- onMouseLeave: onLeave
2591
- },
2592
- i
2593
- ))
2594
- ] }, di);
2595
- })
2596
- ] });
2746
+ )
2747
+ ]
2748
+ }
2749
+ );
2597
2750
  });
2598
2751
  CurveChart.displayName = "CurveChart";
2599
2752
  var BarChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -2758,30 +2911,70 @@ var PieDonutChart = import_react6.default.memo(
2758
2911
  }
2759
2912
  );
2760
2913
  PieDonutChart.displayName = "PieDonutChart";
2761
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
2914
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
2762
2915
  const ref = import_react6.default.useRef(null);
2763
- const [adjustedX, setAdjustedX] = import_react6.default.useState(x);
2764
- import_react6.default.useEffect(() => {
2916
+ const [pos, setPos] = import_react6.default.useState({ left: 0, top: 0 });
2917
+ import_react6.default.useLayoutEffect(() => {
2765
2918
  const el = ref.current;
2766
2919
  if (!el) return;
2767
2920
  const w = el.offsetWidth;
2768
- const half = w / 2;
2769
- const margin = 8;
2770
- let nx = x;
2771
- if (x - half < margin) nx = half + margin;
2772
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
2773
- setAdjustedX(nx);
2774
- }, [x, containerWidth]);
2921
+ const h = el.offsetHeight;
2922
+ const vw = window.innerWidth;
2923
+ let left = clientX + TOOLTIP_OFFSET;
2924
+ let top = clientY - h - TOOLTIP_OFFSET;
2925
+ if (left + w > vw - 8) left = clientX - w - TOOLTIP_OFFSET;
2926
+ if (top < 8) top = clientY + TOOLTIP_OFFSET;
2927
+ if (left < 8) left = 8;
2928
+ setPos({ left, top });
2929
+ }, [clientX, clientY]);
2775
2930
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2776
2931
  "div",
2777
2932
  {
2778
2933
  ref,
2779
- className: "chart-tooltip",
2780
- style: { left: adjustedX, top: y },
2934
+ className: `chart-tooltip ${visible ? "chart-tooltip-show" : "chart-tooltip-hide"}`,
2935
+ style: { position: "fixed", left: pos.left, top: pos.top, zIndex: 1100 },
2781
2936
  children
2782
2937
  }
2783
2938
  );
2784
2939
  };
2940
+ var ChartLegend = ({ data, labels, type }) => {
2941
+ const entries = Object.entries(data);
2942
+ if (type === "pie" || type === "doughnut") {
2943
+ const values = entries.flatMap(([, v]) => v);
2944
+ const total = values.reduce((a, b) => a + b, 0) || 1;
2945
+ const firstKey = entries[0]?.[0] ?? "";
2946
+ const colorOffset = hashString(firstKey);
2947
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("div", { className: "chart-legend", children: values.map((v, i) => {
2948
+ const pct = Math.round(v / total * 100);
2949
+ const color = PIE_COLORS[(i + colorOffset) % PIE_COLORS.length];
2950
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-item", children: [
2951
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
2952
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-text", children: [
2953
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-label", children: labels[i] || `${i + 1}` }),
2954
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("span", { className: "chart-legend-value", children: [
2955
+ v.toLocaleString(),
2956
+ "(",
2957
+ pct,
2958
+ "%)"
2959
+ ] })
2960
+ ] })
2961
+ ] }, i);
2962
+ }) });
2963
+ }
2964
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("div", { className: "chart-legend", children: entries.map(([key], di) => {
2965
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
2966
+ const color = palette[2];
2967
+ const values = entries[di][1];
2968
+ const sum = values.reduce((a, b) => a + b, 0);
2969
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-item", children: [
2970
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
2971
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-text", children: [
2972
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-label", children: key }),
2973
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-value", children: sum.toLocaleString() })
2974
+ ] })
2975
+ ] }, di);
2976
+ }) });
2977
+ };
2785
2978
  var Chart = import_react6.default.memo((props) => {
2786
2979
  const { type, data, labels, tooltip: showTooltip = true } = props;
2787
2980
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -2797,7 +2990,8 @@ var Chart = import_react6.default.memo((props) => {
2797
2990
  ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2798
2991
  ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2799
2992
  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 }),
2800
- tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
2993
+ ready && (type === "bar" || type === "pie" || type === "doughnut") && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(ChartLegend, { data: stableData, labels: stableLabels, type }),
2994
+ tooltip.content && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(ChartTooltipPortal, { clientX: tooltip.clientX, clientY: tooltip.clientY, visible: tooltip.visible, children: tooltip.content })
2801
2995
  ] });
2802
2996
  });
2803
2997
  Chart.displayName = "Chart";