@x-plat/design-system 0.5.13 → 0.5.15

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.
@@ -80,26 +80,47 @@ var toSmoothPath = (points) => {
80
80
  }
81
81
  return d;
82
82
  };
83
+ var RESIZE_SETTLE_MS = 150;
83
84
  var useChartSize = (ref) => {
84
85
  const [size, setSize] = import_react.default.useState({ width: 0, height: 0 });
86
+ const settleTimer = import_react.default.useRef(0);
87
+ const committedSize = import_react.default.useRef({ width: 0, height: 0 });
88
+ const initialRef = import_react.default.useRef(true);
85
89
  import_react.default.useEffect(() => {
86
90
  const el = ref.current;
87
91
  if (!el) return;
88
- let rafId = 0;
89
92
  const observer = new ResizeObserver((entries) => {
90
- cancelAnimationFrame(rafId);
91
- rafId = requestAnimationFrame(() => {
92
- const entry = entries[0];
93
- if (!entry) return;
94
- const { width, height } = entry.contentRect;
95
- const w = Math.floor(width);
96
- const h = Math.floor(height);
97
- setSize((prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h });
98
- });
93
+ const entry = entries[0];
94
+ if (!entry) return;
95
+ const { width, height } = entry.contentRect;
96
+ const w = Math.floor(width);
97
+ const h = Math.floor(height);
98
+ if (w <= 0 || h <= 0) return;
99
+ if (w === committedSize.current.width && h === committedSize.current.height) return;
100
+ if (initialRef.current) {
101
+ initialRef.current = false;
102
+ committedSize.current = { width: w, height: h };
103
+ setSize({ width: w, height: h });
104
+ return;
105
+ }
106
+ const prev = committedSize.current;
107
+ if (el.firstElementChild && prev.width > 0 && prev.height > 0) {
108
+ const svg = el.firstElementChild;
109
+ svg.style.transformOrigin = "0 0";
110
+ svg.style.transform = `scale(${w / prev.width}, ${h / prev.height})`;
111
+ }
112
+ window.clearTimeout(settleTimer.current);
113
+ settleTimer.current = window.setTimeout(() => {
114
+ if (el.firstElementChild) {
115
+ el.firstElementChild.style.transform = "";
116
+ }
117
+ committedSize.current = { width: w, height: h };
118
+ setSize({ width: w, height: h });
119
+ }, RESIZE_SETTLE_MS);
99
120
  });
100
121
  observer.observe(el);
101
122
  return () => {
102
- cancelAnimationFrame(rafId);
123
+ window.clearTimeout(settleTimer.current);
103
124
  observer.disconnect();
104
125
  };
105
126
  }, [ref]);
@@ -294,17 +315,19 @@ var BarChart = import_react.default.memo(({ data, labels, width, height, onHover
294
315
  const chartW = width - PADDING.left - PADDING.right;
295
316
  const chartH = height - PADDING.top - PADDING.bottom;
296
317
  const groupW = chartW / count;
297
- const barW = Math.max(1, Math.min(32, groupW * 0.7 / groupCount));
318
+ const barGap = groupCount > 1 ? 2 : 0;
319
+ const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
298
320
  const bars = import_react.default.useMemo(
299
321
  () => entries.map(
300
322
  ([, values], di) => values.map((v, i) => {
323
+ const totalBarsW = barW * groupCount + barGap * (groupCount - 1);
301
324
  const h = Math.max(0, v / maxVal * chartH);
302
- const x = PADDING.left + groupW * i + (groupW - barW * groupCount) / 2 + barW * di;
325
+ const x = PADDING.left + groupW * i + (groupW - totalBarsW) / 2 + (barW + barGap) * di;
303
326
  const y = PADDING.top + chartH - h;
304
327
  return { x, y, w: barW, h, v };
305
328
  })
306
329
  ),
307
- [entries, maxVal, chartH, groupW, barW, groupCount]
330
+ [entries, maxVal, chartH, groupW, barW, barGap, groupCount]
308
331
  );
309
332
  const barLabelStep = getLabelStep(count, chartW);
310
333
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
@@ -316,22 +339,22 @@ var BarChart = import_react.default.memo(({ data, labels, width, height, onHover
316
339
  entries.map(([key], di) => {
317
340
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
318
341
  const color = palette[2];
319
- return bars[di].map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
320
- "rect",
321
- {
322
- x: b.x,
323
- y: b.y,
324
- width: b.w,
325
- height: b.h,
326
- rx: Math.min(4, b.w / 2),
327
- fill: color,
328
- className: "chart-bar",
329
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
330
- onMouseMove: onMove,
331
- onMouseLeave: onLeave
332
- },
333
- `${di}-${i}`
334
- ));
342
+ return bars[di].map((b, i) => {
343
+ const r = Math.min(4, b.w / 2);
344
+ const d = b.h <= r ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r} Q ${b.x} ${b.y} ${b.x + r} ${b.y} H ${b.x + b.w - r} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r} V ${b.y + b.h} Z`;
345
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
346
+ "path",
347
+ {
348
+ d,
349
+ fill: color,
350
+ className: "chart-bar",
351
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
352
+ onMouseMove: onMove,
353
+ onMouseLeave: onLeave
354
+ },
355
+ `${di}-${i}`
356
+ );
357
+ });
335
358
  })
336
359
  ] });
337
360
  });
@@ -418,20 +441,22 @@ var TooltipBubble = ({ x, y, containerWidth, children }) => {
418
441
  }
419
442
  );
420
443
  };
421
- var Chart = (props) => {
444
+ var Chart = import_react.default.memo((props) => {
422
445
  const { type, data, labels, tooltip: showTooltip = true } = props;
423
446
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
424
447
  const { width, height } = useChartSize(containerRef);
448
+ const stableData = import_react.default.useMemo(() => data, [JSON.stringify(data)]);
449
+ const stableLabels = import_react.default.useMemo(() => labels, [JSON.stringify(labels)]);
425
450
  const ready = width > 0 && height > 0;
426
451
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "lib-xplat-chart", ref: containerRef, children: [
427
- ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
428
- ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
429
- ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
430
- ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
431
- ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
452
+ ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
453
+ ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
454
+ ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
455
+ ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
456
+ ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
432
457
  tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
433
458
  ] });
434
- };
459
+ });
435
460
  Chart.displayName = "Chart";
436
461
  var Chart_default = Chart;
437
462
  // Annotate the CommonJS export names for ESM import in node:
@@ -9,6 +9,8 @@
9
9
  display: block;
10
10
  width: 100%;
11
11
  height: 100%;
12
+ will-change: transform;
13
+ contain: layout style paint;
12
14
  }
13
15
  .lib-xplat-chart .chart-grid {
14
16
  stroke: var(--semantic-border-subtle);
@@ -6,6 +6,6 @@ interface ChartProps {
6
6
  labels: string[];
7
7
  tooltip?: boolean;
8
8
  }
9
- declare const Chart: React.FC<ChartProps>;
9
+ declare const Chart: React.NamedExoticComponent<ChartProps>;
10
10
 
11
11
  export { Chart };
@@ -6,6 +6,6 @@ interface ChartProps {
6
6
  labels: string[];
7
7
  tooltip?: boolean;
8
8
  }
9
- declare const Chart: React.FC<ChartProps>;
9
+ declare const Chart: React.NamedExoticComponent<ChartProps>;
10
10
 
11
11
  export { Chart };
@@ -44,26 +44,47 @@ var toSmoothPath = (points) => {
44
44
  }
45
45
  return d;
46
46
  };
47
+ var RESIZE_SETTLE_MS = 150;
47
48
  var useChartSize = (ref) => {
48
49
  const [size, setSize] = React.useState({ width: 0, height: 0 });
50
+ const settleTimer = React.useRef(0);
51
+ const committedSize = React.useRef({ width: 0, height: 0 });
52
+ const initialRef = React.useRef(true);
49
53
  React.useEffect(() => {
50
54
  const el = ref.current;
51
55
  if (!el) return;
52
- let rafId = 0;
53
56
  const observer = new ResizeObserver((entries) => {
54
- cancelAnimationFrame(rafId);
55
- rafId = requestAnimationFrame(() => {
56
- const entry = entries[0];
57
- if (!entry) return;
58
- const { width, height } = entry.contentRect;
59
- const w = Math.floor(width);
60
- const h = Math.floor(height);
61
- setSize((prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h });
62
- });
57
+ const entry = entries[0];
58
+ if (!entry) return;
59
+ const { width, height } = entry.contentRect;
60
+ const w = Math.floor(width);
61
+ const h = Math.floor(height);
62
+ if (w <= 0 || h <= 0) return;
63
+ if (w === committedSize.current.width && h === committedSize.current.height) return;
64
+ if (initialRef.current) {
65
+ initialRef.current = false;
66
+ committedSize.current = { width: w, height: h };
67
+ setSize({ width: w, height: h });
68
+ return;
69
+ }
70
+ const prev = committedSize.current;
71
+ if (el.firstElementChild && prev.width > 0 && prev.height > 0) {
72
+ const svg = el.firstElementChild;
73
+ svg.style.transformOrigin = "0 0";
74
+ svg.style.transform = `scale(${w / prev.width}, ${h / prev.height})`;
75
+ }
76
+ window.clearTimeout(settleTimer.current);
77
+ settleTimer.current = window.setTimeout(() => {
78
+ if (el.firstElementChild) {
79
+ el.firstElementChild.style.transform = "";
80
+ }
81
+ committedSize.current = { width: w, height: h };
82
+ setSize({ width: w, height: h });
83
+ }, RESIZE_SETTLE_MS);
63
84
  });
64
85
  observer.observe(el);
65
86
  return () => {
66
- cancelAnimationFrame(rafId);
87
+ window.clearTimeout(settleTimer.current);
67
88
  observer.disconnect();
68
89
  };
69
90
  }, [ref]);
@@ -258,17 +279,19 @@ var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLea
258
279
  const chartW = width - PADDING.left - PADDING.right;
259
280
  const chartH = height - PADDING.top - PADDING.bottom;
260
281
  const groupW = chartW / count;
261
- const barW = Math.max(1, Math.min(32, groupW * 0.7 / groupCount));
282
+ const barGap = groupCount > 1 ? 2 : 0;
283
+ const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
262
284
  const bars = React.useMemo(
263
285
  () => entries.map(
264
286
  ([, values], di) => values.map((v, i) => {
287
+ const totalBarsW = barW * groupCount + barGap * (groupCount - 1);
265
288
  const h = Math.max(0, v / maxVal * chartH);
266
- const x = PADDING.left + groupW * i + (groupW - barW * groupCount) / 2 + barW * di;
289
+ const x = PADDING.left + groupW * i + (groupW - totalBarsW) / 2 + (barW + barGap) * di;
267
290
  const y = PADDING.top + chartH - h;
268
291
  return { x, y, w: barW, h, v };
269
292
  })
270
293
  ),
271
- [entries, maxVal, chartH, groupW, barW, groupCount]
294
+ [entries, maxVal, chartH, groupW, barW, barGap, groupCount]
272
295
  );
273
296
  const barLabelStep = getLabelStep(count, chartW);
274
297
  return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
@@ -280,22 +303,22 @@ var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLea
280
303
  entries.map(([key], di) => {
281
304
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
282
305
  const color = palette[2];
283
- return bars[di].map((b, i) => /* @__PURE__ */ jsx(
284
- "rect",
285
- {
286
- x: b.x,
287
- y: b.y,
288
- width: b.w,
289
- height: b.h,
290
- rx: Math.min(4, b.w / 2),
291
- fill: color,
292
- className: "chart-bar",
293
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
294
- onMouseMove: onMove,
295
- onMouseLeave: onLeave
296
- },
297
- `${di}-${i}`
298
- ));
306
+ return bars[di].map((b, i) => {
307
+ const r = Math.min(4, b.w / 2);
308
+ const d = b.h <= r ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r} Q ${b.x} ${b.y} ${b.x + r} ${b.y} H ${b.x + b.w - r} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r} V ${b.y + b.h} Z`;
309
+ return /* @__PURE__ */ jsx(
310
+ "path",
311
+ {
312
+ d,
313
+ fill: color,
314
+ className: "chart-bar",
315
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
316
+ onMouseMove: onMove,
317
+ onMouseLeave: onLeave
318
+ },
319
+ `${di}-${i}`
320
+ );
321
+ });
299
322
  })
300
323
  ] });
