@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.
package/dist/index.css CHANGED
@@ -1999,19 +1999,43 @@
1999
1999
  .lib-xplat-chart .chart-area {
2000
2000
  opacity: 1;
2001
2001
  }
2002
+ .lib-xplat-chart .chart-crosshair {
2003
+ stroke: var(--semantic-border-strong);
2004
+ stroke-width: 1;
2005
+ stroke-dasharray: 4 3;
2006
+ pointer-events: none;
2007
+ }
2008
+ .lib-xplat-chart .chart-point-active {
2009
+ pointer-events: none;
2010
+ transition: cx 0.05s, cy 0.05s;
2011
+ }
2012
+ .lib-xplat-chart .chart-crosshair-label {
2013
+ font-size: 11px;
2014
+ font-weight: 500;
2015
+ color: var(--semantic-text-strong);
2016
+ text-align: center;
2017
+ white-space: nowrap;
2018
+ overflow: visible;
2019
+ }
2002
2020
  .lib-xplat-chart .chart-tooltip {
2003
- position: absolute;
2004
- transform: translate(-50%, -100%);
2005
- padding: var(--spacing-space-2) var(--spacing-space-3);
2021
+ padding: var(--spacing-space-3);
2006
2022
  background-color: var(--semantic-surface-neutral-strong);
2007
2023
  color: var(--semantic-text-inverse);
2008
2024
  font-size: 12px;
2025
+ line-height: 18px;
2009
2026
  font-weight: 500;
2010
2027
  border-radius: var(--spacing-radius-md);
2011
- white-space: nowrap;
2028
+ max-width: 240px;
2012
2029
  pointer-events: none;
2013
- z-index: 10;
2014
- 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;
2015
2039
  }
2016
2040
  .lib-xplat-chart .chart-bar-animate {
2017
2041
  animation: chart-bar-grow 800ms ease-out both;
@@ -2023,6 +2047,38 @@
2023
2047
  .lib-xplat-chart .chart-area[style*=animationDelay] {
2024
2048
  animation: chart-fade-in 800ms ease-out both;
2025
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
+ }
2026
2082
  @keyframes chart-bar-grow {
2027
2083
  from {
2028
2084
  transform: scaleY(0);
@@ -2042,11 +2098,17 @@
2042
2098
  @keyframes chart-tooltip-in {
2043
2099
  from {
2044
2100
  opacity: 0;
2045
- transform: translate(-50%, -90%);
2046
2101
  }
2047
2102
  to {
2048
2103
  opacity: 1;
2049
- transform: translate(-50%, -100%);
2104
+ }
2105
+ }
2106
+ @keyframes chart-tooltip-out {
2107
+ from {
2108
+ opacity: 1;
2109
+ }
2110
+ to {
2111
+ opacity: 0;
2050
2112
  }
2051
2113
  }
2052
2114
  @media (prefers-reduced-motion: reduce) {
@@ -3844,6 +3906,10 @@
3844
3906
  height: 100%;
3845
3907
  position: relative;
3846
3908
  overflow: auto;
3909
+ scrollbar-width: none;
3910
+ }
3911
+ .lib-xplat-table-wrapper::-webkit-scrollbar {
3912
+ display: none;
3847
3913
  }
3848
3914
  .lib-xplat-table-wrapper.sm > .lib-xplat-table > thead > tr > th,
3849
3915
  .lib-xplat-table-wrapper.sm > .lib-xplat-table > thead > tr > td,
package/dist/index.js CHANGED
@@ -6351,40 +6351,28 @@ var useChartAnimation = (containerRef, dataKey) => {
6351
6351
  }, [dataKey]);
6352
6352
  return animate || prefersReducedMotion();
6353
6353
  };
6354
+ var TOOLTIP_OFFSET = 12;
6354
6355
  var useChartTooltip = (enabled) => {
6355
6356
  const [tooltip, setTooltip] = React6.useState({
6356
6357
  visible: false,
6357
- x: 0,
6358
- y: 0,
6358
+ clientX: 0,
6359
+ clientY: 0,
6359
6360
  content: ""
6360
6361
  });
6361
6362
  const containerRef = React6.useRef(null);
6362
6363
  const rafRef = React6.useRef(0);
6363
6364
  const move = React6.useCallback((e) => {
6364
6365
  if (!enabled) return;
6365
- const clientX = e.clientX;
6366
- const clientY = e.clientY;
6366
+ const cx = e.clientX;
6367
+ const cy = e.clientY;
6367
6368
  cancelAnimationFrame(rafRef.current);
6368
6369
  rafRef.current = requestAnimationFrame(() => {
6369
- const rect = containerRef.current?.getBoundingClientRect();
6370
- if (!rect) return;
6371
- setTooltip((prev) => ({
6372
- ...prev,
6373
- x: clientX - rect.left,
6374
- y: clientY - rect.top - 12
6375
- }));
6370
+ setTooltip((prev) => ({ ...prev, clientX: cx, clientY: cy }));
6376
6371
  });
6377
6372
  }, [enabled]);
6378
6373
  const show = React6.useCallback((e, content) => {
6379
6374
  if (!enabled) return;
6380
- const rect = containerRef.current?.getBoundingClientRect();
6381
- if (!rect) return;
6382
- setTooltip({
6383
- visible: true,
6384
- x: e.clientX - rect.left,
6385
- y: e.clientY - rect.top - 12,
6386
- content
6387
- });
6375
+ setTooltip({ visible: true, clientX: e.clientX, clientY: e.clientY, content });
6388
6376
  }, [enabled]);
6389
6377
  const hide = React6.useCallback(() => {
6390
6378
  cancelAnimationFrame(rafRef.current);
@@ -6416,6 +6404,45 @@ var AxisLabels = React6.memo(({ labels, count, chartW, height }) => {
6416
6404
  }) });
6417
6405
  });
