@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.
@@ -121,40 +121,28 @@ var useChartAnimation = (containerRef, dataKey) => {
121
121
  }, [dataKey]);
122
122
  return animate || prefersReducedMotion();
123
123
  };
124
+ var TOOLTIP_OFFSET = 12;
124
125
  var useChartTooltip = (enabled) => {
125
126
  const [tooltip, setTooltip] = React.useState({
126
127
  visible: false,
127
- x: 0,
128
- y: 0,
128
+ clientX: 0,
129
+ clientY: 0,
129
130
  content: ""
130
131
  });
131
132
  const containerRef = React.useRef(null);
132
133
  const rafRef = React.useRef(0);
133
134
  const move = React.useCallback((e) => {
134
135
  if (!enabled) return;
135
- const clientX = e.clientX;
136
- const clientY = e.clientY;
136
+ const cx = e.clientX;
137
+ const cy = e.clientY;
137
138
  cancelAnimationFrame(rafRef.current);
138
139
  rafRef.current = requestAnimationFrame(() => {
139
- const rect = containerRef.current?.getBoundingClientRect();
140
- if (!rect) return;
141
- setTooltip((prev) => ({
142
- ...prev,
143
- x: clientX - rect.left,
144
- y: clientY - rect.top - 12
145
- }));
140
+ setTooltip((prev) => ({ ...prev, clientX: cx, clientY: cy }));
146
141
  });
147
142
  }, [enabled]);
148
143
  const show = React.useCallback((e, content) => {
149
144
  if (!enabled) return;
150
- const rect = containerRef.current?.getBoundingClientRect();
151
- if (!rect) return;
152
- setTooltip({
153
- visible: true,
154
- x: e.clientX - rect.left,
155
- y: e.clientY - rect.top - 12,
156
- content
157
- });
145
+ setTooltip({ visible: true, clientX: e.clientX, clientY: e.clientY, content });
158
146
  }, [enabled]);
159
147
  const hide = React.useCallback(() => {
160
148
  cancelAnimationFrame(rafRef.current);
@@ -188,14 +176,14 @@ var AxisLabels = React.memo(({ labels, count, chartW, height }) => {
188
176
  AxisLabels.displayName = "AxisLabels";
189
177
  var useCrosshair = (seriesPoints, entries, labels, chartH) => {
190
178
  const [activeIndex, setActiveIndex] = React.useState(null);
191
- const [mouseX, setMouseX] = React.useState(null);
192
179
  const handleMouseMove = React.useCallback((e) => {
193
180
  const svg = e.currentTarget;
194
181
  const rect = svg.getBoundingClientRect();
195
182
  const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
196
- setMouseX(mx);
197
183
  if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
198
184
  const points = seriesPoints[0];
185
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
186
+ const threshold = step / 2;
199
187
  let closest = 0;
200
188
  let minDist = Math.abs(points[0].x - mx);
201
189
  for (let i = 1; i < points.length; i++) {
@@ -205,11 +193,10 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
205
193
  closest = i;
206
194
  }
207
195
  }
208
- setActiveIndex(closest);
196
+ setActiveIndex(minDist <= threshold ? closest : null);
209
197
  }, [seriesPoints]);
210
198
  const handleMouseLeave = React.useCallback(() => {
211
199
  setActiveIndex(null);
212
- setMouseX(null);
213
200
  }, []);
214
201
  const tooltipContent = React.useMemo(() => {
215
202
  if (activeIndex === null) return "";
@@ -218,7 +205,13 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
218
205
  return p ? `${key}: ${p.v}` : "";
219
206
  }).filter(Boolean).join(" / ");
220
207
  }, [activeIndex, entries, seriesPoints]);
221
- return { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent };
208
+ const getTooltipAt = React.useCallback((idx) => {
209
+ return entries.map(([key], di) => {
210
+ const p = seriesPoints[di]?.[idx];
211
+ return p ? `${key}: ${p.v}` : "";
212
+ }).filter(Boolean).join(" / ");
213
+ }, [entries, seriesPoints]);
214
+ return { activeIndex, handleMouseMove, handleMouseLeave, tooltipContent, getTooltipAt };
222
215
  };
223
216
  var LineChart = React.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
224
217
  const entries = React.useMemo(() => Object.entries(data), [data]);
@@ -241,7 +234,7 @@ var LineChart = React.memo(({ data, labels, width, height, animate, onHover, onM
241
234
  );
242
235
  const lineRefs = React.useRef([]);
243
236
  const clipRef = React.useRef(null);
244
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
237
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
245
238
  React.useEffect(() => {
246
239
  if (!animate) return;
247
240
  lineRefs.current.forEach((el) => {
@@ -264,8 +257,7 @@ var LineChart = React.memo(({ data, labels, width, height, animate, onHover, onM
264
257
  });
265
258
  }
266
259
  }, [animate, seriesPoints, width]);
267
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
268
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
260
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
269
261
  const lineClipId = "line-area-clip";
270
262
  return /* @__PURE__ */ jsxs(
271
263
  "svg",
@@ -274,7 +266,26 @@ var LineChart = React.memo(({ data, labels, width, height, animate, onHover, onM
274
266
  className: "chart-svg",
275
267
  onMouseMove: (e) => {
276
268
  handleMouseMove(e);
277
- onMove(e);
269
+ const svg = e.currentTarget;
270
+ const rect = svg.getBoundingClientRect();
271
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
272
+ const points = seriesPoints[0];
273
+ if (!points || points.length === 0) return;
274
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
275
+ let closest = 0;
276
+ let minDist = Math.abs(points[0].x - mx);
277
+ for (let i = 1; i < points.length; i++) {
278
+ const dist = Math.abs(points[i].x - mx);
279
+ if (dist < minDist) {
280
+ minDist = dist;
281
+ closest = i;
282
+ }
283
+ }
284
+ if (minDist <= step / 2) {
285
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
286
+ } else {
287
+ onLeave();
288
+ }
278
289
  },
279
290
  onMouseLeave: () => {
280
291
  handleMouseLeave();
@@ -329,21 +340,16 @@ var LineChart = React.memo(({ data, labels, width, height, animate, onHover, onM
329
340
  )
330
341
  ] }, di);
331
342
  }),
332
- guideX !== null && /* @__PURE__ */ jsx(
343
+ activeX !== null && /* @__PURE__ */ jsx(
333
344
  "line",
334
345
  {
335
- x1: guideX,
346
+ x1: activeX,
336
347
  y1: PADDING.top,
337
- x2: guideX,
348
+ x2: activeX,
338
349
  y2: PADDING.top + chartH,
339
350
  className: "chart-crosshair"
340
351
  }
341
352
  ),
342
- activeIndex !== null && activeX !== null && /* @__PURE__ */ jsx("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ jsxs("div", { className: "chart-crosshair-label", children: [
343
- labels[activeIndex],
344
- " \u2014 ",
345
- tooltipContent
346
- ] }) }),
347
353
  /* @__PURE__ */ jsx(
348
354
  "rect",
349
355
  {
@@ -381,7 +387,7 @@ var CurveChart = React.memo(({ data, labels, width, height, animate, onHover, on
381
387
  );
382
388
  const lineRefs = React.useRef([]);
383
389
  const curveClipRef = React.useRef(null);
384
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
390
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
385
391
  React.useEffect(() => {
386
392
  if (!animate) return;
387
393
  lineRefs.current.forEach((el) => {
@@ -404,8 +410,7 @@ var CurveChart = React.memo(({ data, labels, width, height, animate, onHover, on
404
410
  });
405
411
  }
406
412
  }, [animate, seriesPoints, width]);
407
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
408
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
413
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
409
414
  const curveClipId = "curve-area-clip";
410
415
  return /* @__PURE__ */ jsxs(
411
416
  "svg",
@@ -414,7 +419,26 @@ var CurveChart = React.memo(({ data, labels, width, height, animate, onHover, on
414
419
  className: "chart-svg",
415
420
  onMouseMove: (e) => {
416
421
  handleMouseMove(e);
417
- onMove(e);
422
+ const svg = e.currentTarget;
423
+ const rect = svg.getBoundingClientRect();
424
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
425
+ const points = seriesPoints[0];
426
+ if (!points || points.length === 0) return;
427
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
428
+ let closest = 0;
429
+ let minDist = Math.abs(points[0].x - mx);
430
+ for (let i = 1; i < points.length; i++) {
431
+ const dist = Math.abs(points[i].x - mx);
432
+ if (dist < minDist) {
433
+ minDist = dist;
434
+ closest = i;
435
+ }
436
+ }
437
+ if (minDist <= step / 2) {
438
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
439
+ } else {
440
+ onLeave();
441
+ }
418
442
  },
419
443
  onMouseLeave: () => {
420
444
  handleMouseLeave();
@@ -469,21 +493,16 @@ var CurveChart = React.memo(({ data, labels, width, height, animate, onHover, on
469
493
  )
470
494
  ] }, di);
471
495
  }),
472
- guideX !== null && /* @__PURE__ */ jsx(
496
+ activeX !== null && /* @__PURE__ */ jsx(
473
497
  "line",
474
498
  {
475
- x1: guideX,
499
+ x1: activeX,
476
500
  y1: PADDING.top,
477
- x2: guideX,
501
+ x2: activeX,
478
502
  y2: PADDING.top + chartH,
479
503
  className: "chart-crosshair"
480
504
  }
481
505
  ),
482
- activeIndex !== null && activeX !== null && /* @__PURE__ */ jsx("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ jsxs("div", { className: "chart-crosshair-label", children: [
483
- labels[activeIndex],
484
- " \u2014 ",
485
- tooltipContent
486
- ] }) }),
487
506
  /* @__PURE__ */ jsx(
488
507
  "rect",
489
508
  {
@@ -662,30 +681,70 @@ var PieDonutChart = React.memo(
662
681
  }
663
682
  );
664
683
  PieDonutChart.displayName = "PieDonutChart";
665
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
684
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
666
685
  const ref = React.useRef(null);
667
- const [adjustedX, setAdjustedX] = React.useState(x);
668
- React.useEffect(() => {
686
+ const [pos, setPos] = React.useState({ left: 0, top: 0 });
687
+ React.useLayoutEffect(() => {
669
688
  const el = ref.current;
670
689
  if (!el) return;
671
690
  const w = el.offsetWidth;
672
- const half = w / 2;
673
- const margin = 8;
674
- let nx = x;
675
- if (x - half < margin) nx = half + margin;
676
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
677
- setAdjustedX(nx);
678
- }, [x, containerWidth]);
691
+ const h = el.offsetHeight;
692
+ const vw = window.innerWidth;
693
+ let left = clientX + TOOLTIP_OFFSET;
694
+ let top = clientY - h - TOOLTIP_OFFSET;
695
+ if (left + w > vw - 8) left = clientX - w - TOOLTIP_OFFSET;
696
+ if (top < 8) top = clientY + TOOLTIP_OFFSET;
697
+ if (left < 8) left = 8;
698
+ setPos({ left, top });
699
+ }, [clientX, clientY]);
679
700
  return /* @__PURE__ */ jsx(
680
701
  "div",
681
702
  {
682
703
  ref,
683
- className: "chart-tooltip",
684
- style: { left: adjustedX, top: y },
704
+ className: `chart-tooltip ${visible ? "chart-tooltip-show" : "chart-tooltip-hide"}`,
705
+ style: { position: "fixed", left: pos.left, top: pos.top, zIndex: 1100 },
685
706
  children
686
707
  }
687
708
  );
688
709
  };
710
+ var ChartLegend = ({ data, labels, type }) => {
711
+ const entries = Object.entries(data);
712
+ if (type === "pie" || type === "doughnut") {
713
+ const values = entries.flatMap(([, v]) => v);
714
+ const total = values.reduce((a, b) => a + b, 0) || 1;
715
+ const firstKey = entries[0]?.[0] ?? "";
716
+ const colorOffset = hashString(firstKey);
717
+ return /* @__PURE__ */ jsx("div", { className: "chart-legend", children: values.map((v, i) => {
718
+ const pct = Math.round(v / total * 100);
719
+ const color = PIE_COLORS[(i + colorOffset) % PIE_COLORS.length];
720
+ return /* @__PURE__ */ jsxs("div", { className: "chart-legend-item", children: [
721
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
722
+ /* @__PURE__ */ jsxs("div", { className: "chart-legend-text", children: [
723
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-label", children: labels[i] || `${i + 1}` }),
724
+ /* @__PURE__ */ jsxs("span", { className: "chart-legend-value", children: [
725
+ v.toLocaleString(),
726
+ "(",
727
+ pct,
728
+ "%)"
729
+ ] })
730
+ ] })
731
+ ] }, i);
732
+ }) });
733
+ }
734
+ return /* @__PURE__ */ jsx("div", { className: "chart-legend", children: entries.map(([key], di) => {
735
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
736
+ const color = palette[2];
737
+ const values = entries[di][1];
738
+ const sum = values.reduce((a, b) => a + b, 0);
739
+ return /* @__PURE__ */ jsxs("div", { className: "chart-legend-item", children: [
740
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
741
+ /* @__PURE__ */ jsxs("div", { className: "chart-legend-text", children: [
742
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-label", children: key }),
743
+ /* @__PURE__ */ jsx("span", { className: "chart-legend-value", children: sum.toLocaleString() })
744
+ ] })
745
+ ] }, di);
746
+ }) });
747
+ };
689
748
  var Chart = React.memo((props) => {
690
749
  const { type, data, labels, tooltip: showTooltip = true } = props;
691
750
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -701,7 +760,8 @@ var Chart = React.memo((props) => {
701
760
  ready && type === "bar" && /* @__PURE__ */ jsx(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
702
761
  ready && type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
703
762
  ready && type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
704
- tooltip.visible && /* @__PURE__ */ jsx(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
763
+ ready && (type === "bar" || type === "pie" || type === "doughnut") && /* @__PURE__ */ jsx(ChartLegend, { data: stableData, labels: stableLabels, type }),
764
+ tooltip.content && /* @__PURE__ */ jsx(ChartTooltipPortal, { clientX: tooltip.clientX, clientY: tooltip.clientY, visible: tooltip.visible, children: tooltip.content })
705
765
  ] });
706
766
  });
707
767
  Chart.displayName = "Chart";
@@ -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);
@@ -2418,14 +2406,14 @@ var AxisLabels = import_react6.default.memo(({ labels, count, chartW, height })
2418
2406
  AxisLabels.displayName = "AxisLabels";
2419
2407
  var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2420
2408
  const [activeIndex, setActiveIndex] = import_react6.default.useState(null);
2421
- const [mouseX, setMouseX] = import_react6.default.useState(null);
2422
2409
  const handleMouseMove = import_react6.default.useCallback((e) => {
2423
2410
  const svg = e.currentTarget;
2424
2411
  const rect = svg.getBoundingClientRect();
2425
2412
  const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2426
- setMouseX(mx);
2427
2413
  if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
2428
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;
2429
2417
  let closest = 0;
2430
2418
  let minDist = Math.abs(points[0].x - mx);
2431
2419
  for (let i = 1; i < points.length; i++) {
@@ -2435,11 +2423,10 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2435
2423
  closest = i;
2436
2424
  }
2437
2425
  }
2438
- setActiveIndex(closest);
2426
+ setActiveIndex(minDist <= threshold ? closest : null);
2439
2427
  }, [seriesPoints]);
2440
2428
  const handleMouseLeave = import_react6.default.useCallback(() => {
2441
2429
  setActiveIndex(null);
2442
- setMouseX(null);
2443
2430
  }, []);
2444
2431
  const tooltipContent = import_react6.default.useMemo(() => {
2445
2432
  if (activeIndex === null) return "";
@@ -2448,7 +2435,13 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2448
2435
  return p ? `${key}: ${p.v}` : "";
2449
2436
  }).filter(Boolean).join(" / ");
2450
2437
  }, [activeIndex, entries, seriesPoints]);
2451
- return { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent };
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 };
2452
2445
  };
2453
2446
  var LineChart = import_react6.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
2454
2447
  const entries = import_react6.default.useMemo(() => Object.entries(data), [data]);
@@ -2471,7 +2464,7 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2471
2464
  );
2472
2465
  const lineRefs = import_react6.default.useRef([]);
2473
2466
  const clipRef = import_react6.default.useRef(null);
2474
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
2467
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2475
2468
  import_react6.default.useEffect(() => {
2476
2469
  if (!animate) return;
2477
2470
  lineRefs.current.forEach((el) => {
@@ -2494,8 +2487,7 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2494
2487
  });
2495
2488
  }
2496
2489
  }, [animate, seriesPoints, width]);
2497
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
2498
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
2490
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2499
2491
  const lineClipId = "line-area-clip";
2500
2492
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
2501
2493
  "svg",
@@ -2504,7 +2496,26 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2504
2496
  className: "chart-svg",
2505
2497
  onMouseMove: (e) => {
2506
2498
  handleMouseMove(e);
2507
- onMove(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
+ }
2508
2519
  },
2509
2520
  onMouseLeave: () => {
2510
2521
  handleMouseLeave();
@@ -2559,21 +2570,16 @@ var LineChart = import_react6.default.memo(({ data, labels, width, height, anima
2559
2570
  )
2560
2571
  ] }, di);
2561
2572
  }),
2562
- guideX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2573
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2563
2574
  "line",
2564
2575
  {
2565
- x1: guideX,
2576
+ x1: activeX,
2566
2577
  y1: PADDING.top,
2567
- x2: guideX,
2578
+ x2: activeX,
2568
2579
  y2: PADDING.top + chartH,
2569
2580
  className: "chart-crosshair"
2570
2581
  }
2571
2582
  ),
2572
- activeIndex !== null && activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-crosshair-label", children: [
2573
- labels[activeIndex],
2574
- " \u2014 ",
2575
- tooltipContent
2576
- ] }) }),
2577
2583
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2578
2584
  "rect",
