@x-plat/design-system 0.5.30 → 0.5.32

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.
@@ -222,6 +222,40 @@ var AxisLabels = import_react.default.memo(({ labels, count, chartW, height }) =
222
222
  }) });
223
223
  });
224
224
  AxisLabels.displayName = "AxisLabels";
225
+ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
226
+ const [activeIndex, setActiveIndex] = import_react.default.useState(null);
227
+ const [mouseX, setMouseX] = import_react.default.useState(null);
228
+ const handleMouseMove = import_react.default.useCallback((e) => {
229
+ const svg = e.currentTarget;
230
+ const rect = svg.getBoundingClientRect();
231
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
232
+ setMouseX(mx);
233
+ if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
234
+ const points = seriesPoints[0];
235
+ let closest = 0;
236
+ let minDist = Math.abs(points[0].x - mx);
237
+ for (let i = 1; i < points.length; i++) {
238
+ const dist = Math.abs(points[i].x - mx);
239
+ if (dist < minDist) {
240
+ minDist = dist;
241
+ closest = i;
242
+ }
243
+ }
244
+ setActiveIndex(closest);
245
+ }, [seriesPoints]);
246
+ const handleMouseLeave = import_react.default.useCallback(() => {
247
+ setActiveIndex(null);
248
+ setMouseX(null);
249
+ }, []);
250
+ const tooltipContent = import_react.default.useMemo(() => {
251
+ if (activeIndex === null) return "";
252
+ return entries.map(([key], di) => {
253
+ const p = seriesPoints[di]?.[activeIndex];
254
+ return p ? `${key}: ${p.v}` : "";
255
+ }).filter(Boolean).join(" / ");
256
+ }, [activeIndex, entries, seriesPoints]);
257
+ return { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent };
258
+ };
225
259
  var LineChart = import_react.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
226
260
  const entries = import_react.default.useMemo(() => Object.entries(data), [data]);
227
261
  const maxVal = import_react.default.useMemo(() => {
@@ -241,8 +275,9 @@ var LineChart = import_react.default.memo(({ data, labels, width, height, animat
241
275
  ),
242
276
  [entries, count, chartW, chartH, maxVal]
243
277
  );
244
- const showPoints = count <= 100;
245
278
  const lineRefs = import_react.default.useRef([]);
279
+ const clipRef = import_react.default.useRef(null);
280
+ const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
246
281
  import_react.default.useEffect(() => {
247
282
  if (!animate) return;
248
283
  lineRefs.current.forEach((el) => {
@@ -255,61 +290,110 @@ var LineChart = import_react.default.memo(({ data, labels, width, height, animat
255
290
  el.style.strokeDashoffset = "0";
256
291
  });
257
292
  });
258
- }, [animate, seriesPoints]);
259
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
260
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GridLines, { width, height, chartH, maxVal }),
261
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxisLabels, { labels, count, chartW, height }),
262
- entries.map(([key], di) => {
263
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
264
- const color = palette[2];
265
- const areaColor = palette[0];
266
- const points = seriesPoints[di];
267
- const gradientId = `line-gradient-${di}`;
268
- const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
269
- 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`;
270
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
271
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
272
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
273
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
274
- ] }) }),
275
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
276
- "path",
293
+ if (clipRef.current) {
294
+ clipRef.current.setAttribute("width", "0");
295
+ requestAnimationFrame(() => {
296
+ if (clipRef.current) {
297
+ clipRef.current.style.transition = "width 1200ms ease-out 200ms";
298
+ clipRef.current.setAttribute("width", `${width}`);
299
+ }
300
+ });
301
+ }
302
+ }, [animate, seriesPoints, width]);
303
+ const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
304
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
305
+ const lineClipId = "line-area-clip";
306
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
307
+ "svg",
308
+ {
309
+ viewBox: `0 0 ${width} ${height}`,
310
+ className: "chart-svg",
311
+ onMouseMove: (e) => {
312
+ handleMouseMove(e);
313
+ onMove(e);
314
+ },
315
+ onMouseLeave: () => {
316
+ handleMouseLeave();
317
+ onLeave();
318
+ },
319
+ children: [
320
+ animate && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("clipPath", { id: lineClipId, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { ref: clipRef, x: "0", y: "0", width: animate ? 0 : width, height }) }) }),
321
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GridLines, { width, height, chartH, maxVal }),
322
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxisLabels, { labels, count, chartW, height }),
323
+ entries.map(([key], di) => {
324
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
325
+ const color = palette[2];
326
+ const areaColor = palette[0];
327
+ const points = seriesPoints[di];
328
+ const gradientId = `line-gradient-${di}`;
329
+ const polyPoints = points.map((p) => `${p.x},${p.y}`).join(" ");
330
+ 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`;
331
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
333
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.2" }),
334
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0" })
335
+ ] }) }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
337
+ "path",
338
+ {
339
+ d: areaD,
340
+ fill: `url(#${gradientId})`,
341
+ clipPath: animate ? `url(#${lineClipId})` : void 0
342
+ }
343
+ ),
344
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
345
+ "polyline",
346
+ {
347
+ ref: (el) => {
348
+ lineRefs.current[di] = el;
349
+ },
350
+ points: polyPoints,
351
+ fill: "none",
352
+ stroke: color,
353
+ strokeWidth: "2"
354
+ }
355
+ ),
356
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
357
+ "circle",
358
+ {
359
+ cx: points[activeIndex].x,
360
+ cy: points[activeIndex].y,
361
+ r: "5",
362
+ fill: color,
363
+ className: "chart-point-active"
364
+ }
365
+ )
366
+ ] }, di);
367
+ }),
368
+ guideX !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
369
+ "line",
277
370
  {
278
- d: areaD,
279
- fill: `url(#${gradientId})`,
280
- className: "chart-area",
281
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
371
+ x1: guideX,
372
+ y1: PADDING.top,
373
+ x2: guideX,
374
+ y2: PADDING.top + chartH,
375
+ className: "chart-crosshair"
282
376
  }