6418
6406
  AxisLabels.displayName = "AxisLabels";
6407
+ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
6408
+ const [activeIndex, setActiveIndex] = React6.useState(null);
6409
+ const handleMouseMove = React6.useCallback((e) => {
6410
+ const svg = e.currentTarget;
6411
+ const rect = svg.getBoundingClientRect();
6412
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
6413
+ if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
6414
+ const points = seriesPoints[0];
6415
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
6416
+ const threshold = step / 2;
6417
+ let closest = 0;
6418
+ let minDist = Math.abs(points[0].x - mx);
6419
+ for (let i = 1; i < points.length; i++) {
6420
+ const dist = Math.abs(points[i].x - mx);
6421
+ if (dist < minDist) {
6422
+ minDist = dist;
6423
+ closest = i;
6424
+ }
6425
+ }
6426
+ setActiveIndex(minDist <= threshold ? closest : null);
6427
+ }, [seriesPoints]);
6428
+ const handleMouseLeave = React6.useCallback(() => {
6429
+ setActiveIndex(null);
6430
+ }, []);
6431
+ const tooltipContent = React6.useMemo(() => {
6432
+ if (activeIndex === null) return "";
6433
+ return entries.map(([key], di) => {
6434
+ const p = seriesPoints[di]?.[activeIndex];
6435
+ return p ? `${key}: ${p.v}` : "";
6436
+ }).filter(Boolean).join(" / ");
6437
+ }, [activeIndex, entries, seriesPoints]);
6438
+ const getTooltipAt = React6.useCallback((idx) => {
6439
+ return entries.map(([key], di) => {
6440
+ const p = seriesPoints[di]?.[idx];
6441
+ return p ? `${key}: ${p.v}` : "";
6442
+ }).filter(Boolean).join(" / ");
6443
+ }, [entries, seriesPoints]);
6444
+ return { activeIndex, handleMouseMove, handleMouseLeave, tooltipContent, getTooltipAt };
6445
+ };
6419
6446
  var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