2579
2585
  {
@@ -2611,7 +2617,7 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2611
2617
  );
2612
2618
  const lineRefs = import_react6.default.useRef([]);
2613
2619
  const curveClipRef = import_react6.default.useRef(null);
2614
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
2620
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2615
2621
  import_react6.default.useEffect(() => {
2616
2622
  if (!animate) return;
2617
2623
  lineRefs.current.forEach((el) => {
@@ -2634,8 +2640,7 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2634
2640
  });
2635
2641
  }
2636
2642
  }, [animate, seriesPoints, width]);
2637
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
2638
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
2643
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2639
2644
  const curveClipId = "curve-area-clip";
2640
2645
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)(
2641
2646
  "svg",
@@ -2644,7 +2649,26 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2644
2649
  className: "chart-svg",
2645
2650
  onMouseMove: (e) => {
2646
2651
  handleMouseMove(e);
2647
- onMove(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
+ }
2648
2672
  },
2649
2673
  onMouseLeave: () => {
2650
2674
  handleMouseLeave();
@@ -2699,21 +2723,16 @@ var CurveChart = import_react6.default.memo(({ data, labels, width, height, anim
2699
2723
  )
2700
2724
  ] }, di);
2701
2725
  }),
2702
- guideX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2726
+ activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2703
2727
  "line",