283
377
  ),
378
+ activeIndex !== null && activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "chart-crosshair-label", children: [
379
+ labels[activeIndex],
380
+ " \u2014 ",
381
+ tooltipContent
382
+ ] }) }),
284
383
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
285
- "polyline",
384
+ "rect",
286
385
  {
287
- ref: (el) => {
288
- lineRefs.current[di] = el;
289
- },
290
- points: polyPoints,
291
- fill: "none",
292
- stroke: color,
293
- strokeWidth: "2"
386
+ x: PADDING.left,
387
+ y: PADDING.top,
388
+ width: chartW,
389
+ height: chartH,
390
+ fill: "transparent",
391
+ style: { cursor: "crosshair" }
294
392
  }
295
- ),
296
- showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
297
- "circle",
298
- {
299
- cx: p.x,
300
- cy: p.y,
301
- r: "4",
302
- fill: color,
303
- className: "chart-point",
304
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
305
- onMouseMove: onMove,
306
- onMouseLeave: onLeave
307
- },
308
- i
309
- ))
310
- ] }, di);
311
- })
312
- ] });
393
+ )
394
+ ]
395
+ }
396
+ );
313
397
  });
314
398
  LineChart.displayName = "LineChart";
315
399
  var CurveChart = import_react.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -331,8 +415,9 @@ var CurveChart = import_react.default.memo(({ data, labels, width, height, anima
331
415
  ),
332
416
  [entries, count, chartW, chartH, maxVal]
333
417
  );
334
- const showPoints = count <= 100;
335
418
  const lineRefs = import_react.default.useRef([]);
419
+ const curveClipRef = import_react.default.useRef(null);
420
+ const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
336
421
  import_react.default.useEffect(() => {
337
422
  if (!animate) return;
338
423
  lineRefs.current.forEach((el) => {
@@ -345,61 +430,110 @@ var CurveChart = import_react.default.memo(({ data, labels, width, height, anima
345
430
  el.style.strokeDashoffset = "0";
346
431
  });
347
432
  });
348
- }, [animate, seriesPoints]);
349
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
350
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GridLines, { width, height, chartH, maxVal }),
351
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxisLabels, { labels, count, chartW, height }),
352
- entries.map(([key], di) => {
353
- const palette = getPalette(LINE_BAR_PALETTES, di, key);
354
- const color = palette[2];
355
- const areaColor = palette[0];
356
- const points = seriesPoints[di];
357
- const gradientId = `curve-gradient-${di}`;
358
- const linePath = toSmoothPath(points);
359
- const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
360
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
361
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
362
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
363
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
364
- ] }) }),
365
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
366
- "path",
433
+ if (curveClipRef.current) {
434
+ curveClipRef.current.setAttribute("width", "0");
435
+ requestAnimationFrame(() => {
436
+ if (curveClipRef.current) {
437
+ curveClipRef.current.style.transition = "width 1200ms ease-out 200ms";
438
+ curveClipRef.current.setAttribute("width", `${width}`);
439
+ }
440
+ });
441
+ }
442
+ }, [animate, seriesPoints, width]);
443
+ const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
444
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
445
+ const curveClipId = "curve-area-clip";
446
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
447
+ "svg",
448
+ {
449
+ viewBox: `0 0 ${width} ${height}`,
450
+ className: "chart-svg",
451
+ onMouseMove: (e) => {
452
+ handleMouseMove(e);
453
+ onMove(e);
454
+ },
455
+ onMouseLeave: () => {
456
+ handleMouseLeave();
457
+ onLeave();
458
+ },
459
+ children: [
460
+ animate && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("clipPath", { id: curveClipId, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { ref: curveClipRef, x: "0", y: "0", width: animate ? 0 : width, height }) }) }),
461
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GridLines, { width, height, chartH, maxVal }),
462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AxisLabels, { labels, count, chartW, height }),
463
+ entries.map(([key], di) => {
464
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
465
+ const color = palette[2];
466
+ const areaColor = palette[0];
467
+ const points = seriesPoints[di];
468
+ const gradientId = `curve-gradient-${di}`;
469
+ const linePath = toSmoothPath(points);
470
+ const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
471
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { children: [
472
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
474
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
475
+ ] }) }),
476
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
477
+ "path",
478
+ {
479
+ d: areaPath,
480
+ fill: `url(#${gradientId})`,
481
+ clipPath: animate ? `url(#${curveClipId})` : void 0
482
+ }
483
+ ),
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
485
+ "path",
486
+ {
487
+ ref: (el) => {
488
+ lineRefs.current[di] = el;
489
+ },
490
+ d: linePath,
491
+ fill: "none",
492
+ stroke: color,
493
+ strokeWidth: "2"
494
+ }
495
+ ),
496
+ activeIndex !== null && points[activeIndex] && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
497
+ "circle",
498
+ {
499
+ cx: points[activeIndex].x,
500
+ cy: points[activeIndex].y,
501
+ r: "5",
502
+ fill: color,
503
+ className: "chart-point-active"
504
+ }
505
+ )
506
+ ] }, di);
507
+ }),
508
+ guideX !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
509
+ "line",
367
510
  {
368
- d: areaPath,
369
- fill: `url(#${gradientId})`,
370
- className: "chart-area",
371
- style: animate ? { animationDelay: "600ms" } : { opacity: 1 }
511
+ x1: guideX,
512
+ y1: PADDING.top,
513
+ x2: guideX,
514
+ y2: PADDING.top + chartH,
515
+ className: "chart-crosshair"
372
516
  }