6420
6447
  const entries = React6.useMemo(() => Object.entries(data), [data]);
6421
6448
  const maxVal = React6.useMemo(() => {
@@ -6435,8 +6462,9 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
6435
6462
  ),
6436
6463
  [entries, count, chartW, chartH, maxVal]
6437
6464
  );
6438
- const showPoints = count <= 100;
6439
6465
  const lineRefs = React6.useRef([]);
6466
+ const clipRef = React6.useRef(null);
6467
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
6440
6468
  React6.useEffect(() => {
6441
6469
  if (!animate) return;
6442
6470
  lineRefs.current.forEach((el) => {
@@ -6449,61 +6477,123 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
6449
6477
  el.style.strokeDashoffset = "0";
6450
6478
  });
6451
6479
  });
6452
- }, [animate, seriesPoints]);
6453
- return /* @__PURE__ */ jsxs197("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
6454
- /* @__PURE__ */ jsx307(GridLines, { width, height, chartH, maxVal }),
6455
- /* @__PURE__ */ jsx307(AxisLabels, { labels, count, chartW, height }),
6456
- entries.map(([key], di) => {
6457
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
6458
- const color = palette[2];
6459
- const areaColor = palette[0];
6460
- const points = seriesPoints[di];
6461
- const gradientId = `line-gradient-${di}`;
6462
- const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
6463
- 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`;
6464
- return /* @__PURE__ */ jsxs197("g", { children: [
6465
- /* @__PURE__ */ jsx307("defs", { children: /* @__PURE__ */ jsxs197("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
6466
- /* @__PURE__ */ jsx307("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
6467
- /* @__PURE__ */ jsx307("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
6468
- ] }) }),
6469
- /* @__PURE__ */ jsx307(
6470
- "path",
6480
+ if (clipRef.current) {
6481
+ clipRef.current.setAttribute("width", "0");
6482
+ requestAnimationFrame(() => {
6483
+ if (clipRef.current) {
6484
+ clipRef.current.style.transition = "width 1200ms ease-out 200ms";
6485
+ clipRef.current.setAttribute("width", `${width}`);
6486
+ }
6487
+ });
6488
+ }
6489
+ }, [animate, seriesPoints, width]);
6490
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
6491
+ const lineClipId = "line-area-clip";
6492
+ return /* @__PURE__ */ jsxs197(
6493
+ "svg",
6494
+ {
6495
+ viewBox: `0 0 ${width} ${height}`,
6496
+ className: "chart-svg",
6497
+ onMouseMove: (e) => {
6498
+ handleMouseMove(e);
6499
+ const svg = e.currentTarget;
6500
+ const rect = svg.getBoundingClientRect();
6501
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
6502
+ const points = seriesPoints[0];
6503
+ if (!points || points.length === 0) return;
6504
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
6505
+ let closest = 0;
6506
+ let minDist = Math.abs(points[0].x - mx);
6507
+ for (let i = 1; i < points.length; i++) {
6508
+ const dist = Math.abs(points[i].x - mx);
6509
+ if (dist < minDist) {
6510
+ minDist = dist;
6511
+ closest = i;
6512
+ }
6513
+ }
6514
+ if (minDist <= step / 2) {
6515
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
6516
+ } else {
6517
+ onLeave();
6518
+ }
6519
+ },
6520
+ onMouseLeave: () => {
6521
+ handleMouseLeave();
6522
+ onLeave();
6523
+ },
6524
+ children: [
6525
+ animate && /* @__PURE__ */ jsx307("defs", { children: /* @__PURE__ */ jsx307("clipPath", { id: lineClipId, children: /* @__PURE__ */ jsx307("rect", { ref: clipRef, x: "0", y: "0", width: animate ? 0 : width, height }) }) }),
6526
+ /* @__PURE__ */ jsx307(GridLines, { width, height, chartH, maxVal }),
6527
+ /* @__PURE__ */ jsx307(AxisLabels, { labels, count, chartW, height }),
6528
+ entries.map(([key], di) => {
6529
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
6530
+ const color = palette[2];
6531
+ const areaColor = palette[0];
6532
+ const points = seriesPoints[di];
6533
+ const gradientId = `line-gradient-${di}`;
6534
+ const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
6535
+ 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`;
6536
+ return /* @__PURE__ */ jsxs197("g", { children: [
6537
+ /* @__PURE__ */ jsx307("defs", { children: /* @__PURE__ */ jsxs197("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
6538
+ /* @__PURE__ */ jsx307("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
6539
+ /* @__PURE__ */ jsx307("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
6540
+ ] }) }),
6541
+ /* @__PURE__ */ jsx307(
6542
+ "path",
6543
+ {
6544
+ d: areaD,
6545
+ fill: `url(#${gradientId})`,
6546
+ clipPath: animate ? `url(#${lineClipId})` : void 0
6547
+ }
6548
+ ),
6549
+ /* @__PURE__ */ jsx307(
6550
+ "polyline",
6551
+ {
6552
+ ref: (el) => {
6553
+ lineRefs.current[di] = el;
6554
+ },
6555
+ points: polyPoints,
6556
+ fill: "none",
6557
+ stroke: color,
6558
+ strokeWidth: "2"
6559
+ }
6560
+ ),
6561
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ jsx307(
6562
+ "circle",
6563
+ {
6564
+ cx: points[activeIndex].x,
6565
+ cy: points[activeIndex].y,
6566
+ r: "5",
6567
+ fill: color,
6568
+ className: "chart-point-active"
6569
+ }
6570
+ )
6571
+ ] }, di);
6572
+ }),
6573
+ activeX !== null && /* @__PURE__ */ jsx307(
6574
+ "line",
6471
6575
  {
6472
- d: areaD,
6473
- fill: `url(#${gradientId})`,
6474
- className: "chart-area",
6475
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
6576
+ x1: activeX,
6577
+ y1: PADDING.top,
6578
+ x2: activeX,
6579
+ y2: PADDING.top + chartH,
6580
+ className: "chart-crosshair"
6476
6581
  }
6477
6582
  ),
6478
6583
  /* @__PURE__ */ jsx307(
6479
- "polyline",
6584
+ "rect",
6480
6585
  {
6481
- ref: (el) => {
6482
- lineRefs.current[di] = el;
6483
- },
6484
- points: polyPoints,
6485
- fill: "none",
6486
- stroke: color,
6487
- strokeWidth: "2"
6586
+ x: PADDING.left,
6587
+ y: PADDING.top,
6588
+ width: chartW,
6589
+ height: chartH,
6590
+ fill: "transparent",
6591
+ style: { cursor: "crosshair" }
6488
6592
  }
6489
- ),
6490
- showPoints && points.map((p, i) => /* @__PURE__ */ jsx307(
6491
- "circle",
6492
- {
6493
- cx: p.x,
6494
- cy: p.y,
6495
- r: "4",
6496
- fill: color,
6497
- className: "chart-point",
6498
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
6499
- onMouseMove: onMove,
6500
- onMouseLeave: onLeave
6501
- },
6502
- i
6503
- ))
6504
- ] }, di);
6505
- })
6506
- ] });
6593
+ )
6594
+ ]
6595
+ }
6596
+ );
6507
6597
  });