2704
2728
  {
2705
- x1: guideX,
2729
+ x1: activeX,
2706
2730
  y1: PADDING.top,
2707
- x2: guideX,
2731
+ x2: activeX,
2708
2732
  y2: PADDING.top + chartH,
2709
2733
  className: "chart-crosshair"
2710
2734
  }
2711
2735
  ),
2712
- activeIndex !== null && activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ (0, import_jsx_runtime307.jsxs)("div", { className: "chart-crosshair-label", children: [
2713
- labels[activeIndex],
2714
- " \u2014 ",
2715
- tooltipContent
2716
- ] }) }),
2717
2736
  /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2718
2737
  "rect",
2719
2738
  {
@@ -2892,30 +2911,70 @@ var PieDonutChart = import_react6.default.memo(
2892
2911
  }
2893
2912
  );
2894
2913
  PieDonutChart.displayName = "PieDonutChart";
2895
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
2914
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
2896
2915
  const ref = import_react6.default.useRef(null);
2897
- const [adjustedX, setAdjustedX] = import_react6.default.useState(x);
2898
- import_react6.default.useEffect(() => {
2916
+ const [pos, setPos] = import_react6.default.useState({ left: 0, top: 0 });
2917
+ import_react6.default.useLayoutEffect(() => {
2899
2918
  const el = ref.current;
2900
2919
  if (!el) return;
2901
2920
  const w = el.offsetWidth;
2902
- const half = w / 2;
2903
- const margin = 8;
2904
- let nx = x;
2905
- if (x - half < margin) nx = half + margin;
2906
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
2907
- setAdjustedX(nx);
2908
- }, [x, containerWidth]);
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]);
2909
2930
  return /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
2910
2931
  "div",
2911
2932
  {
2912
2933
  ref,
2913
- className: "chart-tooltip",
2914
- 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 },
2915
2936
  children
2916
2937
  }
2917
2938
  );
2918
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
+ };
2919
2978
  var Chart = import_react6.default.memo((props) => {
2920
2979
  const { type, data, labels, tooltip: showTooltip = true } = props;
2921
2980
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -2931,7 +2990,8 @@ var Chart = import_react6.default.memo((props) => {
2931
2990
  ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2932
2991
  ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2933
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 }),
2934
- 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 })
2935
2995
  ] });
2936
2996
  });
2937
2997
  Chart.displayName = "Chart";