373
517
  ),
518
+ activeIndex !== null && activeX !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "chart-crosshair-label", children: [
519
+ labels[activeIndex],
520
+ " \u2014 ",
521
+ tooltipContent
522
+ ] }) }),
374
523
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
375
- "path",
524
+ "rect",
376
525
  {
377
- ref: (el) => {
378
- lineRefs.current[di] = el;
379
- },
380
- d: linePath,
381
- fill: "none",
382
- stroke: color,
383
- strokeWidth: "2"
526
+ x: PADDING.left,
527
+ y: PADDING.top,
528
+ width: chartW,
529
+ height: chartH,
530
+ fill: "transparent",
531
+ style: { cursor: "crosshair" }
384
532
  }
385
- ),
386
- showPoints && points.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
387
- "circle",
388
- {
389
- cx: p.x,
390
- cy: p.y,
391
- r: "4",
392
- fill: color,
393
- className: "chart-point",
394
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
395
- onMouseMove: onMove,
396
- onMouseLeave: onLeave
397
- },
398
- i
399
- ))
400
- ] }, di);
401
- })
402
- ] });
533
+ )
534
+ ]
535
+ }
536
+ );
403
537
  });
404
538
  CurveChart.displayName = "CurveChart";
405
539
  var BarChart = import_react.default.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
@@ -74,6 +74,24 @@
74
74
  .lib-xplat-chart .chart-area {
75
75
  opacity: 1;
76
76
  }
77
+ .lib-xplat-chart .chart-crosshair {
78
+ stroke: var(--semantic-border-subtle);
79
+ stroke-width: 1;
80
+ stroke-dasharray: 4 2;
81
+ pointer-events: none;
82
+ }
83
+ .lib-xplat-chart .chart-point-active {
84
+ pointer-events: none;
85
+ transition: cx 0.05s, cy 0.05s;
86
+ }
87
+ .lib-xplat-chart .chart-crosshair-label {
88
+ font-size: 11px;
89
+ font-weight: 500;
90
+ color: var(--semantic-text-strong);
91
+ text-align: center;
92
+ white-space: nowrap;
93
+ overflow: visible;
94
+ }
77
95
  .lib-xplat-chart .chart-tooltip {
78
96
  position: absolute;
79
97
  transform: translate(-50%, -100%);