6508
6598
  LineChart.displayName = "LineChart";
6509
6599
  var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -6525,8 +6615,9 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
6525
6615
  ),
6526
6616
  [entries, count, chartW, chartH, maxVal]
6527
6617
  );
6528
- const showPoints = count <= 100;
6529
6618
  const lineRefs = React6.useRef([]);
6619
+ const curveClipRef = React6.useRef(null);
6620
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
6530
6621
  React6.useEffect(() => {
6531
6622
  if (!animate) return;
6532
6623
  lineRefs.current.forEach((el) => {
@@ -6539,61 +6630,123 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
6539
6630
  el.style.strokeDashoffset = "0";
6540
6631
  });
6541
6632
  });
6542
- }, [animate, seriesPoints]);
6543
- return /* @__PURE__ */ jsxs197("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
6544
- /* @__PURE__ */ jsx307(GridLines, { width, height, chartH, maxVal }),
6545
- /* @__PURE__ */ jsx307(AxisLabels, { labels, count, chartW, height }),
6546
- entries.map(([key], di) => {
6547
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
6548
- const color = palette[2];
6549
- const areaColor = palette[0];
6550
- const points = seriesPoints[di];
6551
- const gradientId = `curve-gradient-${di}`;
6552
- const linePath = toSmoothPath(points);
6553
- const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
6554
- return /* @__PURE__ */ jsxs197("g", { children: [
6555
- /* @__PURE__ */ jsx307("defs", { children: /* @__PURE__ */ jsxs197("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
6556
- /* @__PURE__ */ jsx307("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
6557
- /* @__PURE__ */ jsx307("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
6558
- ] }) }),
6559
- /* @__PURE__ */ jsx307(
6560
- "path",
6633
+ if (curveClipRef.current) {
6634
+ curveClipRef.current.setAttribute("width", "0");
6635
+ requestAnimationFrame(() => {
6636
+ if (curveClipRef.current) {
6637
+ curveClipRef.current.style.transition = "width 1200ms ease-out 200ms";
6638
+ curveClipRef.current.setAttribute("width", `${width}`);
6639
+ }
6640
+ });
6641
+ }
6642
+ }, [animate, seriesPoints, width]);
6643
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
6644
+ const curveClipId = "curve-area-clip";
6645
+ return /* @__PURE__ */ jsxs197(
6646
+ "svg",
6647
+ {
6648
+ viewBox: `0 0 ${width} ${height}`,
6649
+ className: "chart-svg",
6650
+ onMouseMove: (e) => {
6651
+ handleMouseMove(e);
6652
+ const svg = e.currentTarget;
6653
+ const rect = svg.getBoundingClientRect();
6654
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
6655
+ const points = seriesPoints[0];
6656
+ if (!points || points.length === 0) return;
6657
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
6658
+ let closest = 0;
6659
+ let minDist = Math.abs(points[0].x - mx);
6660
+ for (let i = 1; i < points.length; i++) {
6661
+ const dist = Math.abs(points[i].x - mx);
6662
+ if (dist < minDist) {
6663
+ minDist = dist;
6664
+ closest = i;
6665
+ }
6666
+ }
6667
+ if (minDist <= step / 2) {
6668
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
6669
+ } else {
6670
+ onLeave();
6671
+ }
6672
+ },
6673
+ onMouseLeave: () => {
6674
+ handleMouseLeave();
6675
+ onLeave();
6676
+ },
6677
+ children: [
6678
+ animate && /* @__PURE__ */ jsx307("defs", { children: /* @__PURE__ */ jsx307("clipPath", { id: curveClipId, children: /* @__PURE__ */ jsx307("rect", { ref: curveClipRef, x: "0", y: "0", width: animate ? 0 : width, height }) }) }),
6679
+ /* @__PURE__ */ jsx307(GridLines, { width, height, chartH, maxVal }),
6680
+ /* @__PURE__ */ jsx307(AxisLabels, { labels, count, chartW, height }),
6681
+ entries.map(([key], di) => {
6682
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
6683
+ const color = palette[2];
6684
+ const areaColor = palette[0];
6685
+ const points = seriesPoints[di];
6686
+ const gradientId = `curve-gradient-${di}`;
6687
+ const linePath = toSmoothPath(points);
6688
+ const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
6689
+ return /* @__PURE__ */ jsxs197("g", { children: [
6690
+ /* @__PURE__ */ jsx307("defs", { children: /* @__PURE__ */ jsxs197("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
6691
+ /* @__PURE__ */ jsx307("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
6692
+ /* @__PURE__ */ jsx307("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
6693
+ ] }) }),
6694
+ /* @__PURE__ */ jsx307(
6695
+ "path",
6696
+ {
6697
+ d: areaPath,
6698
+ fill: `url(#${gradientId})`,
6699
+ clipPath: animate ? `url(#${curveClipId})` : void 0
6700
+ }
6701
+ ),
6702
+ /* @__PURE__ */ jsx307(
6703
+ "path",
6704
+ {
6705
+ ref: (el) => {
6706
+ lineRefs.current[di] = el;
6707
+ },
6708
+ d: linePath,
6709
+ fill: "none",
6710
+ stroke: color,
6711
+ strokeWidth: "2"
6712
+ }
6713
+ ),
6714
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ jsx307(
6715
+ "circle",
6716
+ {
6717
+ cx: points[activeIndex].x,
6718
+ cy: points[activeIndex].y,
6719
+ r: "5",
6720
+ fill: color,
6721
+ className: "chart-point-active"
6722
+ }
6723
+ )
6724
+ ] }, di);
6725
+ }),
6726
+ activeX !== null && /* @__PURE__ */ jsx307(
6727
+ "line",
6561
6728
  {
6562
- d: areaPath,
6563
- fill: `url(#${gradientId})`,
6564
- className: "chart-area",
6565
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
6729
+ x1: activeX,
6730
+ y1: PADDING.top,
6731
+ x2: activeX,
6732
+ y2: PADDING.top + chartH,
6733
+ className: "chart-crosshair"
6566
6734
  }
6567
6735
  ),
6568
6736
  /* @__PURE__ */ jsx307(
6569
- "path",
6737
+ "rect",
6570
6738
  {
6571
- ref: (el) => {
6572
- lineRefs.current[di] = el;
6573
- },
6574
- d: linePath,
6575
- fill: "none",
6576
- stroke: color,
6577
- strokeWidth: "2"
6739
+ x: PADDING.left,
6740
+ y: PADDING.top,
6741
+ width: chartW,
6742
+ height: chartH,
6743
+ fill: "transparent",
6744
+ style: { cursor: "crosshair" }
6578
6745
  }
6579
- ),
6580
- showPoints && points.map((p, i) => /* @__PURE__ */ jsx307(
6581
- "circle",
6582
- {
6583
- cx: p.x,
6584
- cy: p.y,
6585
- r: "4",
6586
- fill: color,
6587
- className: "chart-point",
6588
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
6589
- onMouseMove: onMove,
6590
- onMouseLeave: onLeave
6591
- },
6592
- i
6593
- ))
6594
- ] }, di);
6595
- })
6596
- ] });
6746
+ )
6747
+ ]
6748
+ }
6749
+ );
6597
6750
  });
