@spatika/react 1.3.1 → 1.5.0

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.
Files changed (127) hide show
  1. package/AGENTS.md +6 -3
  2. package/README.md +1 -1
  3. package/dist/composites/EventCalendar.d.ts +13 -2
  4. package/dist/composites/EventCalendar.d.ts.map +1 -1
  5. package/dist/composites/EventCalendar.js +13 -27
  6. package/dist/composites/GlassCard.d.ts +1 -1
  7. package/dist/composites/RichTextEditor.d.ts +1 -4
  8. package/dist/composites/RichTextEditor.d.ts.map +1 -1
  9. package/dist/composites/RichTextEditor.js +12 -19
  10. package/dist/composites/SurfaceCard.d.ts +1 -1
  11. package/dist/composites/Tag.d.ts +2 -2
  12. package/dist/composites/cartesian-charts.d.ts +12 -2
  13. package/dist/composites/cartesian-charts.d.ts.map +1 -1
  14. package/dist/composites/cartesian-charts.js +27 -13
  15. package/dist/composites/chart-composition.d.ts +19 -1
  16. package/dist/composites/chart-composition.d.ts.map +1 -1
  17. package/dist/composites/chart-composition.js +124 -62
  18. package/dist/composites/chart-interaction.d.ts +81 -0
  19. package/dist/composites/chart-interaction.d.ts.map +1 -0
  20. package/dist/composites/chart-interaction.js +56 -0
  21. package/dist/composites/chart-ui.d.ts +8 -13
  22. package/dist/composites/chart-ui.d.ts.map +1 -1
  23. package/dist/composites/chart-ui.js +9 -6
  24. package/dist/composites/flow-charts.d.ts +6 -1
  25. package/dist/composites/flow-charts.d.ts.map +1 -1
  26. package/dist/composites/flow-charts.js +28 -14
  27. package/dist/composites/radial-charts.d.ts +11 -1
  28. package/dist/composites/radial-charts.d.ts.map +1 -1
  29. package/dist/composites/radial-charts.js +33 -14
  30. package/dist/composites/scheduler-ui.d.ts +14 -1
  31. package/dist/composites/scheduler-ui.d.ts.map +1 -1
  32. package/dist/composites/scheduler-ui.js +23 -2
  33. package/dist/index.d.ts +14 -26
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +8 -14
  36. package/dist/lib/breakpoints.d.ts +17 -0
  37. package/dist/lib/breakpoints.d.ts.map +1 -0
  38. package/dist/lib/breakpoints.js +23 -0
  39. package/dist/lib/chips.d.ts +3 -0
  40. package/dist/lib/chips.d.ts.map +1 -1
  41. package/dist/lib/chips.js +10 -0
  42. package/dist/lib/create-theme.d.ts +162 -0
  43. package/dist/lib/create-theme.d.ts.map +1 -0
  44. package/dist/lib/create-theme.js +138 -0
  45. package/dist/lib/scheduler.d.ts +12 -0
  46. package/dist/lib/scheduler.d.ts.map +1 -1
  47. package/dist/lib/scheduler.js +24 -2
  48. package/dist/lib/themes.d.ts +17 -7
  49. package/dist/lib/themes.d.ts.map +1 -1
  50. package/dist/lib/themes.js +22 -8
  51. package/dist/lib/use-media-query.d.ts +9 -0
  52. package/dist/lib/use-media-query.d.ts.map +1 -1
  53. package/dist/lib/use-media-query.js +28 -1
  54. package/dist/primitives/Alert.d.ts +1 -1
  55. package/dist/primitives/Badge.d.ts +2 -2
  56. package/dist/primitives/Button.d.ts +2 -2
  57. package/dist/primitives/Container.d.ts +2 -2
  58. package/dist/primitives/Fab.d.ts +4 -4
  59. package/dist/primitives/Grid.d.ts +2 -1
  60. package/dist/primitives/Grid.d.ts.map +1 -1
  61. package/dist/primitives/Grid.js +10 -2
  62. package/dist/primitives/IconButton.d.ts +2 -2
  63. package/dist/primitives/NativeSelect.d.ts +10 -1
  64. package/dist/primitives/NativeSelect.d.ts.map +1 -1
  65. package/dist/primitives/NativeSelect.js +2 -2
  66. package/dist/primitives/ToggleButton.d.ts +1 -1
  67. package/dist/theme/SpatikaThemeProvider.d.ts +4 -5
  68. package/dist/theme/SpatikaThemeProvider.d.ts.map +1 -1
  69. package/dist/theme/SpatikaThemeProvider.js +28 -13
  70. package/package.json +4 -2
  71. package/skills/spatika-ui/SKILL.md +28 -5
  72. package/src/composites/EventCalendar.test.tsx +293 -0
  73. package/src/composites/EventCalendar.tsx +38 -27
  74. package/src/composites/RichTextEditor.test.tsx +51 -0
  75. package/src/composites/RichTextEditor.tsx +20 -84
  76. package/src/composites/chart-ui.test.tsx +21 -0
  77. package/src/composites/scheduler-ui.tsx +128 -26
  78. package/src/composites/scheduler.test.tsx +0 -219
  79. package/src/index.test.ts +10 -0
  80. package/src/index.ts +148 -65
  81. package/src/lib/breakpoints.test.ts +29 -0
  82. package/src/lib/breakpoints.ts +28 -0
  83. package/src/lib/chips.ts +16 -0
  84. package/src/lib/create-theme.test.ts +79 -0
  85. package/src/lib/create-theme.ts +249 -0
  86. package/src/lib/scheduler.test.ts +26 -0
  87. package/src/lib/scheduler.ts +36 -2
  88. package/src/lib/themes.test.ts +17 -0
  89. package/src/lib/themes.ts +43 -12
  90. package/src/lib/use-media-query.test.tsx +80 -0
  91. package/src/lib/use-media-query.ts +35 -1
  92. package/src/primitives/Grid.test.tsx +55 -0
  93. package/src/primitives/Grid.tsx +13 -3
  94. package/src/primitives/NativeSelect.test.tsx +28 -0
  95. package/src/primitives/NativeSelect.tsx +14 -4
  96. package/src/primitives/mui-more.test.tsx +17 -0
  97. package/src/theme/SpatikaThemeProvider.test.tsx +38 -0
  98. package/src/theme/SpatikaThemeProvider.tsx +43 -15
  99. package/src/composites/ChartDataGrid.test.tsx +0 -57
  100. package/src/composites/ChartDataGrid.tsx +0 -196
  101. package/src/composites/MapChart.test.tsx +0 -39
  102. package/src/composites/MapChart.tsx +0 -130
  103. package/src/composites/ScatterWebGL.test.tsx +0 -25
  104. package/src/composites/ScatterWebGL.tsx +0 -79
  105. package/src/composites/cartesian-charts.test.tsx +0 -52
  106. package/src/composites/cartesian-charts.tsx +0 -1287
  107. package/src/composites/chart-composition.test.tsx +0 -53
  108. package/src/composites/chart-composition.tsx +0 -898
  109. package/src/composites/chart-ui.tsx +0 -328
  110. package/src/composites/charts-3d.test.tsx +0 -25
  111. package/src/composites/charts-3d.tsx +0 -318
  112. package/src/composites/charts.test.tsx +0 -254
  113. package/src/composites/flow-charts.test.tsx +0 -79
  114. package/src/composites/flow-charts.tsx +0 -607
  115. package/src/composites/radial-charts.test.tsx +0 -74
  116. package/src/composites/radial-charts.tsx +0 -617
  117. package/src/lib/chart-export.test.ts +0 -20
  118. package/src/lib/chart-export.ts +0 -99
  119. package/src/lib/charts-platform.test.ts +0 -93
  120. package/src/lib/charts.test.ts +0 -132
  121. package/src/lib/charts.ts +0 -1021
  122. package/src/lib/geo.test.ts +0 -42
  123. package/src/lib/geo.ts +0 -138
  124. package/src/lib/iso.test.ts +0 -22
  125. package/src/lib/iso.ts +0 -102
  126. package/src/lib/webgl-scatter.test.ts +0 -36
  127. package/src/lib/webgl-scatter.ts +0 -230
