@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.cjs CHANGED
@@ -6750,40 +6750,28 @@ var useChartAnimation = (containerRef, dataKey) => {
6750
6750
  }, [dataKey]);
6751
6751
  return animate || prefersReducedMotion();
6752
6752
  };
6753
+ var TOOLTIP_OFFSET = 12;
6753
6754
  var useChartTooltip = (enabled) => {
6754
6755
  const [tooltip, setTooltip] = import_react6.default.useState({
6755
6756
  visible: false,
6756
- x: 0,
6757
- y: 0,
6757
+ clientX: 0,
6758
+ clientY: 0,
6758
6759
  content: ""
6759
6760
  });
6760
6761
  const containerRef = import_react6.default.useRef(null);
6761
6762
  const rafRef = import_react6.default.useRef(0);
6762
6763
  const move = import_react6.default.useCallback((e) => {
6763
6764
  if (!enabled) return;
6764
- const clientX = e.clientX;
6765
- const clientY = e.clientY;
6765
+ const cx = e.clientX;
6766
+ const cy = e.clientY;
6766
6767
  cancelAnimationFrame(rafRef.current);
6767
6768
  rafRef.current = requestAnimationFrame(() => {
6768
- const rect = containerRef.current?.getBoundingClientRect();
6769
- if (!rect) return;
6770
- setTooltip((prev) => ({
6771
- ...prev,
6772
- x: clientX - rect.left,
6773
- y: clientY - rect.top - 12
6774
- }));
6769
+ setTooltip((prev) => ({ ...prev, clientX: cx, clientY: cy }));
6775
6770
  });
6776
6771
  }, [enabled]);
6777
6772
  const show = import_react6.default.useCallback((e, content) => {
6778
6773
  if (!enabled) return;
6779
- const rect = containerRef.current?.getBoundingClientRect();
6780
- if (!rect) return;
6781
- setTooltip({
6782
- visible: true,
6783
- x: e.clientX - rect.left,
6784
- y: e.clientY - rect.top - 12,
6785
- content
6786
- });
6774
+ setTooltip({ visible: true, clientX: e.clientX, clientY: e.clientY, content });
6787
6775
  }, [enabled]);
6788
6776
  const hide = import_react6.default.useCallback(() => {
6789
6777
  cancelAnimationFrame(rafRef.current);
@@ -6815,6 +6803,45 @@ var AxisLabels = import_react6.default.memo(({ labels, count, chartW, height })
6815
6803
  }) });
6816
6804
  });
6817
6805
  AxisLabels.displayName = "AxisLabels";
6806
+ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
6807
+ const [activeIndex, setActiveIndex] = import_react6.default.useState(null);
6808
+ const handleMouseMove = import_react6.default.useCallback((e) => {
6809
+ const svg = e.currentTarget;
6810
+ const rect = svg.getBoundingClientRect();
6811
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
6812
+ if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
6813
+ const points = seriesPoints[0];
6814
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
6815
+ const threshold = step / 2;
6816
+ let closest = 0;
6817
+ let minDist = Math.abs(points[0].x - mx);
6818
+ for (let i = 1; i < points.length; i++) {
6819
+ const dist = Math.abs(points[i].x - mx);
6820
+ if (dist < minDist) {
6821
+ minDist = dist;
6822
+ closest = i;
6823
+ }
6824
+ }
6825
+ setActiveIndex(minDist <= threshold ? closest : null);
6826
+ }, [seriesPoints]);
6827
+ const handleMouseLeave = import_react6.default.useCallback(() => {
6828
+ setActiveIndex(null);
6829
+ }, []);
6830
+ const tooltipContent = import_react6.default.useMemo(() => {
6831
+ if (activeIndex === null) return "";
6832
+ return entries.map(([key], di) => {
6833
+ const p = seriesPoints[di]?.[activeIndex];
6834
+ return p ? `${key}: ${p.v}` : "";
6835
+ }).filter(Boolean).join(" / ");
6836
+ }, [activeIndex, entries, seriesPoints]);
6837
+ const getTooltipAt = import_react6.default.useCallback((idx) => {
6838
+ return entries.map(([key], di) => {
6839
+ const p = seriesPoints[di]?.[idx];
6840
+ return p ? `${key}: ${p.v}` : "";
6841
+ }).filter(Boolean).join(" / ");
6842
+ }, [entries, seriesPoints]);
6843
+ return { activeIndex, handleMouseMove, handleMouseLeave, tooltipContent, getTooltipAt };
6844
+ };
6818
6845
  var LineChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