6598
6751
  CurveChart.displayName = "CurveChart";
6599
6752
  var BarChart = React6.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -6758,30 +6911,70 @@ var PieDonutChart = React6.memo(
6758
6911
  }
6759
6912
  );
6760
6913
  PieDonutChart.displayName = "PieDonutChart";
6761
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
6914
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
6762
6915
  const ref = React6.useRef(null);
6763
- const [adjustedX, setAdjustedX] = React6.useState(x);
6764
- React6.useEffect(() => {
6916
+ const [pos, setPos] = React6.useState({ left: 0, top: 0 });
6917
+ React6.useLayoutEffect(() => {
6765
6918
  const el = ref.current;
6766
6919
  if (!el) return;
6767
6920
  const w = el.offsetWidth;
6768
- const half = w / 2;
6769
- const margin = 8;
6770
- let nx = x;
6771
- if (x - half < margin) nx = half + margin;
6772
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
6773
- setAdjustedX(nx);
6774
- }, [x, containerWidth]);
6921
+ const h = el.offsetHeight;
6922
+ const vw = window.innerWidth;
6923
+ let left = clientX + TOOLTIP_OFFSET;
6924
+ let top = clientY - h - TOOLTIP_OFFSET;
6925
+ if (left + w > vw - 8) left = clientX - w - TOOLTIP_OFFSET;
6926
+ if (top < 8) top = clientY + TOOLTIP_OFFSET;
6927
+ if (left < 8) left = 8;
6928
+ setPos({ left, top });
6929
+ }, [clientX, clientY]);
6775
6930
  return /* @__PURE__ */ jsx307(
6776
6931
  "div",
6777
6932
  {
6778
6933
  ref,
6779
- className: "chart-tooltip",
6780
- style: { left: adjustedX, top: y },
6934
+ className: `chart-tooltip ${visible ? "chart-tooltip-show" : "chart-tooltip-hide"}`,
6935
+ style: { position: "fixed", left: pos.left, top: pos.top, zIndex: 1100 },
6781
6936
  children
6782
6937
  }
6783
6938
  );