301
324
  });
@@ -382,20 +405,22 @@ var TooltipBubble = ({ x, y, containerWidth, children }) => {
382
405
  }
383
406
  );
384
407
  };
385
- var Chart = (props) => {
408
+ var Chart = React.memo((props) => {
386
409
  const { type, data, labels, tooltip: showTooltip = true } = props;
387
410
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
388
411
  const { width, height } = useChartSize(containerRef);
412
+ const stableData = React.useMemo(() => data, [JSON.stringify(data)]);
413
+ const stableLabels = React.useMemo(() => labels, [JSON.stringify(labels)]);
389
414
  const ready = width > 0 && height > 0;
390
415
  return /* @__PURE__ */ jsxs("div", { className: "lib-xplat-chart", ref: containerRef, children: [
391
- ready && type === "line" && /* @__PURE__ */ jsx(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
392
- ready && type === "curve" && /* @__PURE__ */ jsx(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
393
- ready && type === "bar" && /* @__PURE__ */ jsx(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
394
- ready && type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
395
- ready && type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
416
+ ready && type === "line" && /* @__PURE__ */ jsx(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
417
+ ready && type === "curve" && /* @__PURE__ */ jsx(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
418
+ ready && type === "bar" && /* @__PURE__ */ jsx(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
419
+ ready && type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
420
+ ready && type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
396
421
  tooltip.visible && /* @__PURE__ */ jsx(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
397
422
  ] });
398
- };
423
+ });
399
424
  Chart.displayName = "Chart";
400
425
  var Chart_default = Chart;
401
426
  export {
@@ -2151,26 +2151,47 @@ var toSmoothPath = (points) => {
2151
2151
  }
2152
2152
  return d;
2153
2153
  };
2154
+ var RESIZE_SETTLE_MS = 150;
2154
2155
  var useChartSize = (ref) => {
2155
2156
  const [size, setSize] = import_react5.default.useState({ width: 0, height: 0 });
2157
+ const settleTimer = import_react5.default.useRef(0);
2158
+ const committedSize = import_react5.default.useRef({ width: 0, height: 0 });
2159
+ const initialRef = import_react5.default.useRef(true);
2156
2160
  import_react5.default.useEffect(() => {
2157
2161
  const el = ref.current;
2158
2162
  if (!el) return;
2159
- let rafId = 0;
2160
2163
  const observer = new ResizeObserver((entries) => {
2161
- cancelAnimationFrame(rafId);
2162
- rafId = requestAnimationFrame(() => {
2163
- const entry = entries[0];
2164
- if (!entry) return;
2165
- const { width, height } = entry.contentRect;
2166
- const w = Math.floor(width);
2167
- const h = Math.floor(height);
2168
- setSize((prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h });
2169
- });
2164
+ const entry = entries[0];
2165
+ if (!entry) return;
2166
+ const { width, height } = entry.contentRect;
2167
+ const w = Math.floor(width);
2168
+ const h = Math.floor(height);
2169
+ if (w <= 0 || h <= 0) return;
2170
+ if (w === committedSize.current.width && h === committedSize.current.height) return;
2171
+ if (initialRef.current) {
2172
+ initialRef.current = false;
2173
+ committedSize.current = { width: w, height: h };
2174
+ setSize({ width: w, height: h });
2175
+ return;
2176
+ }
2177
+ const prev = committedSize.current;
2178
+ if (el.firstElementChild && prev.width > 0 && prev.height > 0) {
2179
+ const svg = el.firstElementChild;
2180
+ svg.style.transformOrigin = "0 0";
2181
+ svg.style.transform = `scale(${w / prev.width}, ${h / prev.height})`;
2182
+ }
2183
+ window.clearTimeout(settleTimer.current);
2184
+ settleTimer.current = window.setTimeout(() => {
2185
+ if (el.firstElementChild) {
2186
+ el.firstElementChild.style.transform = "";
2187
+ }
2188
+ committedSize.current = { width: w, height: h };
2189
+ setSize({ width: w, height: h });
2190
+ }, RESIZE_SETTLE_MS);
2170
2191
  });
2171
2192
  observer.observe(el);
2172
2193
  return () => {
2173
- cancelAnimationFrame(rafId);
2194
+ window.clearTimeout(settleTimer.current);
2174
2195
  observer.disconnect();
2175
2196
  };
2176
2197
  }, [ref]);
@@ -2365,17 +2386,19 @@ var BarChart = import_react5.default.memo(({ data, labels, width, height, onHove
2365
2386
  const chartW = width - PADDING.left - PADDING.right;
2366
2387
  const chartH = height - PADDING.top - PADDING.bottom;
2367
2388
  const groupW = chartW / count;
2368
- const barW = Math.max(1, Math.min(32, groupW * 0.7 / groupCount));
2389
+ const barGap = groupCount > 1 ? 2 : 0;
2390
+ const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
2369
2391
  const bars = import_react5.default.useMemo(
2370
2392
  () => entries.map(
2371
2393
  ([, values], di) => values.map((v, i) => {
2394
+ const totalBarsW = barW * groupCount + barGap * (groupCount - 1);
2372
2395
  const h = Math.max(0, v / maxVal * chartH);
2373
- const x = PADDING.left + groupW * i + (groupW - barW * groupCount) / 2 + barW * di;
2396
+ const x = PADDING.left + groupW * i + (groupW - totalBarsW) / 2 + (barW + barGap) * di;
2374
2397
  const y = PADDING.top + chartH - h;
2375
2398
  return { x, y, w: barW, h, v };
2376
2399
  })
2377
2400
  ),
2378
- [entries, maxVal, chartH, groupW, barW, groupCount]
2401
+ [entries, maxVal, chartH, groupW, barW, barGap, groupCount]
2379
2402
  );
2380
2403
  const barLabelStep = getLabelStep(count, chartW);
2381
2404
  return /* @__PURE__ */ (0, import_jsx_runtime305.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
@@ -2387,22 +2410,22 @@ var BarChart = import_react5.default.memo(({ data, labels, width, height, onHove
2387
2410
  entries.map(([key], di) => {
2388
2411
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
2389
2412
  const color = palette[2];
2390
- return bars[di].map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(
2391
- "rect",
2392
- {
2393
- x: b.x,
2394
- y: b.y,
2395
- width: b.w,
2396
- height: b.h,
2397
- rx: Math.min(4, b.w / 2),
2398
- fill: color,
2399
- className: "chart-bar",
2400
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
2401
- onMouseMove: onMove,
2402
- onMouseLeave: onLeave
2403
- },
2404
- `${di}-${i}`
2405
- ));
2413
+ return bars[di].map((b, i) => {
2414
+ const r2 = Math.min(4, b.w / 2);
2415
+ const d = b.h <= r2 ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r2} Q ${b.x} ${b.y} ${b.x + r2} ${b.y} H ${b.x + b.w - r2} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r2} V ${b.y + b.h} Z`;
2416
+ return /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(
2417
+ "path",
2418
+ {
2419
+ d,
2420
+ fill: color,
2421
+ className: "chart-bar",
2422
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
2423
+ onMouseMove: onMove,
2424
+ onMouseLeave: onLeave
2425
+ },
2426
+ `${di}-${i}`
2427
+ );
2428
+ });
2406
2429
  })
2407
2430
  ] });
2408
2431
  });
@@ -2489,20 +2512,22 @@ var TooltipBubble = ({ x, y, containerWidth, children }) => {
2489
2512
  }
2490
2513
  );
2491
2514
  };
2492
- var Chart = (props) => {
2515
+ var Chart = import_react5.default.memo((props) => {
2493
2516
  const { type, data, labels, tooltip: showTooltip = true } = props;
2494
2517
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
2495
2518
  const { width, height } = useChartSize(containerRef);
2519
+ const stableData = import_react5.default.useMemo(() => data, [JSON.stringify(data)]);
2520
+ const stableLabels = import_react5.default.useMemo(() => labels, [JSON.stringify(labels)]);
2496
2521
  const ready = width > 0 && height > 0;
2497
2522
  return /* @__PURE__ */ (0, import_jsx_runtime305.jsxs)("div", { className: "lib-xplat-chart", ref: containerRef, children: [
2498
- ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2499
- ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2500
- ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2501
- ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2502
- ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
2523
+ ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2524
+ ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2525
+ ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2526
+ ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2527
+ ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
2503
2528
  tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
2504
2529
  ] });
2505
- };
2530
+ });
2506
2531
  Chart.displayName = "Chart";
2507
2532
  var Chart_default = Chart;
2508
2533
 
@@ -1810,6 +1810,8 @@
1810
1810
  display: block;
1811
1811
  width: 100%;
1812
1812
  height: 100%;
1813
+ will-change: transform;
1814
+ contain: layout style paint;
1813
1815
  }
1814
1816
  .lib-xplat-chart .chart-grid {
1815
1817
  stroke: var(--semantic-border-subtle);
@@ -2063,26 +2063,47 @@ var toSmoothPath = (points) => {
2063
2063
  }
2064
2064
  return d;
2065
2065
  };
2066
+ var RESIZE_SETTLE_MS = 150;
2066
2067
  var useChartSize = (ref) => {
2067
2068
  const [size, setSize] = React5.useState({ width: 0, height: 0 });
2069
+ const settleTimer = React5.useRef(0);
2070
+ const committedSize = React5.useRef({ width: 0, height: 0 });
2071
+ const initialRef = React5.useRef(true);
2068
2072
  React5.useEffect(() => {
2069
2073
  const el = ref.current;
2070
2074
  if (!el) return;
2071
- let rafId = 0;
2072
2075
  const observer = new ResizeObserver((entries) => {
2073
- cancelAnimationFrame(rafId);
2074
- rafId = requestAnimationFrame(() => {
2075
- const entry = entries[0];
2076
- if (!entry) return;
2077
- const { width, height } = entry.contentRect;
2078
- const w = Math.floor(width);
2079
- const h = Math.floor(height);
2080
- setSize((prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h });
2081
- });
2076
+ const entry = entries[0];
2077
+ if (!entry) return;
2078
+ const { width, height } = entry.contentRect;
2079
+ const w = Math.floor(width);
2080
+ const h = Math.floor(height);
2081
+ if (w <= 0 || h <= 0) return;
2082
+ if (w === committedSize.current.width && h === committedSize.current.height) return;
2083
+ if (initialRef.current) {
2084
+ initialRef.current = false;
2085
+ committedSize.current = { width: w, height: h };
2086
+ setSize({ width: w, height: h });
2087
+ return;
2088
+ }
2089
+ const prev = committedSize.current;
2090
+ if (el.firstElementChild && prev.width > 0 && prev.height > 0) {
2091
+ const svg = el.firstElementChild;
2092
+ svg.style.transformOrigin = "0 0";
2093
+ svg.style.transform = `scale(${w / prev.width}, ${h / prev.height})`;
2094
+ }
2095
+ window.clearTimeout(settleTimer.current);
2096
+ settleTimer.current = window.setTimeout(() => {
2097
+ if (el.firstElementChild) {
2098
+ el.firstElementChild.style.transform = "";
2099
+ }
2100
+ committedSize.current = { width: w, height: h };
2101
+ setSize({ width: w, height: h });
2102
+ }, RESIZE_SETTLE_MS);
2082
2103
  });
2083
2104
  observer.observe(el);
2084
2105
  return () => {
2085
- cancelAnimationFrame(rafId);
2106
+ window.clearTimeout(settleTimer.current);
2086
2107
  observer.disconnect();
2087
2108
  };
2088
2109
  }, [ref]);
@@ -2277,17 +2298,19 @@ var BarChart = React5.memo(({ data, labels, width, height, onHover, onMove, onLe
2277
2298
  const chartW = width - PADDING.left - PADDING.right;
2278
2299
  const chartH = height - PADDING.top - PADDING.bottom;
2279
2300
  const groupW = chartW / count;
2280
- const barW = Math.max(1, Math.min(32, groupW * 0.7 / groupCount));
2301
+ const barGap = groupCount > 1 ? 2 : 0;
2302
+ const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
2281
2303
  const bars = React5.useMemo(
2282
2304
  () => entries.map(
2283
2305
  ([, values], di) => values.map((v, i) => {
2306
+ const totalBarsW = barW * groupCount + barGap * (groupCount - 1);
2284
2307
  const h = Math.max(0, v / maxVal * chartH);
2285
- const x = PADDING.left + groupW * i + (groupW - barW * groupCount) / 2 + barW * di;
2308
+ const x = PADDING.left + groupW * i + (groupW - totalBarsW) / 2 + (barW + barGap) * di;
2286
2309
  const y = PADDING.top + chartH - h;
2287
2310
  return { x, y, w: barW, h, v };
2288
2311
  })
2289
2312
  ),
2290
- [entries, maxVal, chartH, groupW, barW, groupCount]
2313
+ [entries, maxVal, chartH, groupW, barW, barGap, groupCount]
2291
2314
  );
2292
2315
  const barLabelStep = getLabelStep(count, chartW);
2293
2316
  return /* @__PURE__ */ jsxs196("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
@@ -2299,22 +2322,22 @@ var BarChart = React5.memo(({ data, labels, width, height, onHover, onMove, onLe
2299
2322
  entries.map(([key], di) => {
2300
2323
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
2301
2324
  const color = palette[2];
2302
- return bars[di].map((b, i) => /* @__PURE__ */ jsx305(
2303
- "rect",
2304
- {
2305
- x: b.x,
2306
- y: b.y,
2307
- width: b.w,
2308
- height: b.h,
2309
- rx: Math.min(4, b.w / 2),
2310
- fill: color,
2311
- className: "chart-bar",
2312
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
2313
- onMouseMove: onMove,
2314
- onMouseLeave: onLeave
2315
- },
2316
- `${di}-${i}`
2317
- ));
2325
+ return bars[di].map((b, i) => {
2326
+ const r2 = Math.min(4, b.w / 2);
2327
+ const d = b.h <= r2 ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r2} Q ${b.x} ${b.y} ${b.x + r2} ${b.y} H ${b.x + b.w - r2} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r2} V ${b.y + b.h} Z`;
2328
+ return /* @__PURE__ */ jsx305(
2329
+ "path",
2330
+ {
2331
+ d,
2332
+ fill: color,
2333
+ className: "chart-bar",
2334
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
2335
+ onMouseMove: onMove,
2336
+ onMouseLeave: onLeave
2337
+ },
2338
+ `${di}-${i}`
2339
+ );
2340
+ });
2318
2341
  })
2319
2342
  ] });
2320
2343
  });
@@ -2401,20 +2424,22 @@ var TooltipBubble = ({ x, y, containerWidth, children }) => {
2401
2424
  }
2402
2425
  );
2403
2426
  };
2404
- var Chart = (props) => {
2427
+ var Chart = React5.memo((props) => {
2405
2428
  const { type, data, labels, tooltip: showTooltip = true } = props;
2406
2429
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
2407
2430
  const { width, height } = useChartSize(containerRef);
2431
+ const stableData = React5.useMemo(() => data, [JSON.stringify(data)]);
2432
+ const stableLabels = React5.useMemo(() => labels, [JSON.stringify(labels)]);
2408
2433
  const ready = width > 0 && height > 0;
2409
2434
  return /* @__PURE__ */ jsxs196("div", { className: "lib-xplat-chart", ref: containerRef, children: [
2410
- ready && type === "line" && /* @__PURE__ */ jsx305(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2411
- ready && type === "curve" && /* @__PURE__ */ jsx305(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2412
- ready && type === "bar" && /* @__PURE__ */ jsx305(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2413
- ready && type === "pie" && /* @__PURE__ */ jsx305(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
2414
- ready && type === "doughnut" && /* @__PURE__ */ jsx305(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
2435
+ ready && type === "line" && /* @__PURE__ */ jsx305(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2436
+ ready && type === "curve" && /* @__PURE__ */ jsx305(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2437
+ ready && type === "bar" && /* @__PURE__ */ jsx305(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2438
+ ready && type === "pie" && /* @__PURE__ */ jsx305(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
2439
+ ready && type === "doughnut" && /* @__PURE__ */ jsx305(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
2415
2440
  tooltip.visible && /* @__PURE__ */ jsx305(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
2416
2441
  ] });
2417
- };
2442
+ });
2418
2443
  Chart.displayName = "Chart";
2419
2444
  var Chart_default = Chart;
2420
2445
 
package/dist/index.cjs CHANGED
@@ -6562,26 +6562,47 @@ var toSmoothPath = (points) => {
6562
6562
  }
6563
6563
  return d;
6564
6564
  };
6565
+ var RESIZE_SETTLE_MS = 150;
6565
6566
  var useChartSize = (ref) => {
6566
6567
  const [size, setSize] = import_react5.default.useState({ width: 0, height: 0 });
6568
+ const settleTimer = import_react5.default.useRef(0);
6569
+ const committedSize = import_react5.default.useRef({ width: 0, height: 0 });
6570
+ const initialRef = import_react5.default.useRef(true);
6567
6571
  import_react5.default.useEffect(() => {
6568
6572
  const el = ref.current;
6569
6573
  if (!el) return;
6570
- let rafId = 0;
6571
6574
  const observer = new ResizeObserver((entries) => {
6572
- cancelAnimationFrame(rafId);
6573
- rafId = requestAnimationFrame(() => {
6574
- const entry = entries[0];
6575
- if (!entry) return;
6576
- const { width, height } = entry.contentRect;
6577
- const w = Math.floor(width);
6578
- const h = Math.floor(height);
6579
- setSize((prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h });
6580
- });
6575
+ const entry = entries[0];
6576
+ if (!entry) return;
6577
+ const { width, height } = entry.contentRect;
6578
+ const w = Math.floor(width);
6579
+ const h = Math.floor(height);
6580
+ if (w <= 0 || h <= 0) return;
6581
+ if (w === committedSize.current.width && h === committedSize.current.height) return;
6582
+ if (initialRef.current) {
6583
+ initialRef.current = false;
6584
+ committedSize.current = { width: w, height: h };
6585
+ setSize({ width: w, height: h });
6586
+ return;
6587
+ }
6588
+ const prev = committedSize.current;
6589
+ if (el.firstElementChild && prev.width > 0 && prev.height > 0) {
6590
+ const svg = el.firstElementChild;
6591
+ svg.style.transformOrigin = "0 0";
6592
+ svg.style.transform = `scale(${w / prev.width}, ${h / prev.height})`;
6593
+ }
6594
+ window.clearTimeout(settleTimer.current);
6595
+ settleTimer.current = window.setTimeout(() => {
6596
+ if (el.firstElementChild) {
6597
+ el.firstElementChild.style.transform = "";
6598
+ }
6599
+ committedSize.current = { width: w, height: h };
6600
+ setSize({ width: w, height: h });
6601
+ }, RESIZE_SETTLE_MS);
6581
6602
  });
6582
6603
  observer.observe(el);
6583
6604
  return () => {
6584
- cancelAnimationFrame(rafId);
6605
+ window.clearTimeout(settleTimer.current);
6585
6606
  observer.disconnect();
6586
6607
  };
6587
6608
  }, [ref]);
@@ -6776,17 +6797,19 @@ var BarChart = import_react5.default.memo(({ data, labels, width, height, onHove
6776
6797
  const chartW = width - PADDING.left - PADDING.right;
6777
6798
  const chartH = height - PADDING.top - PADDING.bottom;
6778
6799
  const groupW = chartW / count;
6779
- const barW = Math.max(1, Math.min(32, groupW * 0.7 / groupCount));
6800
+ const barGap = groupCount > 1 ? 2 : 0;
6801
+ const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
6780
6802
  const bars = import_react5.default.useMemo(
6781
6803
  () => entries.map(
6782
6804
  ([, values], di) => values.map((v, i) => {
6805
+ const totalBarsW = barW * groupCount + barGap * (groupCount - 1);
6783
6806
  const h = Math.max(0, v / maxVal * chartH);
6784
- const x = PADDING.left + groupW * i + (groupW - barW * groupCount) / 2 + barW * di;
6807
+ const x = PADDING.left + groupW * i + (groupW - totalBarsW) / 2 + (barW + barGap) * di;
6785
6808
  const y = PADDING.top + chartH - h;
6786
6809
  return { x, y, w: barW, h, v };
6787
6810
  })
6788
6811
  ),
6789
- [entries, maxVal, chartH, groupW, barW, groupCount]
6812
+ [entries, maxVal, chartH, groupW, barW, barGap, groupCount]
6790
6813
  );
6791
6814
  const barLabelStep = getLabelStep(count, chartW);
6792
6815
  return /* @__PURE__ */ (0, import_jsx_runtime305.jsxs)("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
@@ -6798,22 +6821,22 @@ var BarChart = import_react5.default.memo(({ data, labels, width, height, onHove
6798
6821
  entries.map(([key], di) => {
6799
6822
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
6800
6823
  const color = palette[2];
6801
- return bars[di].map((b, i) => /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(
6802
- "rect",
6803
- {
6804
- x: b.x,
6805
- y: b.y,
6806
- width: b.w,
6807
- height: b.h,
6808
- rx: Math.min(4, b.w / 2),
6809
- fill: color,
6810
- className: "chart-bar",
6811
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
6812
- onMouseMove: onMove,
6813
- onMouseLeave: onLeave
6814
- },
6815
- `${di}-${i}`
6816
- ));
6824
+ return bars[di].map((b, i) => {
6825
+ const r2 = Math.min(4, b.w / 2);
6826
+ const d = b.h <= r2 ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r2} Q ${b.x} ${b.y} ${b.x + r2} ${b.y} H ${b.x + b.w - r2} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r2} V ${b.y + b.h} Z`;
6827
+ return /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(
6828
+ "path",
6829
+ {
6830
+ d,
6831
+ fill: color,
6832
+ className: "chart-bar",
6833
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
6834
+ onMouseMove: onMove,
6835
+ onMouseLeave: onLeave
6836
+ },
6837
+ `${di}-${i}`
6838
+ );
6839
+ });
6817
6840
  })
6818
6841
  ] });
6819
6842
  });
@@ -6900,20 +6923,22 @@ var TooltipBubble = ({ x, y, containerWidth, children }) => {
6900
6923
  }
6901
6924
  );
6902
6925
  };
6903
- var Chart = (props) => {
6926
+ var Chart = import_react5.default.memo((props) => {
6904
6927
  const { type, data, labels, tooltip: showTooltip = true } = props;
6905
6928
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
6906
6929
  const { width, height } = useChartSize(containerRef);
6930
+ const stableData = import_react5.default.useMemo(() => data, [JSON.stringify(data)]);
6931
+ const stableLabels = import_react5.default.useMemo(() => labels, [JSON.stringify(labels)]);
6907
6932
  const ready = width > 0 && height > 0;
6908
6933
  return /* @__PURE__ */ (0, import_jsx_runtime305.jsxs)("div", { className: "lib-xplat-chart", ref: containerRef, children: [
6909
- ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6910
- ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6911
- ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6912
- ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6913
- ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
6934
+ ready && type === "line" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6935
+ ready && type === "curve" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6936
+ ready && type === "bar" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6937
+ ready && type === "pie" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6938
+ ready && type === "doughnut" && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
6914
6939
  tooltip.visible && /* @__PURE__ */ (0, import_jsx_runtime305.jsx)(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
6915
6940
  ] });
6916
- };
6941
+ });
6917
6942
  Chart.displayName = "Chart";
6918
6943
  var Chart_default = Chart;
6919
6944
 
package/dist/index.css CHANGED
@@ -1810,6 +1810,8 @@
1810
1810
  display: block;
1811
1811
  width: 100%;
1812
1812
  height: 100%;
1813
+ will-change: transform;
1814
+ contain: layout style paint;
1813
1815
  }
1814
1816
  .lib-xplat-chart .chart-grid {
1815
1817
  stroke: var(--semantic-border-subtle);
package/dist/index.js CHANGED
@@ -6165,26 +6165,47 @@ var toSmoothPath = (points) => {
6165
6165
  }
6166
6166
  return d;
6167
6167
  };
6168
+ var RESIZE_SETTLE_MS = 150;
6168
6169
  var useChartSize = (ref) => {
6169
6170
  const [size, setSize] = React5.useState({ width: 0, height: 0 });
6171
+ const settleTimer = React5.useRef(0);
6172
+ const committedSize = React5.useRef({ width: 0, height: 0 });
6173
+ const initialRef = React5.useRef(true);
6170
6174
  React5.useEffect(() => {
6171
6175
  const el = ref.current;
6172
6176
  if (!el) return;
6173
- let rafId = 0;
6174
6177
  const observer = new ResizeObserver((entries) => {
6175
- cancelAnimationFrame(rafId);
6176
- rafId = requestAnimationFrame(() => {
6177
- const entry = entries[0];
6178
- if (!entry) return;
6179
- const { width, height } = entry.contentRect;
6180
- const w = Math.floor(width);
6181
- const h = Math.floor(height);
6182
- setSize((prev) => prev.width === w && prev.height === h ? prev : { width: w, height: h });
6183
- });
6178
+ const entry = entries[0];
6179
+ if (!entry) return;
6180
+ const { width, height } = entry.contentRect;
6181
+ const w = Math.floor(width);
6182
+ const h = Math.floor(height);
6183
+ if (w <= 0 || h <= 0) return;
6184
+ if (w === committedSize.current.width && h === committedSize.current.height) return;
6185
+ if (initialRef.current) {
6186
+ initialRef.current = false;
6187
+ committedSize.current = { width: w, height: h };
6188
+ setSize({ width: w, height: h });
6189
+ return;
6190
+ }
6191
+ const prev = committedSize.current;
6192
+ if (el.firstElementChild && prev.width > 0 && prev.height > 0) {
6193
+ const svg = el.firstElementChild;
6194
+ svg.style.transformOrigin = "0 0";
6195
+ svg.style.transform = `scale(${w / prev.width}, ${h / prev.height})`;
6196
+ }
6197
+ window.clearTimeout(settleTimer.current);
6198
+ settleTimer.current = window.setTimeout(() => {
6199
+ if (el.firstElementChild) {
6200
+ el.firstElementChild.style.transform = "";
6201
+ }
6202
+ committedSize.current = { width: w, height: h };
6203
+ setSize({ width: w, height: h });
6204
+ }, RESIZE_SETTLE_MS);
6184
6205
  });
6185
6206
  observer.observe(el);
6186
6207
  return () => {
6187
- cancelAnimationFrame(rafId);
6208
+ window.clearTimeout(settleTimer.current);
6188
6209
  observer.disconnect();
6189
6210
  };
6190
6211
  }, [ref]);
@@ -6379,17 +6400,19 @@ var BarChart = React5.memo(({ data, labels, width, height, onHover, onMove, onLe
6379
6400
  const chartW = width - PADDING.left - PADDING.right;
6380
6401
  const chartH = height - PADDING.top - PADDING.bottom;
6381
6402
  const groupW = chartW / count;
6382
- const barW = Math.max(1, Math.min(32, groupW * 0.7 / groupCount));
6403
+ const barGap = groupCount > 1 ? 2 : 0;
6404
+ const barW = Math.max(1, Math.min(32, (groupW * 0.7 - barGap * (groupCount - 1)) / groupCount));
6383
6405
  const bars = React5.useMemo(
6384
6406
  () => entries.map(
6385
6407
  ([, values], di) => values.map((v, i) => {
6408
+ const totalBarsW = barW * groupCount + barGap * (groupCount - 1);
6386
6409
  const h = Math.max(0, v / maxVal * chartH);
6387
- const x = PADDING.left + groupW * i + (groupW - barW * groupCount) / 2 + barW * di;
6410
+ const x = PADDING.left + groupW * i + (groupW - totalBarsW) / 2 + (barW + barGap) * di;
6388
6411
  const y = PADDING.top + chartH - h;
6389
6412
  return { x, y, w: barW, h, v };
6390
6413
  })
6391
6414
  ),
6392
- [entries, maxVal, chartH, groupW, barW, groupCount]
6415
+ [entries, maxVal, chartH, groupW, barW, barGap, groupCount]
6393
6416
  );
6394
6417
  const barLabelStep = getLabelStep(count, chartW);
6395
6418
  return /* @__PURE__ */ jsxs196("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
@@ -6401,22 +6424,22 @@ var BarChart = React5.memo(({ data, labels, width, height, onHover, onMove, onLe
6401
6424
  entries.map(([key], di) => {
6402
6425
  const palette = getPalette(LINE_BAR_PALETTES, di, key);
6403
6426
  const color = palette[2];
6404
- return bars[di].map((b, i) => /* @__PURE__ */ jsx305(
6405
- "rect",
6406
- {
6407
- x: b.x,
6408
- y: b.y,
6409
- width: b.w,
6410
- height: b.h,
6411
- rx: Math.min(4, b.w / 2),
6412
- fill: color,
6413
- className: "chart-bar",
6414
- onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
6415
- onMouseMove: onMove,
6416
- onMouseLeave: onLeave
6417
- },
6418
- `${di}-${i}`
6419
- ));
6427
+ return bars[di].map((b, i) => {
6428
+ const r2 = Math.min(4, b.w / 2);
6429
+ const d = b.h <= r2 ? `M ${b.x} ${b.y + b.h} V ${b.y} H ${b.x + b.w} V ${b.y + b.h} Z` : `M ${b.x} ${b.y + b.h} V ${b.y + r2} Q ${b.x} ${b.y} ${b.x + r2} ${b.y} H ${b.x + b.w - r2} Q ${b.x + b.w} ${b.y} ${b.x + b.w} ${b.y + r2} V ${b.y + b.h} Z`;
6430
+ return /* @__PURE__ */ jsx305(
6431
+ "path",
6432
+ {
6433
+ d,
6434
+ fill: color,
6435
+ className: "chart-bar",
6436
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
6437
+ onMouseMove: onMove,
6438
+ onMouseLeave: onLeave
6439
+ },
6440
+ `${di}-${i}`
6441
+ );
6442
+ });
6420
6443
  })
6421
6444
  ] });
6422
6445
  });
@@ -6503,20 +6526,22 @@ var TooltipBubble = ({ x, y, containerWidth, children }) => {
6503
6526
  }
6504
6527
  );
6505
6528
  };
6506
- var Chart = (props) => {
6529
+ var Chart = React5.memo((props) => {
6507
6530
  const { type, data, labels, tooltip: showTooltip = true } = props;
6508
6531
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
6509
6532
  const { width, height } = useChartSize(containerRef);
6533
+ const stableData = React5.useMemo(() => data, [JSON.stringify(data)]);
6534
+ const stableLabels = React5.useMemo(() => labels, [JSON.stringify(labels)]);
6510
6535
  const ready = width > 0 && height > 0;
6511
6536
  return /* @__PURE__ */ jsxs196("div", { className: "lib-xplat-chart", ref: containerRef, children: [
6512
- ready && type === "line" && /* @__PURE__ */ jsx305(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6513
- ready && type === "curve" && /* @__PURE__ */ jsx305(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6514
- ready && type === "bar" && /* @__PURE__ */ jsx305(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6515
- ready && type === "pie" && /* @__PURE__ */ jsx305(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
6516
- ready && type === "doughnut" && /* @__PURE__ */ jsx305(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
6537
+ ready && type === "line" && /* @__PURE__ */ jsx305(LineChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6538
+ ready && type === "curve" && /* @__PURE__ */ jsx305(CurveChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6539
+ ready && type === "bar" && /* @__PURE__ */ jsx305(BarChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6540
+ ready && type === "pie" && /* @__PURE__ */ jsx305(PieDonutChart, { data: stableData, labels: stableLabels, width, height, onHover: show, onMove: move, onLeave: hide }),
6541
+ ready && type === "doughnut" && /* @__PURE__ */ jsx305(PieDonutChart, { data: stableData, labels: stableLabels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
6517
6542
  tooltip.visible && /* @__PURE__ */ jsx305(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
6518
6543
  ] });
6519
- };
6544
+ });
6520
6545
  Chart.displayName = "Chart";
6521
6546
  var Chart_default = Chart;
6522
6547
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x-plat/design-system",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "XPLAT UI Design System",
5
5
  "author": "XPLAT WOONG",
6
6
  "main": "dist/index.cjs",