@x-plat/design-system 0.5.7 → 0.5.9

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.
@@ -1,25 +1,65 @@
1
1
  // src/components/Chart/Chart.tsx
2
2
  import React from "react";
3
3
  import { jsx, jsxs } from "react/jsx-runtime";
4
- var LINE_BAR_PALETTES = [
5
- ["var(--primitive-red-100)", "var(--primitive-red-200)", "var(--primitive-red-300)", "var(--primitive-red-400)", "var(--primitive-red-500)", "var(--primitive-red-600)"],
6
- ["var(--primitive-orange-100)", "var(--primitive-orange-200)", "var(--primitive-orange-300)", "var(--primitive-orange-400)", "var(--primitive-orange-500)", "var(--primitive-orange-600)"],
7
- ["var(--primitive-yellow-100)", "var(--primitive-yellow-200)", "var(--primitive-yellow-300)", "var(--primitive-yellow-400)", "var(--primitive-yellow-500)", "var(--primitive-yellow-600)"],
8
- ["var(--primitive-green-100)", "var(--primitive-green-200)", "var(--primitive-green-300)", "var(--primitive-green-400)", "var(--primitive-green-500)", "var(--primitive-green-600)"],
9
- ["var(--primitive-blue-100)", "var(--primitive-blue-200)", "var(--primitive-blue-300)", "var(--primitive-blue-400)", "var(--primitive-blue-500)", "var(--primitive-blue-600)"],
10
- ["var(--primitive-light-blue-100)", "var(--primitive-light-blue-200)", "var(--primitive-light-blue-300)", "var(--primitive-light-blue-400)", "var(--primitive-light-blue-500)", "var(--primitive-light-blue-600)"],
11
- ["var(--primitive-purple-100)", "var(--primitive-purple-200)", "var(--primitive-purple-300)", "var(--primitive-purple-400)", "var(--primitive-purple-500)", "var(--primitive-purple-600)"],
12
- ["var(--primitive-pink-100)", "var(--primitive-pink-200)", "var(--primitive-pink-300)", "var(--primitive-pink-400)", "var(--primitive-pink-500)", "var(--primitive-pink-600)"]
13
- ];
14
- var PIE_PALETTES = [
15
- ["var(--primitive-orange-300)", "var(--primitive-red-300)", "var(--primitive-yellow-300)", "var(--primitive-green-300)", "var(--primitive-blue-300)", "var(--primitive-light-blue-300)"],
16
- ["var(--primitive-orange-400)", "var(--primitive-red-400)", "var(--primitive-yellow-400)", "var(--primitive-green-400)", "var(--primitive-blue-400)", "var(--primitive-light-blue-400)"],
17
- ["var(--primitive-orange-500)", "var(--primitive-red-500)", "var(--primitive-yellow-500)", "var(--primitive-green-500)", "var(--primitive-blue-500)", "var(--primitive-light-blue-500)"]
18
- ];
19
- var getPalette = (palettes, index) => {
20
- return palettes[index % palettes.length];
4
+ var CATEGORICAL_COUNT = 8;
5
+ var LINE_BAR_PALETTES = Array.from({ length: CATEGORICAL_COUNT }, (_, i) => {
6
+ const n = i + 1;
7
+ return [
8
+ `var(--semantic-categorical-${n}-bg)`,
9
+ `var(--semantic-categorical-${n}-area)`,
10
+ `var(--semantic-categorical-${n}-fill)`,
11
+ `var(--semantic-categorical-${n}-text)`
12
+ ];
13
+ });
14
+ var PIE_COLORS = Array.from(
15
+ { length: CATEGORICAL_COUNT },
16
+ (_, i) => `var(--semantic-categorical-${i + 1}-fill)`
17
+ );
18
+ var hashString = (str) => {
19
+ let hash = 0;
20
+ for (let i = 0; i < str.length; i++) {
21
+ hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
22
+ }
23
+ return Math.abs(hash);
24
+ };
25
+ var getPalette = (palettes, index, key) => {
26
+ const offset = key ? hashString(key) : 0;
27
+ return palettes[(index + offset) % palettes.length];
21
28
  };
22
29
  var PADDING = { top: 20, right: 20, bottom: 30, left: 40 };
30
+ var toSmoothPath = (points) => {
31
+ if (points.length < 2) return "";
32
+ const p = points;
33
+ let d = `M ${p[0].x} ${p[0].y}`;
34
+ for (let i = 0; i < p.length - 1; i++) {
35
+ const p0 = p[Math.max(0, i - 1)];
36
+ const p1 = p[i];
37
+ const p2 = p[i + 1];
38
+ const p3 = p[Math.min(p.length - 1, i + 2)];
39
+ const cp1x = p1.x + (p2.x - p0.x) / 6;
40
+ const cp1y = p1.y + (p2.y - p0.y) / 6;
41
+ const cp2x = p2.x - (p3.x - p1.x) / 6;
42
+ const cp2y = p2.y - (p3.y - p1.y) / 6;
43
+ d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`;
44
+ }
45
+ return d;
46
+ };
47
+ var useChartSize = (ref) => {
48
+ const [size, setSize] = React.useState({ width: 0, height: 0 });
49
+ React.useEffect(() => {
50
+ const el = ref.current;
51
+ if (!el) return;
52
+ const observer = new ResizeObserver((entries) => {
53
+ const entry = entries[0];
54
+ if (!entry) return;
55
+ const { width, height } = entry.contentRect;
56
+ setSize({ width: Math.floor(width), height: Math.floor(height) });
57
+ });
58
+ observer.observe(el);
59
+ return () => observer.disconnect();
60
+ }, [ref]);
61
+ return size;
62
+ };
23
63
  var useChartTooltip = (enabled) => {
24
64
  const [tooltip, setTooltip] = React.useState({
25
65
  visible: false,
@@ -54,15 +94,15 @@ var useChartTooltip = (enabled) => {
54
94
  }, []);
55
95
  return { tooltip, show, hide, move, containerRef };
56
96
  };
57
- var LineChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
97
+ var LineChart = React.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
58
98
  const entries = React.useMemo(() => Object.entries(data), [data]);
59
99
  const maxVal = React.useMemo(() => {
60
100
  const allValues = entries.flatMap(([, v]) => v);
61
101
  return Math.max(...allValues) * 1.2 || 1;
62
102
  }, [entries]);
63
103
  const count = labels.length;
64
- const chartW = 600 - PADDING.left - PADDING.right;
65
- const chartH = 300 - PADDING.top - PADDING.bottom;
104
+ const chartW = width - PADDING.left - PADDING.right;
105
+ const chartH = height - PADDING.top - PADDING.bottom;
66
106
  const seriesPoints = React.useMemo(
67
107
  () => entries.map(
68
108
  ([, values]) => values.map((v, i) => ({
@@ -73,22 +113,22 @@ var LineChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
73
113
  ),
74
114
  [entries, count, chartW, chartH, maxVal]
75
115
  );
76
- return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 600 300", className: "chart-svg", children: [
116
+ return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
77
117
  [0, 0.25, 0.5, 0.75, 1].map((ratio) => {
78
118
  const y = PADDING.top + (1 - ratio) * chartH;
79
119
  const val = Math.round(maxVal * ratio);
80
120
  return /* @__PURE__ */ jsxs("g", { children: [
81
- /* @__PURE__ */ jsx("line", { x1: PADDING.left, y1: y, x2: 600 - PADDING.right, y2: y, className: "chart-grid" }),
121
+ /* @__PURE__ */ jsx("line", { x1: PADDING.left, y1: y, x2: width - PADDING.right, y2: y, className: "chart-grid" }),
82
122
  /* @__PURE__ */ jsx("text", { x: PADDING.left - 8, y: y + 4, className: "chart-axis-label", textAnchor: "end", children: val })
83
123
  ] }, ratio);
84
124
  }),
85
125
  labels.map((label, i) => {
86
126
  const x = PADDING.left + i / (count - 1 || 1) * chartW;
87
- return /* @__PURE__ */ jsx("text", { x, y: 300 - 8, className: "chart-axis-label", textAnchor: "middle", children: label }, i);
127
+ return /* @__PURE__ */ jsx("text", { x, y: height - 8, className: "chart-axis-label", textAnchor: "middle", children: label }, i);
88
128
  }),
89
129
  entries.map(([key], di) => {
90
- const palette = getPalette(LINE_BAR_PALETTES, di);
91
- const color = palette[4];
130
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
131
+ const color = palette[2];
92
132
  const points = seriesPoints[di];
93
133
  return /* @__PURE__ */ jsxs("g", { children: [
94
134
  /* @__PURE__ */ jsx(
@@ -119,7 +159,73 @@ var LineChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
119
159
  ] });
120
160
  });
121
161
  LineChart.displayName = "LineChart";
122
- var BarChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
162
+ var CurveChart = React.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
163
+ const entries = React.useMemo(() => Object.entries(data), [data]);
164
+ const maxVal = React.useMemo(() => {
165
+ const allValues = entries.flatMap(([, v]) => v);
166
+ return Math.max(...allValues) * 1.2 || 1;
167
+ }, [entries]);
168
+ const count = labels.length;
169
+ const chartW = width - PADDING.left - PADDING.right;
170
+ const chartH = height - PADDING.top - PADDING.bottom;
171
+ const seriesPoints = React.useMemo(
172
+ () => entries.map(
173
+ ([, values]) => values.map((v, i) => ({
174
+ x: PADDING.left + i / (count - 1 || 1) * chartW,
175
+ y: PADDING.top + (1 - v / maxVal) * chartH,
176
+ v
177
+ }))
178
+ ),
179
+ [entries, count, chartW, chartH, maxVal]
180
+ );
181
+ return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
182
+ [0, 0.25, 0.5, 0.75, 1].map((ratio) => {
183
+ const y = PADDING.top + (1 - ratio) * chartH;
184
+ const val = Math.round(maxVal * ratio);
185
+ return /* @__PURE__ */ jsxs("g", { children: [
186
+ /* @__PURE__ */ jsx("line", { x1: PADDING.left, y1: y, x2: width - PADDING.right, y2: y, className: "chart-grid" }),
187
+ /* @__PURE__ */ jsx("text", { x: PADDING.left - 8, y: y + 4, className: "chart-axis-label", textAnchor: "end", children: val })
188
+ ] }, ratio);
189
+ }),
190
+ labels.map((label, i) => {
191
+ const x = PADDING.left + i / (count - 1 || 1) * chartW;
192
+ return /* @__PURE__ */ jsx("text", { x, y: height - 8, className: "chart-axis-label", textAnchor: "middle", children: label }, i);
193
+ }),
194
+ entries.map(([key], di) => {
195
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
196
+ const color = palette[2];
197
+ const areaColor = palette[0];
198
+ const points = seriesPoints[di];
199
+ const gradientId = `curve-gradient-${di}`;
200
+ const linePath = toSmoothPath(points);
201
+ const areaPath = linePath + ` L ${points[points.length - 1].x} ${PADDING.top + chartH} L ${points[0].x} ${PADDING.top + chartH} Z`;
202
+ return /* @__PURE__ */ jsxs("g", { children: [
203
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
204
+ /* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: areaColor, stopOpacity: "0.4" }),
205
+ /* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: areaColor, stopOpacity: "0.02" })
206
+ ] }) }),
207
+ /* @__PURE__ */ jsx("path", { d: areaPath, fill: `url(#${gradientId})` }),
208
+ /* @__PURE__ */ jsx("path", { d: linePath, fill: "none", stroke: color, strokeWidth: "2" }),
209
+ points.map((p, i) => /* @__PURE__ */ jsx(
210
+ "circle",
211
+ {
212
+ cx: p.x,
213
+ cy: p.y,
214
+ r: "4",
215
+ fill: color,
216
+ className: "chart-point",
217
+ onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${p.v}`),
218
+ onMouseMove: onMove,
219
+ onMouseLeave: onLeave
220
+ },
221
+ i
222
+ ))
223
+ ] }, di);
224
+ })
225
+ ] });
226
+ });
227
+ CurveChart.displayName = "CurveChart";
228
+ var BarChart = React.memo(({ data, labels, width, height, onHover, onMove, onLeave }) => {
123
229
  const entries = React.useMemo(() => Object.entries(data), [data]);
124
230
  const maxVal = React.useMemo(() => {
125
231
  const allValues = entries.flatMap(([, v]) => v);
@@ -127,8 +233,8 @@ var BarChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
127
233
  }, [entries]);
128
234
  const count = labels.length;
129
235
  const groupCount = entries.length;
130
- const chartW = 600 - PADDING.left - PADDING.right;
131
- const chartH = 300 - PADDING.top - PADDING.bottom;
236
+ const chartW = width - PADDING.left - PADDING.right;
237
+ const chartH = height - PADDING.top - PADDING.bottom;
132
238
  const groupW = chartW / count;
133
239
  const barW = Math.min(32, groupW * 0.7 / groupCount);
134
240
  const bars = React.useMemo(
@@ -142,19 +248,19 @@ var BarChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
142
248
  ),
143
249
  [entries, maxVal, chartH, groupW, barW, groupCount]
144
250
  );
145
- return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 600 300", className: "chart-svg", children: [
251
+ return /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 ${width} ${height}`, className: "chart-svg", children: [
146
252
  [0, 0.25, 0.5, 0.75, 1].map((ratio) => {
147
253
  const y = PADDING.top + (1 - ratio) * chartH;
148
254
  const val = Math.round(maxVal * ratio);
149
255
  return /* @__PURE__ */ jsxs("g", { children: [
150
- /* @__PURE__ */ jsx("line", { x1: PADDING.left, y1: y, x2: 600 - PADDING.right, y2: y, className: "chart-grid" }),
256
+ /* @__PURE__ */ jsx("line", { x1: PADDING.left, y1: y, x2: width - PADDING.right, y2: y, className: "chart-grid" }),
151
257
  /* @__PURE__ */ jsx("text", { x: PADDING.left - 8, y: y + 4, className: "chart-axis-label", textAnchor: "end", children: val })
152
258
  ] }, ratio);
153
259
  }),
154
- labels.map((label, i) => /* @__PURE__ */ jsx("text", { x: PADDING.left + groupW * i + groupW / 2, y: 300 - 8, className: "chart-axis-label", textAnchor: "middle", children: label }, i)),
260
+ labels.map((label, i) => /* @__PURE__ */ jsx("text", { x: PADDING.left + groupW * i + groupW / 2, y: height - 8, className: "chart-axis-label", textAnchor: "middle", children: label }, i)),
155
261
  entries.map(([key], di) => {
156
- const palette = getPalette(LINE_BAR_PALETTES, di);
157
- const color = palette[4];
262
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
263
+ const color = palette[2];
158
264
  return bars[di].map((b, i) => /* @__PURE__ */ jsx(
159
265
  "rect",
160
266
  {
@@ -162,7 +268,7 @@ var BarChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
162
268
  y: b.y,
163
269
  width: b.w,
164
270
  height: b.h,
165
- rx: "2",
271
+ rx: Math.min(4, b.w / 2),
166
272
  fill: color,
167
273
  className: "chart-bar",
168
274
  onMouseEnter: (e) => onHover(e, `${key}: ${labels[i]} \u2014 ${b.v}`),
@@ -176,14 +282,17 @@ var BarChart = React.memo(({ data, labels, onHover, onMove, onLeave }) => {
176
282
  });
177
283
  BarChart.displayName = "BarChart";
178
284
  var PieDonutChart = React.memo(
179
- ({ data, labels, isDoughnut, onHover, onMove, onLeave }) => {
180
- const values = React.useMemo(() => Object.entries(data).flatMap(([, v]) => v), [data]);
285
+ ({ data, labels, width, height, isDoughnut, onHover, onMove, onLeave }) => {
286
+ const entries = React.useMemo(() => Object.entries(data), [data]);
287
+ const values = React.useMemo(() => entries.flatMap(([, v]) => v), [entries]);
181
288
  const total = React.useMemo(() => values.reduce((a, b) => a + b, 0) || 1, [values]);
182
- const cx = 150;
183
- const cy = 150;
184
- const r = 120;
185
- const innerR = isDoughnut ? 60 : 0;
186
- const palette = getPalette(PIE_PALETTES, 0);
289
+ const size = Math.min(width, height);
290
+ const cx = size / 2;
291
+ const cy = size / 2;
292
+ const r = size * 0.4;
293
+ const innerR = isDoughnut ? r * 0.5 : 0;
294
+ const firstKey = entries[0]?.[0] ?? "";
295
+ const colorOffset = hashString(firstKey);
187
296
  const sliceData = React.useMemo(() => {
188
297
  let angle0 = -Math.PI / 2;
189
298
  return values.map((v, i) => {
@@ -212,13 +321,13 @@ var PieDonutChart = React.memo(
212
321
  angle0 = endAngle;
213
322
  return { d, lx, ly, v, pct, angle, label: labels[i] || `${i + 1}` };
214
323
  });
215
- }, [values, total, innerR, labels]);
216
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 300 300", className: "chart-svg chart-pie", children: sliceData.map((s, i) => /* @__PURE__ */ jsxs("g", { children: [
324
+ }, [values, total, cx, cy, r, innerR, labels]);
325
+ return /* @__PURE__ */ jsx("svg", { viewBox: `0 0 ${size} ${size}`, className: "chart-svg chart-pie", children: sliceData.map((s, i) => /* @__PURE__ */ jsxs("g", { children: [
217
326
  /* @__PURE__ */ jsx(
218
327
  "path",
219
328
  {
220
329
  d: s.d,
221
- fill: palette[i % palette.length],
330
+ fill: PIE_COLORS[(i + colorOffset) % PIE_COLORS.length],
222
331
  className: "chart-slice",
223
332
  onMouseEnter: (e) => onHover(e, `${s.label}: ${s.v} (${s.pct}%)`),
224
333
  onMouseMove: onMove,
@@ -230,22 +339,42 @@ var PieDonutChart = React.memo(
230
339
  }
231
340
  );
232
341
  PieDonutChart.displayName = "PieDonutChart";
342
+ var TooltipBubble = ({ x, y, containerWidth, children }) => {
343
+ const ref = React.useRef(null);
344
+ const [adjustedX, setAdjustedX] = React.useState(x);
345
+ React.useEffect(() => {
346
+ const el = ref.current;
347
+ if (!el) return;
348
+ const w = el.offsetWidth;
349
+ const half = w / 2;
350
+ const margin = 8;
351
+ let nx = x;
352
+ if (x - half < margin) nx = half + margin;
353
+ else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
354
+ setAdjustedX(nx);
355
+ }, [x, containerWidth]);
356
+ return /* @__PURE__ */ jsx(
357
+ "div",
358
+ {
359
+ ref,
360
+ className: "chart-tooltip",
361
+ style: { left: adjustedX, top: y },
362
+ children
363
+ }
364
+ );
365
+ };
233
366
  var Chart = (props) => {
234
367
  const { type, data, labels, tooltip: showTooltip = true } = props;
235
368
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
369
+ const { width, height } = useChartSize(containerRef);
370
+ const ready = width > 0 && height > 0;
236
371
  return /* @__PURE__ */ jsxs("div", { className: "lib-xplat-chart", ref: containerRef, children: [
237
- type === "line" && /* @__PURE__ */ jsx(LineChart, { data, labels, onHover: show, onMove: move, onLeave: hide }),
238
- type === "bar" && /* @__PURE__ */ jsx(BarChart, { data, labels, onHover: show, onMove: move, onLeave: hide }),
239
- type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data, labels, onHover: show, onMove: move, onLeave: hide }),
240
- type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data, labels, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
241
- tooltip.visible && /* @__PURE__ */ jsx(
242
- "div",
243
- {
244
- className: "chart-tooltip",
245
- style: { left: tooltip.x, top: tooltip.y },
246
- children: tooltip.content
247
- }
248
- )
372
+ ready && type === "line" && /* @__PURE__ */ jsx(LineChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
373
+ ready && type === "curve" && /* @__PURE__ */ jsx(CurveChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
374
+ ready && type === "bar" && /* @__PURE__ */ jsx(BarChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
375
+ ready && type === "pie" && /* @__PURE__ */ jsx(PieDonutChart, { data, labels, width, height, onHover: show, onMove: move, onLeave: hide }),
376
+ ready && type === "doughnut" && /* @__PURE__ */ jsx(PieDonutChart, { data, labels, width, height, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
377
+ tooltip.visible && /* @__PURE__ */ jsx(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
249
378
  ] });
250
379
  };
251
380
  Chart.displayName = "Chart";