@x-plat/design-system 0.5.32 → 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.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);
@@ -6418,14 +6406,14 @@ var AxisLabels = React6.memo(({ labels, count, chartW, height }) => {
6418
6406
  AxisLabels.displayName = "AxisLabels";
6419
6407
  var useCrosshair = (seriesPoints, entries, labels, chartH) => {
6420
6408
  const [activeIndex, setActiveIndex] = React6.useState(null);
6421
- const [mouseX, setMouseX] = React6.useState(null);
6422
6409
  const handleMouseMove = React6.useCallback((e) => {
6423
6410
  const svg = e.currentTarget;
6424
6411
  const rect = svg.getBoundingClientRect();
6425
6412
  const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
6426
- setMouseX(mx);
6427
6413
  if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
6428
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;
6429
6417
  let closest = 0;
6430
6418
  let minDist = Math.abs(points[0].x - mx);
6431
6419
  for (let i = 1; i < points.length; i++) {
@@ -6435,11 +6423,10 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
6435
6423
  closest = i;
6436
6424
  }
6437
6425
  }
6438
- setActiveIndex(closest);
6426
+ setActiveIndex(minDist <= threshold ? closest : null);
6439
6427
  }, [seriesPoints]);
6440
6428
  const handleMouseLeave = React6.useCallback(() => {
6441
6429
  setActiveIndex(null);
6442
- setMouseX(null);
6443
6430
  }, []);
6444
6431
  const tooltipContent = React6.useMemo(() => {
6445
6432
  if (activeIndex === null) return "";
@@ -6448,7 +6435,13 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
6448
6435
  return p ? `${key}: ${p.v}` : "";
6449
6436
  }).filter(Boolean).join(" / ");
6450
6437
  }, [activeIndex, entries, seriesPoints]);
6451
- return { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent };
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 };
6452
6445
  };
6453
6446
  var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
6454
6447
  const entries = React6.useMemo(() => Object.entries(data), [data]);
@@ -6471,7 +6464,7 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
6471
6464
  );
6472
6465
  const lineRefs = React6.useRef([]);
6473
6466
  const clipRef = React6.useRef(null);
6474
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
6467
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
6475
6468
  React6.useEffect(() => {
6476
6469
  if (!animate) return;
6477
6470
  lineRefs.current.forEach((el) => {
@@ -6494,8 +6487,7 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
6494
6487
  });
6495
6488
  }
6496
6489
  }, [animate, seriesPoints, width]);
6497
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
6498
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
6490
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
6499
6491
  const lineClipId = "line-area-clip";
6500
6492
  return /* @__PURE__ */ jsxs197(
6501
6493
  "svg",
@@ -6504,7 +6496,26 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
6504
6496
  className: "chart-svg",
6505
6497
  onMouseMove: (e) => {
6506
6498
  handleMouseMove(e);
6507
- onMove(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
+ }
6508
6519
  },
6509
6520
  onMouseLeave: () => {
6510
6521
  handleMouseLeave();
@@ -6559,21 +6570,16 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
6559
6570
  )
6560
6571
  ] }, di);
6561
6572
  }),
6562
- guideX !== null && /* @__PURE__ */ jsx307(
6573
+ activeX !== null && /* @__PURE__ */ jsx307(
6563
6574
  "line",
6564
6575
  {
6565
- x1: guideX,
6576
+ x1: activeX,
6566
6577
  y1: PADDING.top,
6567
- x2: guideX,
6578
+ x2: activeX,
6568
6579
  y2: PADDING.top + chartH,
6569
6580
  className: "chart-crosshair"
6570
6581
  }
6571
6582
  ),
6572
- activeIndex !== null && activeX !== null && /* @__PURE__ */ jsx307("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ jsxs197("div", { className: "chart-crosshair-label", children: [
6573
- labels[activeIndex],
6574
- " \u2014 ",
6575
- tooltipContent
6576
- ] }) }),
6577
6583
  /* @__PURE__ */ jsx307(
6578
6584
  "rect",
6579
6585
  {
@@ -6611,7 +6617,7 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
6611
6617
  );
6612
6618
  const lineRefs = React6.useRef([]);
6613
6619
  const curveClipRef = React6.useRef(null);
6614
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
6620
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
6615
6621
  React6.useEffect(() => {
6616
6622
  if (!animate) return;
6617
6623
  lineRefs.current.forEach((el) => {
@@ -6634,8 +6640,7 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
6634
6640
  });
6635
6641
  }
6636
6642
  }, [animate, seriesPoints, width]);
