@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/components/Chart/index.cjs +121 -61
- package/dist/components/Chart/index.css +54 -10
- package/dist/components/Chart/index.js +121 -61
- package/dist/components/index.cjs +121 -61
- package/dist/components/index.css +54 -10
- package/dist/components/index.js +121 -61
- package/dist/index.cjs +121 -61
- package/dist/index.css +54 -10
- package/dist/index.js +121 -61
- package/package.json +1 -1
|
@@ -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
|
-
|
|
128
|
-
|
|
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
|
|
136
|
-
const
|
|
136
|
+
const cx = e.clientX;
|
|
137
|
+
const cy = e.clientY;
|
|
137
138
|
cancelAnimationFrame(rafRef.current);
|
|
138
139
|
rafRef.current = requestAnimationFrame(() => {
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
343
|
+
activeX !== null && /* @__PURE__ */ jsx(
|
|
333
344
|
"line",
|
|
334
345
|
{
|
|
335
|
-
x1:
|
|
346
|
+
x1: activeX,
|
|
336
347
|
y1: PADDING.top,
|
|
337
|
-
x2:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
496
|
+
activeX !== null && /* @__PURE__ */ jsx(
|
|
473
497
|
"line",
|
|
474
498
|
{
|
|
475
|
-
x1:
|
|
499
|
+
x1: activeX,
|
|
476
500
|
y1: PADDING.top,
|
|
477
|
-
x2:
|
|
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
|
|
684
|
+
var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
|
|
666
685
|
const ref = React.useRef(null);
|
|
667
|
-
const [
|
|
668
|
-
React.
|
|
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
|
|
673
|
-
const
|
|
674
|
-
let
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
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
|
|
2366
|
-
const
|
|
2366
|
+
const cx = e.clientX;
|
|
2367
|
+
const cy = e.clientY;
|
|
2367
2368
|
cancelAnimationFrame(rafRef.current);
|
|
2368
2369
|
rafRef.current = requestAnimationFrame(() => {
|
|
2369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2573
|
+
activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
|
|
2563
2574
|
"line",
|
|
2564
2575
|
{
|
|
2565
|
-
x1:
|
|
2576
|
+
x1: activeX,
|
|
2566
2577
|
y1: PADDING.top,
|
|
2567
|
-
x2:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2726
|
+
activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime307.jsx)(
|
|
2703
2727
|
"line",
|
|
2704
2728
|
{
|
|
2705
|
-
x1:
|
|
2729
|
+
x1: activeX,
|
|
2706
2730
|
y1: PADDING.top,
|
|
2707
|
-
x2:
|
|
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
|
|
2914
|
+
var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
|
|
2896
2915
|
const ref = import_react6.default.useRef(null);
|
|
2897
|
-
const [
|
|
2898
|
-
import_react6.default.
|
|
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
|
|
2903
|
-
const
|
|
2904
|
-
let
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
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:
|
|
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
|
-
|
|
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";
|