6784
6939
  };
6940
+ var ChartLegend = ({ data, labels, type }) => {
6941
+ const entries = Object.entries(data);
6942
+ if (type === "pie" || type === "doughnut") {
6943
+ const values = entries.flatMap(([, v]) => v);
6944
+ const total = values.reduce((a, b) => a + b, 0) || 1;
6945
+ const firstKey = entries[0]?.[0] ?? "";
6946
+ const colorOffset = hashString(firstKey);
6947
+ return /* @__PURE__ */ jsx307("div", { className: "chart-legend", children: values.map((v, i) => {
6948
+ const pct = Math.round(v / total * 100);
6949
+ const color = PIE_COLORS[(i + colorOffset) % PIE_COLORS.length];
6950
+ return /* @__PURE__ */ jsxs197("div", { className: "chart-legend-item", children: [
6951
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
6952
+ /* @__PURE__ */ jsxs197("div", { className: "chart-legend-text", children: [
6953
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-label", children: labels[i] || `${i + 1}` }),
6954
+ /* @__PURE__ */ jsxs197("span", { className: "chart-legend-value", children: [
6955
+ v.toLocaleString(),
6956
+ "(",
6957
+ pct,
6958
+ "%)"
6959
+ ] })
6960
+ ] })
6961
+ ] }, i);
6962
+ }) });
6963
+ }
6964
+ return /* @__PURE__ */ jsx307("div", { className: "chart-legend", children: entries.map(([key], di) => {
6965
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
6966
+ const color = palette[2];
6967
+ const values = entries[di][1];
6968
+ const sum = values.reduce((a, b) => a + b, 0);
6969
+ return /* @__PURE__ */ jsxs197("div", { className: "chart-legend-item", children: [
6970
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
6971
+ /* @__PURE__ */ jsxs197("div", { className: "chart-legend-text", children: [
6972
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-label", children: key }),
6973
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-value", children: sum.toLocaleString() })
6974
+ ] })
6975
+ ] }, di);
6976
+ }) });
6977
+ };
6785
6978
  var Chart = React6.memo((props) => {
6786
6979
  const { type, data, labels, tooltip: showTooltip = true } = props;
6787
6980
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -6797,7 +6990,8 @@ var Chart = React6.memo((props) => {
6797
6990
  ready && type === "bar" && /* @__PURE__ */ jsx307(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
6798
6991
  ready && type === "pie" && /* @__PURE__ */ jsx307(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
6799
6992
  ready && type === "doughnut" && /* @__PURE__ */ jsx307(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
6800
- tooltip.visible && /* @__PURE__ */ jsx307(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
6993
+ ready && (type === "bar" || type === "pie" || type === "doughnut") && /* @__PURE__ */ jsx307(ChartLegend, { data: stableData, labels: stableLabels, type }),
6994
+ tooltip.content && /* @__PURE__ */ jsx307(ChartTooltipPortal, { clientX: tooltip.clientX, clientY: tooltip.clientY, visible: tooltip.visible, children: tooltip.content })
6801
6995
  ] });
6802
6996
  });
6803
6997
  Chart.displayName = "Chart";