6637
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
6638
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
6643
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
6639
6644
  const curveClipId = "curve-area-clip";
6640
6645
  return /* @__PURE__ */ jsxs197(
6641
6646
  "svg",
@@ -6644,7 +6649,26 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
6644
6649
  className: "chart-svg",
6645
6650
  onMouseMove: (e) => {
6646
6651
  handleMouseMove(e);
6647
- onMove(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
+ }
6648
6672
  },
6649
6673
  onMouseLeave: () => {
6650
6674
  handleMouseLeave();
@@ -6699,21 +6723,16 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
6699
6723
  )
6700
6724
  ] }, di);
6701
6725
  }),
6702
- guideX !== null && /* @__PURE__ */ jsx307(
6726
+ activeX !== null && /* @__PURE__ */ jsx307(
6703
6727
  "line",
6704
6728
  {
6705
- x1: guideX,
6729
+ x1: activeX,
6706
6730
  y1: PADDING.top,
6707
- x2: guideX,
6731
+ x2: activeX,
6708
6732
  y2: PADDING.top + chartH,
6709
6733
  className: "chart-crosshair"
6710
6734
  }
6711
6735
  ),
6712
- activeIndex !== null && activeX !== null && /* @__PURE__ */ jsx307("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ jsxs197("div", { className: "chart-crosshair-label", children: [
6713
- labels[activeIndex],
6714
- " \u2014 ",
6715
- tooltipContent
6716
- ] }) }),
6717
6736
  /* @__PURE__ */ jsx307(
6718
6737
  "rect",
6719
6738
  {
@@ -6892,30 +6911,70 @@ var PieDonutChart = React6.memo(
6892
6911
  }
6893
6912
  );
6894
6913
  PieDonutChart.displayName = "PieDonutChart";
6895
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
6914
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
6896
6915
  const ref = React6.useRef(null);
6897
- const [adjustedX, setAdjustedX] = React6.useState(x);
6898
- React6.useEffect(() => {
6916
+ const [pos, setPos] = React6.useState({ left: 0, top: 0 });
6917
+ React6.useLayoutEffect(() => {
6899
6918
  const el = ref.current;
6900
6919
  if (!el) return;
6901
6920
  const w = el.offsetWidth;
6902
- const half = w / 2;
6903
- const margin = 8;
6904
- let nx = x;
6905
- if (x - half < margin) nx = half + margin;
6906
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
6907
- setAdjustedX(nx);
6908
- }, [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]);
6909
6930
  return /* @__PURE__ */ jsx307(
6910
6931
  "div",
6911
6932
  {
6912
6933
  ref,
6913
- className: "chart-tooltip",
6914
- 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 },
6915
6936
  children
6916
6937
  }
6917
6938
  );
6918
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
+ };
6919
6978
  var Chart = React6.memo((props) => {
6920
6979
  const { type, data, labels, tooltip: showTooltip = true } = props;
6921
6980
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -6931,7 +6990,8 @@ var Chart = React6.memo((props) => {
6931
6990
  ready && type === "bar" && /* @__PURE__ */ jsx307(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
6932
6991
  ready && type === "pie" && /* @__PURE__ */ jsx307(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
6933
6992
  ready && type === "doughnut" && /* @__PURE__ */ jsx307(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
6934
- 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 })
6935
6995
  ] });
6936
6996
  });
6937
6997
  Chart.displayName = "Chart";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-plat/design-system",
3
- "version": "0.5.32",
3
+ "version": "0.5.33",
4
4
  "description": "XPLAT UI Design System",
5
5
  "author": "XPLAT WOONG",
6
6
  "main": "dist/index.cjs",