6819
6846
  const entries = import_react6.default.useMemo(() => Object.entries(data), [data]);
6820
6847
  const maxVal = import_react6.default.useMemo(() => {
@@ -6834,8 +6861,9 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
6834
6861
  ),
6835
6862
  [entries, count, chartW, chartH, maxVal]
6836
6863
  );
6837
- const showPoints = count <= 100;
6838
6864
  const lineRefs = import_react6.default.useRef([]);
6865
+ const clipRef = import_react6.default.useRef(null);
6866
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
6839
6867
  import_react6.default.useEffect(() => {
6840
6868
  if (!animate) return;
6841
6869
  lineRefs.current.forEach((el) => {
@@ -6848,61 +6876,123 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
6848
6876
  el.style.strokeDashoffset = "0";
6849
6877
  });
6850
6878
  });
6851
- }, [animate, seriesPoints]);
6852
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
6853
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
6854
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
6855
- entries.map(([key], di) => {
6856
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
6857
- const color = palette[2];
6858
- const areaColor = palette[0];
6859
- const points = seriesPoints[di];
6860
- const gradientId = `line-gradient-${di}`;
6861
- const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
6862
- 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`;
6863
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
6864
- /* @__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: [
6865
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
6866
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
6867
- ] }) }),
6868
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6869
- "path",
6879
+ if (clipRef.current) {
6880
+ clipRef.current.setAttribute("width", "0");
6881
+ requestAnimationFrame(() => {
6882
+ if (clipRef.current) {
6883
+ clipRef.current.style.transition = "width 1200ms ease-out 200ms";
6884
+ clipRef.current.setAttribute("width", `${width}`);
6885
+ }
6886
+ });
6887
+ }
6888
+ }, [animate, seriesPoints, width]);
6889
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
6890
+ const lineClipId = "line-area-clip";
6891
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
6892
+ "svg",
6893
+ {
6894
+ viewBox: `0 0 ${width} ${height}`,
6895
+ className: "chart-svg",
6896
+ onMouseMove: (e) => {
6897
+ handleMouseMove(e);
6898
+ const svg = e.currentTarget;
6899
+ const rect = svg.getBoundingClientRect();
6900
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
6901
+ const points = seriesPoints[0];
6902
+ if (!points || points.length === 0) return;
6903
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
6904
+ let closest = 0;
6905
+ let minDist = Math.abs(points[0].x - mx);
6906
+ for (let i = 1; i < points.length; i++) {
6907
+ const dist = Math.abs(points[i].x - mx);
6908
+ if (dist < minDist) {
6909
+ minDist = dist;
6910
+ closest = i;
6911
+ }
6912
+ }
6913
+ if (minDist <= step / 2) {
6914
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
6915
+ } else {
6916
+ onLeave();
6917
+ }
6918
+ },
6919
+ onMouseLeave: () => {
6920
+ handleMouseLeave();
6921
+ onLeave();
6922
+ },
6923
+ children: [
6924
+ 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 }) }) }),
6925
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
6926
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
6927
+ entries.map(([key], di) => {
6928
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
6929
+ const color = palette[2];
6930
+ const areaColor = palette[0];
6931
+ const points = seriesPoints[di];
6932
+ const gradientId = `line-gradient-${di}`;
6933
+ const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
6934
+ 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`;
6935
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
6936
+ /* @__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: [
6937
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
6938
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
6939
+ ] }) }),
6940
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6941
+ "path",
6942
+ {
6943
+ d: areaD,
6944
+ fill: `url(#${gradientId})`,
6945
+ clipPath: animate ? `url(#${lineClipId})` : void 0
6946
+ }
6947
+ ),
6948
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6949
+ "polyline",
6950
+ {
6951
+ ref: (el) => {
6952
+ lineRefs.current[di] = el;
6953
+ },
6954
+ points: polyPoints,
6955
+ fill: "none",
6956
+ stroke: color,
6957
+ strokeWidth: "2"
6958
+ }
6959
+ ),
6960
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6961
+ "circle",
6962
+ {
6963
+ cx: points[activeIndex].x,
6964
+ cy: points[activeIndex].y,
6965
+ r: "5",
6966
+ fill: color,
6967
+ className: "chart-point-active"
6968
+ }
6969
+ )
6970
+ ] }, di);
6971
+ }),
6972
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6973
+ "line",
6870
6974
  {
6871
- d: areaD,
6872
- fill: `url(#${gradientId})`,
6873
- className: "chart-area",
6874
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
6975
+ x1: activeX,
6976
+ y1: PADDING.top,
6977
+ x2: activeX,
6978
+ y2: PADDING.top + chartH,
6979
+ className: "chart-crosshair"
6875
6980
  }
6876
6981
  ),
6877
6982
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6878
- "polyline",
6983
+ "rect",
6879
6984
  {
6880
- ref: (el) => {
6881
- lineRefs.current[di] = el;
6882
- },
6883
- points: polyPoints,
6884
- fill: "none",
6885
- stroke: color,
6886
- strokeWidth: "2"
6985
+ x: PADDING.left,
6986
+ y: PADDING.top,
6987
+ width: chartW,
6988
+ height: chartH,
6989
+ fill: "transparent",
6990
+ style: { cursor: "crosshair" }
6887
6991
  }
6888
- ),
6889
- showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6890
- "circle",
6891
- {
6892
- cx: p.x,
6893
- cy: p.y,
6894
- r: "4",
6895
- fill: color,
6896
- className: "chart-point",
6897
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
6898
- onMouseMove: onMove,
6899
- onMouseLeave: onLeave
6900
- },
6901
- i
6902
- ))
6903
- ] }, di);
6904
- })
6905
- ] });
6992
+ )
6993
+ ]
6994
+ }
6995
+ );
6906
6996
  });
6907
6997
  LineChart.displayName = "LineChart";
6908
6998
  var CurveChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -6924,8 +7014,9 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
6924
7014
  ),
6925
7015
  [entries, count, chartW, chartH, maxVal]
6926
7016
  );
6927
- const showPoints = count <= 100;
6928
7017
  const lineRefs = import_react6.default.useRef([]);
7018
+ const curveClipRef = import_react6.default.useRef(null);
7019
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
6929
7020
  import_react6.default.useEffect(() => {
6930
7021
  if (!animate) return;
6931
7022
  lineRefs.current.forEach((el) => {
@@ -6938,61 +7029,123 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
6938
7029
  el.style.strokeDashoffset = "0";
6939
7030
  });
6940
7031
  });
6941
- }, [animate, seriesPoints]);
6942
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
6943
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
6944
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
6945
- entries.map(([key], di) => {
6946
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
6947
- const color = palette[2];
6948
- const areaColor = palette[0];
6949
- const points = seriesPoints[di];
6950
- const gradientId = `curve-gradient-${di}`;
6951
- const linePath = toSmoothPath(points);
6952
- const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
6953
- return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
6954
- /* @__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: [
6955
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
6956
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
6957
- ] }) }),
6958
- /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6959
- "path",
7032
+ if (curveClipRef.current) {
7033
+ curveClipRef.current.setAttribute("width", "0");
7034
+ requestAnimationFrame(() => {
7035
+ if (curveClipRef.current) {
7036
+ curveClipRef.current.style.transition = "width 1200ms ease-out 200ms";
7037
+ curveClipRef.current.setAttribute("width", `${width}`);
7038
+ }
7039
+ });
7040
+ }
7041
+ }, [animate, seriesPoints, width]);
7042
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
7043
+ const curveClipId = "curve-area-clip";
7044
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
7045
+ "svg",
7046
+ {
7047
+ viewBox: `0 0 ${width} ${height}`,
7048
+ className: "chart-svg",
7049
+ onMouseMove: (e) => {
7050
+ handleMouseMove(e);
7051
+ const svg = e.currentTarget;
7052
+ const rect = svg.getBoundingClientRect();
7053
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
7054
+ const points = seriesPoints[0];
7055
+ if (!points || points.length === 0) return;
7056
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
7057
+ let closest = 0;
7058
+ let minDist = Math.abs(points[0].x - mx);
7059
+ for (let i = 1; i < points.length; i++) {
7060
+ const dist = Math.abs(points[i].x - mx);
7061
+ if (dist < minDist) {
7062
+ minDist = dist;
7063
+ closest = i;
7064
+ }
7065
+ }
7066
+ if (minDist <= step / 2) {
7067
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
7068
+ } else {
7069
+ onLeave();
7070
+ }
7071
+ },
7072
+ onMouseLeave: () => {
7073
+ handleMouseLeave();
7074
+ onLeave();
7075
+ },
7076
+ children: [
7077
+ 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 }) }) }),
7078
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(GridLines, { width, height, chartH, maxVal }),
7079
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(AxisLabels, { labels, count, chartW, height }),
7080
+ entries.map(([key], di) => {
7081
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
7082
+ const color = palette[2];
7083
+ const areaColor = palette[0];
7084
+ const points = seriesPoints[di];
7085
+ const gradientId = `curve-gradient-${di}`;
7086
+ const linePath = toSmoothPath(points);
7087
+ const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
7088
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("g", { children: [
7089
+ /* @__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: [
7090
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
7091
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
7092
+ ] }) }),
7093
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
7094
+ "path",
7095
+ {
7096
+ d: areaPath,
7097
+ fill: `url(#${gradientId})`,
7098
+ clipPath: animate ? `url(#${curveClipId})` : void 0
7099
+ }
7100
+ ),
7101
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
7102
+ "path",
7103
+ {
7104
+ ref: (el) => {
7105
+ lineRefs.current[di] = el;
7106
+ },
7107
+ d: linePath,
7108
+ fill: "none",
7109
+ stroke: color,
7110
+ strokeWidth: "2"
7111
+ }
7112
+ ),
7113
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
7114
+ "circle",
7115
+ {
7116
+ cx: points[activeIndex].x,
7117
+ cy: points[activeIndex].y,
7118
+ r: "5",
7119
+ fill: color,
7120
+ className: "chart-point-active"
7121
+ }
7122
+ )
7123
+ ] }, di);
7124
+ }),
7125
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
7126
+ "line",
6960
7127
  {
6961
- d: areaPath,
6962
- fill: `url(#${gradientId})`,
6963
- className: "chart-area",
6964
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
7128
+ x1: activeX,
7129
+ y1: PADDING.top,
7130
+ x2: activeX,
7131
+ y2: PADDING.top + chartH,
7132
+ className: "chart-crosshair"
6965
7133
  }
6966
7134
  ),
6967
7135
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6968
- "path",
7136
+ "rect",
6969
7137
  {
6970
- ref: (el) => {
6971
- lineRefs.current[di] = el;
6972
- },
6973
- d: linePath,
6974
- fill: "none",
6975
- stroke: color,
6976
- strokeWidth: "2"
7138
+ x: PADDING.left,
7139
+ y: PADDING.top,
7140
+ width: chartW,
7141
+ height: chartH,
7142
+ fill: "transparent",
7143
+ style: { cursor: "crosshair" }
6977
7144
  }
6978
- ),
6979
- showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
6980
- "circle",
6981
- {
6982
- cx: p.x,
6983
- cy: p.y,
6984
- r: "4",
6985
- fill: color,
6986
- className: "chart-point",
6987
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
6988
- onMouseMove: onMove,
6989
- onMouseLeave: onLeave
6990
- },
6991
- i
6992
- ))
6993
- ] }, di);
6994
- })
6995
- ] });
7145
+ )
7146
+ ]
7147
+ }
7148
+ );
6996
7149
  });
6997
7150
  CurveChart.displayName = "CurveChart";
6998
7151
  var BarChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -7157,30 +7310,70 @@ var PieDonutChart = import_react6.default.memo(
7157
7310
  }
7158
7311
  );
7159
7312
  PieDonutChart.displayName = "PieDonutChart";
7160
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
7313
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
7161
7314
  const ref = import_react6.default.useRef(null);
7162
- const [adjustedX, setAdjustedX] = import_react6.default.useState(x);
7163
- import_react6.default.useEffect(() => {
7315
+ const [pos, setPos] = import_react6.default.useState({ left: 0, top: 0 });
7316
+ import_react6.default.useLayoutEffect(() => {
7164
7317
  const el = ref.current;
7165
7318
  if (!el) return;
7166
7319
  const w = el.offsetWidth;
7167
- const half = w / 2;
7168
- const margin = 8;
7169
- let nx = x;
7170
- if (x - half < margin) nx = half + margin;
7171
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
7172
- setAdjustedX(nx);
7173
- }, [x, containerWidth]);
7320
+ const h = el.offsetHeight;
7321
+ const vw = window.innerWidth;
7322
+ let left = clientX + TOOLTIP_OFFSET;
7323
+ let top = clientY - h - TOOLTIP_OFFSET;
7324
+ if (left + w > vw - 8) left = clientX - w - TOOLTIP_OFFSET;
7325
+ if (top < 8) top = clientY + TOOLTIP_OFFSET;
7326
+ if (left < 8) left = 8;
7327
+ setPos({ left, top });
7328
+ }, [clientX, clientY]);
7174
7329
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
7175
7330
  "div",
7176
7331
  {
7177
7332
  ref,
7178
- className: "chart-tooltip",
7179
- style: { left: adjustedX, top: y },
7333
+ className: `chart-tooltip ${visible ? "chart-tooltip-show" : "chart-tooltip-hide"}`,
7334
+ style: { position: "fixed", left: pos.left, top: pos.top, zIndex: 1100 },
7180
7335
  children
7181
7336
  }
7182
7337
  );
7183
7338
  };
7339
+ var ChartLegend = ({ data, labels, type }) => {
7340
+ const entries = Object.entries(data);
7341
+ if (type === "pie" || type === "doughnut") {
7342
+ const values = entries.flatMap(([, v]) => v);
7343
+ const total = values.reduce((a, b) => a + b, 0) || 1;
7344
+ const firstKey = entries[0]?.[0] ?? "";
7345
+ const colorOffset = hashString(firstKey);
7346
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("div", { className: "chart-legend", children: values.map((v, i) => {
7347
+ const pct = Math.round(v / total * 100);
7348
+ const color = PIE_COLORS[(i + colorOffset) % PIE_COLORS.length];
7349
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-item", children: [
7350
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
7351
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-text", children: [
7352
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-label", children: labels[i] || `${i + 1}` }),
7353
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("span", { className: "chart-legend-value", children: [
7354
+ v.toLocaleString(),
7355
+ "(",
7356
+ pct,
7357
+ "%)"
7358
+ ] })
7359
+ ] })
7360
+ ] }, i);
7361
+ }) });
7362
+ }
7363
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("div", { className: "chart-legend", children: entries.map(([key], di) => {
7364
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
7365
+ const color = palette[2];
7366
+ const values = entries[di][1];
7367
+ const sum = values.reduce((a, b) => a + b, 0);
7368
+ return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-item", children: [
7369
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
7370
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-legend-text", children: [
7371
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-label", children: key }),
7372
+ /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("span", { className: "chart-legend-value", children: sum.toLocaleString() })
7373
+ ] })
7374
+ ] }, di);
7375
+ }) });
7376
+ };
7184
7377
  var Chart = import_react6.default.memo((props) => {
7185
7378
  const { type, data, labels, tooltip: showTooltip = true } = props;
7186
7379
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -7196,7 +7389,8 @@ var Chart = import_react6.default.memo((props) => {
7196
7389
  ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
7197
7390
  ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
7198
7391
  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 }),
7199
- tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
7392
+ ready && (type === "bar" || type === "pie" || type === "doughnut") && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(ChartLegend, { data: stableData, labels: stableLabels, type }),
7393
+ tooltip.content && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(ChartTooltipPortal, { clientX: tooltip.clientX, clientY: tooltip.clientY, visible: tooltip.visible, children: tooltip.content })
7200
7394
  ] });
7201
7395
  });
7202
7396
  Chart.displayName = "Chart";