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