@@ -1,607 +0,0 @@
1
- import { useId } from "react";
2
- import {
3
- chartColor,
4
- chordLayout,
5
- formatChartNumber,
6
- funnelLayout,
7
- heatColor,
8
- pyramidLayout,
9
- sankeyLayout,
10
- squarify,
11
- sunburstLayout,
12
- type ChartAxisConfig,
13
- type ChartMargin,
14
- type SankeyLink,
15
- type SankeyNode,
16
- type TreeNode,
17
- } from "../lib/charts";
18
- import {
19
- ChartFrame,
20
- categoryLabels,
21
- seriesMeta,
22
- useChartHover,
23
- useHiddenSeries,
24
- } from "./chart-ui";
25
-
26
- export type HeatDatum = { x: number; y: number; value: number };
27
-
28
- export type HeatmapProps = {
29
- xAxis?: ChartAxisConfig[];
30
- yAxis?: ChartAxisConfig[];
31
- series: Array<{ data: HeatDatum[] }>;
32
- height?: number;
33
- width?: number;
34
- hideLegend?: boolean;
35
- showColorScale?: boolean;
36
- margin?: ChartMargin;
37
- className?: string;
38
- };
39
-
40
- export function Heatmap({
41
- xAxis,
42
- yAxis,
43
- series,
44
- height = 280,
45
- width,
46
- hideLegend,
47
- showColorScale,
48
- margin,
49
- className,
50
- }: HeatmapProps) {
51
- const cells = series[0]?.data ?? [];
52
- const xs = categoryLabels(xAxis?.[0], Math.max(...cells.map((c) => c.x), 0) + 1);
53
- const ys = categoryLabels(yAxis?.[0], Math.max(...cells.map((c) => c.y), 0) + 1);
54
- const values = cells.map((c) => c.value);
55
- const min = Math.min(...values, 0);
56
- const max = Math.max(...values, 1);
57
- const { hover, setHover, clear } = useChartHover();
58
- const reactId = useId().replace(/:/g, "");
59
- const scaleId = `spk-heat-scale-${reactId}`;
60
- const colorScale = showColorScale ?? !hideLegend;
61
-
62
- return (
63
- <ChartFrame
64
- slot="heatmap"
65
- className={className}
66
- width={width}
67
- height={height}
68
- margin={margin ?? { top: colorScale ? 32 : 12, right: 12, bottom: 36, left: 72 }}
69
- hover={hover}
70
- aria-label="Heatmap"
71
- >
72
- {({ width: w, height: h, m }) => {
73
- const iw = Math.max(1, w - m.left - m.right);
74
- const ih = Math.max(1, h - m.top - m.bottom);
75
- const cw = iw / Math.max(xs.length, 1);
76
- const ch = ih / Math.max(ys.length, 1);
77
- return (
78
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
79
- {colorScale ? (
80
- <g className="spk-chart-color-scale" data-slot="heatmap-color-scale">
81
- <defs>
82
- <linearGradient id={scaleId} x1="0" y1="0" x2="1" y2="0">
83
- <stop offset="0%" stopColor={heatColor(0)} />
84
- <stop offset="50%" stopColor={heatColor(0.5)} />
85
- <stop offset="100%" stopColor={heatColor(1)} />
86
- </linearGradient>
87
- </defs>
88
- <text className="spk-chart-muted" x={m.left} y={m.top - 16}>
89
- {formatChartNumber(min)}
90
- </text>
91
- <text
92
- className="spk-chart-muted"
93
- x={m.left + iw}
94
- y={m.top - 16}
95
- textAnchor="end"
96
- >
97
- {formatChartNumber(max)}
98
- </text>
99
- <rect
100
- x={m.left}
101
- y={m.top - 12}
102
- width={iw}
103
- height={6}
104
- rx={3}
105
- fill={`url(#${scaleId})`}
106
- />
107
- </g>
108
- ) : null}
109
- {ys.map((label, y) => (
110
- <text
111
- key={`y-${String(label)}`}
112
- className="spk-chart-tick"
113
- x={m.left - 8}
114
- y={m.top + y * ch + ch / 2}
115
- textAnchor="end"
116
- dominantBaseline="middle"
117
- >
118
- {String(label)}
119
- </text>
120
- ))}
121
- {xs.map((label, x) => (
122
- <text
123
- key={`x-${String(label)}`}
124
- className="spk-chart-tick"
125
- x={m.left + x * cw + cw / 2}
126
- y={h - m.bottom + 16}
127
- textAnchor="middle"
128
- >
129
- {String(label)}
130
- </text>
131
- ))}
132
- {cells.map((cell, i) => {
133
- const t = (cell.value - min) / (max - min || 1);
134
- return (
135
- <rect
136
- key={i}
137
- x={m.left + cell.x * cw + 1.5}
138
- y={m.top + cell.y * ch + 1.5}
139
- width={Math.max(1, cw - 3)}
140
- height={Math.max(1, ch - 3)}
141
- rx={4}
142
- fill={heatColor(t)}
143
- className="spk-chart-mark"
144
- onMouseEnter={(event) =>
145
- setHover({
146
- x: event.nativeEvent.offsetX,
147
- y: event.nativeEvent.offsetY,
148
- title: `${xs[cell.x]} · ${ys[cell.y]}`,
149
- items: [
150
- {
151
- color: heatColor(t),
152
- label: "value",
153
- value: formatChartNumber(cell.value),
154
- },
155
- ],
156
- })
157
- }
158
- onMouseLeave={clear}
159
- />
160
- );
161
- })}
162
- </svg>
163
- );
164
- }}
165
- </ChartFrame>
166
- );
167
- }
168
-
169
- export type FunnelDatum = { id?: string; label: string; value: number; color?: string };
170
-
171
- export type FunnelSeries = {
172
- id?: string;
173
- label?: string;
174
- data: FunnelDatum[];
175
- };
176
-
177
- export type FunnelLabelPosition = "inside" | "outside" | "both";
178
-
179
- export type FunnelChartProps = {
180
- series: FunnelSeries[];
181
- height?: number;
182
- width?: number;
183
- colors?: string[];
184
- hideLegend?: boolean;
185
- className?: string;
186
- variant?: "funnel" | "pyramid";
187
- gap?: number;
188
- labelPosition?: FunnelLabelPosition;
189
- valueFormatter?: (value: number, percent: number) => string;
190
- };
191
-
192
- export function FunnelChart({
193
- series,
194
- height = 300,
195
- width,
196
- colors,
197
- hideLegend,
198
- className,
199
- variant = "funnel",
200
- gap = 6,
201
- labelPosition,
202
- valueFormatter,
203
- }: FunnelChartProps) {
204
- const stages = series[0]?.data ?? [];
205
- const comparison = series.length > 1;
206
- const position = labelPosition ?? (comparison ? "both" : "inside");
207
- const formatValue =
208
- valueFormatter ?? ((value: number) => formatChartNumber(value));
209
- const meta = seriesMeta(
210
- stages.map((item) => ({ id: item.id ?? item.label, label: item.label, color: item.color })),
211
- colors,
212
- );
213
- const { hover, setHover, clear } = useChartHover();
214
- const ids = meta.map((item) => item.id);
215
- const { hidden, toggle } = useHiddenSeries(ids);
216
- const showInside = position === "inside" || position === "both";
217
- const showOutside = position === "outside" || position === "both";
218
-
219
- return (
220
- <ChartFrame
221
- slot={variant === "pyramid" ? "pyramid-chart" : "funnel-chart"}
222
- className={className}
223
- width={width}
224
- height={height}
225
- margin={{
226
- top: 8,
227
- right: showOutside ? 56 : 8,
228
- bottom: 8,
229
- left: 8,
230
- }}
231
- legend={hideLegend ? undefined : meta}
232
- hiddenIds={hidden}
233
- onToggleSeries={toggle}
234
- hover={hover}
235
- aria-label={variant === "pyramid" ? "Pyramid chart" : "Funnel chart"}
236
- >
237
- {({ width: w, height: h, m }) => {
238
- const visibleIndexes = stages
239
- .map((_, i) => i)
240
- .filter((i) => !hidden.has(meta[i]!.id));
241
- const visibleSeries = series.map((item) => ({
242
- ...item,
243
- data: visibleIndexes.map((i) => item.data[i]).filter((row): row is FunnelDatum => row != null),
244
- }));
245
- const maxValue = Math.max(
246
- ...visibleSeries.flatMap((item) => item.data.map((row) => row.value)),
247
- 1,
248
- );
249
- const innerW = w - m.left - m.right;
250
- const innerH = h - m.top - m.bottom;
251
- const layouts = visibleSeries.map((item) => {
252
- const rows = item.data.map((row, i) => ({
253
- ...row,
254
- color: meta[visibleIndexes[i] ?? i]?.color,
255
- }));
256
- return variant === "pyramid"
257
- ? pyramidLayout(rows, innerW, innerH)
258
- : funnelLayout(rows, innerW, innerH, gap, maxValue);
259
- });
260
- const drawOrder = layouts.map((_, i) => i).sort((a, b) => (a === 0 ? 1 : b === 0 ? -1 : a - b));
261
- return (
262
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
263
- {drawOrder.flatMap((seriesIndex) => {
264
- const layout = layouts[seriesIndex] ?? [];
265
- const primary = seriesIndex === 0;
266
- return layout.map((row, i) => {
267
- const info = meta.find((item) => item.label === row.label) ?? meta[visibleIndexes[i] ?? i]!;
268
- const d = `M${m.left + row.topLeft} ${m.top + row.y} L${m.left + row.topRight} ${m.top + row.y} L${m.left + row.bottomRight} ${m.top + row.y + row.height} L${m.left + row.bottomLeft} ${m.top + row.y + row.height} Z`;
269
- const valueText = formatValue(row.value, row.percent);
270
- const inside =
271
- showInside &&
272
- primary &&
273
- (hideLegend && !comparison ? row.label : valueText);
274
- const outside = showOutside && !primary ? valueText : showOutside && !comparison ? valueText : null;
275
- return (
276
- <g key={`${seriesIndex}-${row.label}`}>
277
- <path
278
- d={d}
279
- fill={info.color}
280
- fillOpacity={primary ? 1 : 0.14}
281
- stroke={primary ? "none" : info.color}
282
- strokeWidth={primary ? 0 : 1.75}
283
- className="spk-chart-mark"
284
- onMouseEnter={(event) =>
285
- setHover({
286
- x: event.nativeEvent.offsetX,
287
- y: event.nativeEvent.offsetY,
288
- title: row.label,
289
- items: [
290
- {
291
- color: info.color,
292
- label: visibleSeries[seriesIndex]?.label ?? row.label,
293
- value: formatChartNumber(row.value),
294
- },
295
- ],
296
- })
297
- }
298
- onMouseLeave={clear}
299
- />
300
- {inside ? (
301
- <text
302
- className="spk-chart-label"
303
- fill="var(--primary-foreground)"
304
- x={w / 2}
305
- y={m.top + row.y + row.height / 2}
306
- textAnchor="middle"
307
- dominantBaseline="middle"
308
- >
309
- {inside}
310
- </text>
311
- ) : null}
312
- {outside ? (
313
- <text
314
- className="spk-chart-tick"
315
- x={m.left + Math.max(row.topRight, row.bottomRight) + 8}
316
- y={m.top + row.y + row.height / 2}
317
- dominantBaseline="middle"
318
- >
319
- {outside}
320
- </text>
321
- ) : null}
322
- </g>
323
- );
324
- });
325
- })}
326
- </svg>
327
- );
328
- }}
329
- </ChartFrame>
330
- );
331
- }
332
-
333
- export function PyramidChart(props: FunnelChartProps) {
334
- return <FunnelChart {...props} variant="pyramid" />;
335
- }
336
-
337
- export type SankeyChartProps = {
338
- series: { data: SankeyNode[]; links: SankeyLink[] };
339
- height?: number;
340
- width?: number;
341
- colors?: string[];
342
- className?: string;
343
- };
344
-
345
- export function SankeyChart({ series, height = 300, width, colors, className }: SankeyChartProps) {
346
- const { hover, setHover, clear } = useChartHover();
347
- return (
348
- <ChartFrame
349
- slot="sankey-chart"
350
- className={className}
351
- width={width}
352
- height={height}
353
- margin={{ top: 12, right: 88, bottom: 12, left: 12 }}
354
- hover={hover}
355
- aria-label="Sankey chart"
356
- >
357
- {({ width: w, height: h, m }) => {
358
- const layout = sankeyLayout(series.data, series.links, w - m.left - m.right, h - m.top - m.bottom);
359
- return (
360
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
361
- {layout.links.map((link, i) => (
362
- <path
363
- key={`${link.source}-${link.target}-${i}`}
364
- d={link.path}
365
- transform={`translate(${m.left} ${m.top})`}
366
- fill={chartColor(i, colors)}
367
- fillOpacity={0.28}
368
- className="spk-chart-mark"
369
- onMouseEnter={(event) =>
370
- setHover({
371
- x: event.nativeEvent.offsetX,
372
- y: event.nativeEvent.offsetY,
373
- title: `${link.source} → ${link.target}`,
374
- items: [
375
- {
376
- color: chartColor(i, colors),
377
- label: "flow",
378
- value: formatChartNumber(link.value),
379
- },
380
- ],
381
- })
382
- }
383
- onMouseLeave={clear}
384
- />
385
- ))}
386
- {layout.nodes.map((node, i) => (
387
- <g key={node.id} transform={`translate(${m.left} ${m.top})`}>
388
- <rect
389
- x={node.x}
390
- y={node.y}
391
- width={node.width}
392
- height={node.height}
393
- rx={4}
394
- fill={node.color ?? chartColor(i, colors)}
395
- />
396
- <text
397
- className="spk-chart-tick"
398
- x={node.x + node.width + 6}
399
- y={node.y + node.height / 2}
400
- dominantBaseline="middle"
401
- >
402
- {node.label}
403
- </text>
404
- </g>
405
- ))}
406
- </svg>
407
- );
408
- }}
409
- </ChartFrame>
410
- );
411
- }
412
-
413
- export type TreemapProps = {
414
- series: Array<{ data: TreeNode[] }>;
415
- height?: number;
416
- width?: number;
417
- colors?: string[];
418
- className?: string;
419
- };
420
-
421
- export function Treemap({ series, height = 280, width, colors, className }: TreemapProps) {
422
- const { hover, setHover, clear } = useChartHover();
423
- const data = series[0]?.data ?? [];
424
- return (
425
- <ChartFrame
426
- slot="treemap"
427
- className={className}
428
- width={width}
429
- height={height}
430
- margin={{ top: 4, right: 4, bottom: 4, left: 4 }}
431
- hover={hover}
432
- aria-label="Treemap"
433
- >
434
- {({ width: w, height: h, m }) => {
435
- const rects = squarify(data, 0, 0, w - m.left - m.right, h - m.top - m.bottom);
436
- return (
437
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
438
- {rects.map((rect, i) => (
439
- <g key={rect.id} transform={`translate(${m.left} ${m.top})`}>
440
- <rect
441
- x={rect.x + 1.5}
442
- y={rect.y + 1.5}
443
- width={Math.max(0, rect.w - 3)}
444
- height={Math.max(0, rect.h - 3)}
445
- rx={8}
446
- fill={rect.color ?? chartColor(i, colors)}
447
- className="spk-chart-mark"
448
- onMouseEnter={(event) =>
449
- setHover({
450
- x: event.nativeEvent.offsetX,
451
- y: event.nativeEvent.offsetY,
452
- title: rect.label,
453
- items: [
454
- {
455
- color: rect.color ?? chartColor(i, colors),
456
- label: rect.label,
457
- value: formatChartNumber(rect.value),
458
- },
459
- ],
460
- })
461
- }
462
- onMouseLeave={clear}
463
- />
464
- {rect.w > 56 && rect.h > 28 ? (
465
- <text
466
- className="spk-chart-label"
467
- x={rect.x + 10}
468
- y={rect.y + 20}
469
- >
470
- {rect.label}
471
- </text>
472
- ) : null}
473
- </g>
474
- ))}
475
- </svg>
476
- );
477
- }}
478
- </ChartFrame>
479
- );
480
- }
481
-
482
- export type SunburstChartProps = {
483
- series: Array<{ data: TreeNode[] }>;
484
- height?: number;
485
- width?: number;
486
- colors?: string[];
487
- className?: string;
488
- };
489
-
490
- export function SunburstChart({ series, height = 300, width, colors, className }: SunburstChartProps) {
491
- const { hover, setHover, clear } = useChartHover();
492
- const data = series[0]?.data ?? [];
493
- return (
494
- <ChartFrame
495
- slot="sunburst-chart"
496
- className={className}
497
- width={width}
498
- height={height}
499
- margin={{ top: 8, right: 8, bottom: 8, left: 8 }}
500
- hover={hover}
501
- aria-label="Sunburst chart"
502
- >
503
- {({ width: w, height: h }) => {
504
- const cx = w / 2;
505
- const cy = h / 2;
506
- const outer = Math.min(w, h) * 0.42;
507
- const slices = sunburstLayout(data, cx, cy, outer * 0.22, outer);
508
- return (
509
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
510
- {slices.map((slice, i) => (
511
- <path
512
- key={`${slice.id}-${i}`}
513
- d={slice.path}
514
- fill={slice.color ?? chartColor(slice.depth + i, colors)}
515
- stroke="var(--background)"
516
- strokeWidth={1.5}
517
- className="spk-chart-mark"
518
- onMouseEnter={(event) =>
519
- setHover({
520
- x: event.nativeEvent.offsetX,
521
- y: event.nativeEvent.offsetY,
522
- title: slice.label,
523
- items: [
524
- {
525
- color: slice.color ?? chartColor(i, colors),
526
- label: slice.label,
527
- value: formatChartNumber(slice.value),
528
- },
529
- ],
530
- })
531
- }
532
- onMouseLeave={clear}
533
- />
534
- ))}
535
- </svg>
536
- );
537
- }}
538
- </ChartFrame>
539
- );
540
- }
541
-
542
- export type ChordChartProps = {
543
- series: { data: string[]; matrix: number[][] };
544
- height?: number;
545
- width?: number;
546
- colors?: string[];
547
- className?: string;
548
- };
549
-
550
- export function ChordChart({ series, height = 300, width, colors, className }: ChordChartProps) {
551
- const { hover, setHover, clear } = useChartHover();
552
- const meta = seriesMeta(
553
- series.data.map((label) => ({ id: label, label })),
554
- colors,
555
- );
556
- return (
557
- <ChartFrame
558
- slot="chord-chart"
559
- className={className}
560
- width={width}
561
- height={height}
562
- margin={{ top: 24, right: 24, bottom: 24, left: 24 }}
563
- legend={meta}
564
- hover={hover}
565
- aria-label="Chord chart"
566
- >
567
- {({ width: w, height: h }) => {
568
- const cx = w / 2;
569
- const cy = h / 2;
570
- const outer = Math.min(w, h) * 0.38;
571
- const inner = outer * 0.9;
572
- const layout = chordLayout(series.data, series.matrix, cx, cy, inner, outer);
573
- return (
574
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
575
- {layout.ribbons.map((ribbon, i) => (
576
- <path
577
- key={`${ribbon.source}-${ribbon.target}-${i}`}
578
- d={ribbon.path}
579
- fill={chartColor(ribbon.source, colors)}
580
- fillOpacity={0.28}
581
- className="spk-chart-mark"
582
- onMouseEnter={(event) =>
583
- setHover({
584
- x: event.nativeEvent.offsetX,
585
- y: event.nativeEvent.offsetY,
586
- title: `${series.data[ribbon.source]} → ${series.data[ribbon.target]}`,
587
- items: [
588
- {
589
- color: chartColor(ribbon.source, colors),
590
- label: "flow",
591
- value: formatChartNumber(ribbon.value),
592
- },
593
- ],
594
- })
595
- }
596
- onMouseLeave={clear}
597
- />
598
- ))}
599
- {layout.groups.map((group) => (
600
- <path key={group.label} d={group.path} fill={chartColor(group.index, colors)} />
601
- ))}
602
- </svg>
603
- );
604
- }}
605
- </ChartFrame>
606
- );
607
- }
@@ -1,74 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
- import { Gauge, LinearGauge, PieChart, RadarChart } from "./radial-charts";
4
-
5
- describe("radial-charts", () => {
6
- it("renders a pie chart from slice values", () => {
7
- const { container } = render(
8
- <PieChart
9
- width={200}
10
- height={200}
11
- series={[{ data: [{ value: 10, label: "A" }, { value: 20, label: "B" }] }]}
12
- />,
13
- );
14
- expect(container.querySelector('[data-slot="pie-chart"]')).toBeTruthy();
15
- expect(screen.getByRole("button", { name: "A" })).toBeInTheDocument();
16
- });
17
-
18
- it("renders a single-value gauge with a percent label", () => {
19
- render(<Gauge value={40} width={160} height={120} />);
20
- expect(screen.getByRole("img", { name: "Gauge 40" })).toBeInTheDocument();
21
- expect(screen.getByText("40%")).toBeInTheDocument();
22
- });
23
-
24
- it("renders a segmented gauge with a section legend", () => {
25
- const { container } = render(
26
- <Gauge
27
- width={160}
28
- height={120}
29
- startAngle={-90}
30
- endAngle={90}
31
- sections={[
32
- { value: 40, label: "v5" },
33
- { value: 35, label: "v6" },
34
- { value: 25, label: "v7" },
35
- ]}
36
- />,
37
- );
38
- expect(container.querySelector('[data-slot="gauge"]')).toBeTruthy();
39
- expect(screen.getByRole("button", { name: "v5" })).toBeInTheDocument();
40
- expect(container.querySelectorAll('[data-slot="gauge"] path').length).toBe(3);
41
- });
42
-
43
- it("hides the section legend and accepts a text formatter", () => {
44
- render(
45
- <Gauge
46
- value={72}
47
- width={160}
48
- height={120}
49
- hideLegend
50
- text={(value) => `${value} pts`}
51
- sections={[{ value: 1, label: "hidden" }]}
52
- />,
53
- );
54
- expect(screen.queryByRole("button", { name: "hidden" })).not.toBeInTheDocument();
55
- expect(screen.getByText("72 pts")).toBeInTheDocument();
56
- });
57
-
58
- it("renders a linear gauge and a radar polygon", () => {
59
- const { container } = render(
60
- <div>
61
- <LinearGauge value={25} width={200} />
62
- <RadarChart
63
- width={220}
64
- height={220}
65
- radar={{ metrics: ["A", "B", "C"] }}
66
- series={[{ label: "One", data: [3, 5, 4] }]}
67
- />
68
- </div>,
69
- );
70
- expect(container.querySelector('[data-slot="linear-gauge"]')).toBeTruthy();
71
- expect(container.querySelector('[data-slot="radar-chart"]')).toBeTruthy();
72
- expect(screen.getByRole("button", { name: "One" })).toBeInTheDocument();
73
- });
74
- });