@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,318 +0,0 @@
1
- import {
2
- extent,
3
- formatChartNumber,
4
- niceTicks,
5
- pieSlices,
6
- type ChartAxisConfig,
7
- type ChartMargin,
8
- } from "../lib/charts";
9
- import { ellipseSlicePath, ellipseWallPath, isoBoxFaces, isoPoint } from "../lib/iso";
10
- import {
11
- CartesianGrid,
12
- ChartFrame,
13
- categoryLabels,
14
- formatSeriesValue,
15
- seriesMeta,
16
- useChartHover,
17
- useHiddenSeries,
18
- } from "./chart-ui";
19
- import type { NumericSeries } from "./cartesian-charts";
20
-
21
- function box(width: number, height: number, m: Required<ChartMargin>) {
22
- return {
23
- left: m.left,
24
- right: width - m.right,
25
- top: m.top,
26
- bottom: height - m.bottom,
27
- iw: Math.max(1, width - m.left - m.right),
28
- ih: Math.max(1, height - m.top - m.bottom),
29
- };
30
- }
31
-
32
- export type BarChart3DProps = {
33
- series: NumericSeries[];
34
- xAxis?: ChartAxisConfig[];
35
- yAxis?: ChartAxisConfig[];
36
- height?: number;
37
- width?: number;
38
- colors?: string[];
39
- hideLegend?: boolean;
40
- hideGrid?: boolean;
41
- margin?: ChartMargin;
42
- className?: string;
43
- depth?: number;
44
- };
45
-
46
- export function BarChart3D({
47
- series,
48
- xAxis,
49
- yAxis,
50
- height = 280,
51
- width,
52
- colors,
53
- hideLegend,
54
- hideGrid,
55
- margin,
56
- className,
57
- depth = 14,
58
- }: BarChart3DProps) {
59
- const meta = seriesMeta(series, colors);
60
- const ids = meta.map((item) => item.id);
61
- const { hidden, toggle } = useHiddenSeries(ids);
62
- const { hover, setHover, clear } = useChartHover();
63
- const categories = categoryLabels(xAxis?.[0], series[0]?.data?.length ?? 0);
64
- const values = series.flatMap((item) => (item.data ?? []).map((v) => v ?? 0));
65
- const domain: [number, number] = [
66
- yAxis?.[0]?.min ?? Math.min(0, extent(values, 0.08)[0]),
67
- yAxis?.[0]?.max ?? extent(values, 0.08)[1],
68
- ];
69
- const ticks = niceTicks(domain[0], domain[1], 4);
70
-
71
- return (
72
- <ChartFrame
73
- slot="bar-chart-3d"
74
- className={className}
75
- width={width}
76
- height={height}
77
- margin={margin ?? { top: 24, right: 28, bottom: 40, left: 48 }}
78
- legend={hideLegend ? undefined : meta}
79
- hiddenIds={hidden}
80
- onToggleSeries={toggle}
81
- hover={hover}
82
- aria-label="3D bar chart"
83
- >
84
- {({ width: w, height: h, m }) => {
85
- const plot = box(w, h, m);
86
- const originX = plot.left + plot.iw * 0.16;
87
- const originY = plot.bottom - 6;
88
- const visible = series
89
- .map((item, s) => ({ item, info: meta[s]!, s }))
90
- .filter(({ info }) => !hidden.has(info.id));
91
- const n = Math.max(categories.length, 1);
92
- const groupW = Math.max(18, (plot.iw * 0.72) / n);
93
- const barW = Math.max(8, (groupW * 0.78) / Math.max(visible.length, 1));
94
- const gap = Math.max(2, (groupW - barW * visible.length) / Math.max(visible.length + 1, 1));
95
- const maxH = plot.ih * 0.78;
96
- const yScale = (value: number) =>
97
- ((value - domain[0]) / (domain[1] - domain[0] || 1)) * maxH;
98
- const yAt = (value: number) => originY - yScale(value);
99
-
100
- return (
101
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
102
- {hideGrid ? null : (
103
- <CartesianGrid
104
- ticks={ticks}
105
- scale={yAt}
106
- left={plot.left}
107
- right={plot.right}
108
- top={plot.top}
109
- bottom={plot.bottom}
110
- />
111
- )}
112
- {ticks.map((tick) => (
113
- <text key={tick} className="spk-chart-tick" x={plot.left - 8} y={yAt(tick)} textAnchor="end">
114
- {formatChartNumber(tick)}
115
- </text>
116
- ))}
117
- {categories.map((label, i) => {
118
- const x0 = i * groupW;
119
- const foot = isoPoint(x0 + groupW / 2, 0, depth, originX, originY);
120
- return (
121
- <text
122
- key={String(label)}
123
- className="spk-chart-tick"
124
- x={foot.x}
125
- y={plot.bottom + 16}
126
- textAnchor="middle"
127
- >
128
- {String(label)}
129
- </text>
130
- );
131
- })}
132
- {categories.flatMap((_, i) =>
133
- visible.map(({ item, info }, s) => {
134
- const value = item.data?.[i] ?? 0;
135
- const barH = Math.max(2, yScale(value));
136
- const x = i * groupW + gap + s * (barW + gap * 0.2);
137
- const faces = isoBoxFaces(x, 0, 0, barW, barH, depth, originX, originY);
138
- const color = info.color;
139
- return (
140
- <g
141
- key={`${info.id}-${i}`}
142
- className="spk-chart-mark"
143
- onMouseEnter={(event) =>
144
- setHover({
145
- x: event.nativeEvent.offsetX,
146
- y: event.nativeEvent.offsetY,
147
- title: String(categories[i]),
148
- items: [
149
- {
150
- color,
151
- label: info.label,
152
- value: formatSeriesValue(value),
153
- },
154
- ],
155
- })
156
- }
157
- onMouseLeave={clear}
158
- >
159
- <path d={faces.side} fill={`color-mix(in srgb, ${color} 78%, black)`} />
160
- <path d={faces.top} fill={`color-mix(in srgb, ${color} 72%, white)`} />
161
- <path d={faces.front} fill={color} />
162
- </g>
163
- );
164
- }),
165
- )}
166
- </svg>
167
- );
168
- }}
169
- </ChartFrame>
170
- );
171
- }
172
-
173
- export type PieChart3DProps = {
174
- series: Array<{
175
- data: Array<{ id?: string | number; value: number; label?: string; color?: string }>;
176
- innerRadius?: number;
177
- paddingAngle?: number;
178
- }>;
179
- height?: number;
180
- width?: number;
181
- colors?: string[];
182
- hideLegend?: boolean;
183
- margin?: ChartMargin;
184
- className?: string;
185
- thickness?: number;
186
- };
187
-
188
- export function PieChart3D({
189
- series,
190
- height = 280,
191
- width,
192
- colors,
193
- hideLegend,
194
- margin,
195
- className,
196
- thickness = 18,
197
- }: PieChart3DProps) {
198
- const first = series[0] ?? { data: [] };
199
- const meta = seriesMeta(
200
- first.data.map((item, i) => ({
201
- id: String(item.id ?? item.label ?? i),
202
- label: item.label ?? `Slice ${i + 1}`,
203
- color: item.color,
204
- })),
205
- colors,
206
- );
207
- const ids = meta.map((item) => item.id);
208
- const { hidden, toggle } = useHiddenSeries(ids);
209
- const { hover, setHover, clear } = useChartHover();
210
-
211
- return (
212
- <ChartFrame
213
- slot="pie-chart-3d"
214
- className={className}
215
- width={width}
216
- height={height}
217
- margin={margin ?? { top: 12, right: 12, bottom: 20, left: 12 }}
218
- legend={hideLegend ? undefined : meta}
219
- hiddenIds={hidden}
220
- onToggleSeries={toggle}
221
- hover={hover}
222
- aria-label="3D pie chart"
223
- >
224
- {({ width: w, height: h, m }) => {
225
- const cx = w / 2;
226
- const cy = (h - thickness) / 2;
227
- const rx = Math.min(w - m.left - m.right, (h - m.top - m.bottom - thickness) * 1.7) / 2;
228
- const ry = rx * 0.42;
229
- const visible = first.data.map((item, i) => (hidden.has(meta[i]!.id) ? 0 : item.value));
230
- const slices = pieSlices(visible, {
231
- cx,
232
- cy,
233
- innerRadius: 0,
234
- outerRadius: rx,
235
- paddingAngle: first.paddingAngle,
236
- });
237
- const ordered = slices
238
- .map((slice, i) => ({ slice, info: meta[i]!, item: first.data[i]! }))
239
- .filter(({ info, slice }) => !hidden.has(info.id) && slice.path)
240
- .sort((a, b) => ellipsePointY(cx, cy, rx, ry, a.slice.midAngle) - ellipsePointY(cx, cy, rx, ry, b.slice.midAngle));
241
-
242
- return (
243
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
244
- {ordered.map(({ slice, info, item }) => (
245
- <g
246
- key={`${info.id}-wall`}
247
- className="spk-chart-mark"
248
- onMouseEnter={(event) =>
249
- setHover({
250
- x: event.nativeEvent.offsetX,
251
- y: event.nativeEvent.offsetY,
252
- title: info.label,
253
- items: [
254
- {
255
- color: info.color,
256
- label: info.label,
257
- value: `${formatChartNumber(item.value)} (${Math.round(slice.percent * 100)}%)`,
258
- },
259
- ],
260
- })
261
- }
262
- onMouseLeave={clear}
263
- >
264
- <path
265
- d={ellipseWallPath(cx, cy, rx, ry, slice.startAngle, slice.endAngle, thickness)}
266
- fill={`color-mix(in srgb, ${info.color} 70%, black)`}
267
- />
268
- </g>
269
- ))}
270
- {ordered.map(({ slice, info, item }) => (
271
- <g
272
- key={info.id}
273
- className="spk-chart-mark"
274
- onMouseEnter={(event) =>
275
- setHover({
276
- x: event.nativeEvent.offsetX,
277
- y: event.nativeEvent.offsetY,
278
- title: info.label,
279
- items: [
280
- {
281
- color: info.color,
282
- label: info.label,
283
- value: `${formatChartNumber(item.value)} (${Math.round(slice.percent * 100)}%)`,
284
- },
285
- ],
286
- })
287
- }
288
- onMouseLeave={clear}
289
- >
290
- <path
291
- d={ellipseSlicePath(cx, cy, rx, ry, slice.startAngle, slice.endAngle)}
292
- fill={info.color}
293
- />
294
- {slice.percent > 0.08 ? (
295
- <text
296
- className="spk-chart-label"
297
- x={cx + Math.cos(((slice.midAngle - 90) * Math.PI) / 180) * rx * 0.55}
298
- y={cy + Math.sin(((slice.midAngle - 90) * Math.PI) / 180) * ry * 0.55}
299
- textAnchor="middle"
300
- dominantBaseline="middle"
301
- fill="var(--primary-foreground)"
302
- >
303
- {Math.round(slice.percent * 100)}%
304
- </text>
305
- ) : null}
306
- </g>
307
- ))}
308
- </svg>
309
- );
310
- }}
311
- </ChartFrame>
312
- );
313
- }
314
-
315
- function ellipsePointY(cx: number, cy: number, rx: number, ry: number, angleDeg: number) {
316
- const rad = ((angleDeg - 90) * Math.PI) / 180;
317
- return cy + ry * Math.sin(rad);
318
- }
@@ -1,254 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, expect, it } from "vitest";
4
- import { ChartContainer } from "./chart-composition";
5
- import { BarChart, LineChart, ScatterChart, SparkLineChart, WaterfallChart } from "./cartesian-charts";
6
- import { Gauge, LinearGauge, PieChart, RadarChart } from "./radial-charts";
7
- import { FunnelChart, Heatmap, SankeyChart, Treemap } from "./flow-charts";
8
- import { MapChart } from "./MapChart";
9
- import { BarChart3D, PieChart3D } from "./charts-3d";
10
- import { ChartDataGrid } from "./ChartDataGrid";
11
-
12
- const months = ["Jan", "Feb", "Mar"];
13
-
14
- describe("charts", () => {
15
- it("renders a grouped bar chart and shows a tooltip on hover", async () => {
16
- const user = userEvent.setup();
17
- render(
18
- <BarChart
19
- width={360}
20
- height={220}
21
- xAxis={[{ data: months }]}
22
- series={[
23
- { label: "Views", data: [4, 3, 5] },
24
- { label: "Signups", data: [1, 2, 2] },
25
- ]}
26
- />,
27
- );
28
- expect(screen.getByRole("img", { name: /bar chart/i })).toBeInTheDocument();
29
- const marks = document.querySelectorAll('[data-slot="bar-plot"] .spk-chart-mark');
30
- expect(marks.length).toBeGreaterThan(0);
31
- await user.hover(marks[0]!);
32
- expect(screen.getByRole("tooltip")).toBeInTheDocument();
33
- });
34
-
35
- it("hides a line series from the legend", async () => {
36
- const user = userEvent.setup();
37
- render(
38
- <LineChart
39
- width={360}
40
- height={220}
41
- xAxis={[{ data: months }]}
42
- series={[
43
- { label: "Revenue", data: [2, 4, 3] },
44
- { label: "Cost", data: [1, 1, 2] },
45
- ]}
46
- />,
47
- );
48
- const paths = () => document.querySelectorAll('[data-slot="line-plot"] path');
49
- const before = paths().length;
50
- await user.click(screen.getByRole("button", { name: "Cost" }));
51
- expect(paths().length).toBeLessThan(before);
52
- });
53
-
54
- it("renders pie, gauge, radar, scatter, sparkline, and linear gauge", () => {
55
- const { container } = render(
56
- <div>
57
- <PieChart
58
- width={200}
59
- height={200}
60
- series={[{ data: [{ value: 10, label: "A" }, { value: 20, label: "B" }] }]}
61
- />
62
- <Gauge value={40} width={160} height={120} />
63
- <Gauge
64
- width={160}
65
- height={120}
66
- startAngle={-90}
67
- endAngle={90}
68
- sections={[
69
- { value: 40, label: "v5" },
70
- { value: 35, label: "v6" },
71
- { value: 25, label: "v7" },
72
- ]}
73
- />
74
- <LinearGauge value={25} width={200} />
75
- <RadarChart
76
- width={220}
77
- height={220}
78
- radar={{ metrics: ["A", "B", "C"] }}
79
- series={[{ label: "One", data: [3, 5, 4] }]}
80
- />
81
- <ScatterChart
82
- width={220}
83
- height={180}
84
- series={[{ label: "Pts", data: [{ x: 1, y: 2, id: 1 }] }]}
85
- />
86
- <SparkLineChart data={[1, 4, 2, 6]} width={160} height={48} />
87
- </div>,
88
- );
89
- expect(container.querySelector('[data-slot="pie-chart"]')).toBeTruthy();
90
- expect(container.querySelector('[data-slot="gauge"]')).toBeTruthy();
91
- expect(screen.getByRole("button", { name: "v5" })).toBeInTheDocument();
92
- expect(container.querySelector('[data-slot="linear-gauge"]')).toBeTruthy();
93
- expect(container.querySelector('[data-slot="radar-chart"]')).toBeTruthy();
94
- expect(container.querySelector('[data-slot="scatter-chart"]')).toBeTruthy();
95
- expect(container.querySelector('[data-slot="sparkline"]')).toBeTruthy();
96
- });
97
-
98
- it("renders heatmap, funnel, sankey, treemap, and waterfall", () => {
99
- const { container } = render(
100
- <div>
101
- <Heatmap
102
- width={240}
103
- height={160}
104
- xAxis={[{ data: ["A", "B"] }]}
105
- yAxis={[{ data: ["1"] }]}
106
- series={[{ data: [{ x: 0, y: 0, value: 2 }, { x: 1, y: 0, value: 5 }] }]}
107
- />
108
- <FunnelChart
109
- width={240}
110
- height={180}
111
- series={[{ data: [{ label: "Visit", value: 10 }, { label: "Paid", value: 3 }] }]}
112
- />
113
- <SankeyChart
114
- width={280}
115
- height={180}
116
- series={{
117
- data: [{ id: "a" }, { id: "b" }],
118
- links: [{ source: "a", target: "b", value: 4 }],
119
- }}
120
- />
121
- <Treemap
122
- width={240}
123
- height={160}
124
- series={[{ data: [{ label: "A", value: 8 }, { label: "B", value: 4 }] }]}
125
- />
126
- <WaterfallChart
127
- width={280}
128
- height={180}
129
- xAxis={[{ data: ["Start", "Up", "End"] }]}
130
- series={[{ data: [10, 4, null] }]}
131
- />
132
- </div>,
133
- );
134
- expect(container.querySelector('[data-slot="heatmap"]')).toBeTruthy();
135
- expect(container.querySelector('[data-slot="funnel-chart"]')).toBeTruthy();
136
- expect(container.querySelector('[data-slot="sankey-chart"]')).toBeTruthy();
137
- expect(container.querySelector('[data-slot="treemap"]')).toBeTruthy();
138
- expect(container.querySelector('[data-slot="waterfall-chart"]')).toBeTruthy();
139
- });
140
-
141
- it("composes bar and line plots from a dataset with a toolbar", async () => {
142
- const user = userEvent.setup();
143
- const { container } = render(
144
- <ChartContainer
145
- width={360}
146
- height={220}
147
- zoom
148
- showToolbar
149
- dataset={[
150
- { month: "Jan", views: 4, conv: 1 },
151
- { month: "Feb", views: 6, conv: 2 },
152
- { month: "Mar", views: 5, conv: 2 },
153
- ]}
154
- xAxis={[{ dataKey: "month" }]}
155
- yAxis={[{ id: "left" }, { id: "right", position: "right" }]}
156
- series={[
157
- { type: "bar", dataKey: "views", label: "Views" },
158
- { type: "line", dataKey: "conv", label: "Conv", yAxisId: "right" },
159
- ]}
160
- />,
161
- );
162
- expect(container.querySelector('[data-slot="bar-plot"]')).toBeTruthy();
163
- expect(container.querySelector('[data-slot="line-plot"]')).toBeTruthy();
164
- expect(container.querySelector('[data-slot="chart-y-axis-right"]')).toBeTruthy();
165
- expect(screen.getByRole("toolbar", { name: "Chart tools" })).toBeInTheDocument();
166
- await user.click(screen.getByRole("button", { name: "Zoom in" }));
167
- expect(container.querySelector("svg")).toBeTruthy();
168
- });
169
-
170
- it("renders a map, isometric 3d charts, and a sparkline bar", () => {
171
- const { container } = render(
172
- <div>
173
- <MapChart
174
- width={240}
175
- height={160}
176
- geoData={{
177
- type: "FeatureCollection",
178
- features: [
179
- {
180
- type: "Feature",
181
- id: "a",
182
- properties: { name: "A" },
183
- geometry: {
184
- type: "Polygon",
185
- coordinates: [
186
- [
187
- [0, 0],
188
- [4, 0],
189
- [4, 3],
190
- [0, 3],
191
- [0, 0],
192
- ],
193
- ],
194
- },
195
- },
196
- ],
197
- }}
198
- series={[{ data: [{ id: "a", value: 12, label: "A" }] }]}
199
- />
200
- <BarChart3D
201
- width={240}
202
- height={160}
203
- xAxis={[{ data: ["A", "B"] }]}
204
- series={[{ label: "Views", data: [3, 5] }]}
205
- />
206
- <PieChart3D
207
- width={200}
208
- height={160}
209
- series={[{ data: [{ value: 2, label: "A" }, { value: 3, label: "B" }] }]}
210
- />
211
- <SparkLineChart data={[1, 3, 2, 5]} plotType="bar" width={120} height={36} />
212
- <ScatterChart
213
- width={200}
214
- height={140}
215
- renderer="webgl"
216
- series={[{ label: "Pts", data: [{ x: 1, y: 2, id: 1 }] }]}
217
- />
218
- </div>,
219
- );
220
- expect(container.querySelector('[data-slot="map-chart"]')).toBeTruthy();
221
- expect(container.querySelector('[data-slot="map-chart"] path')).toBeTruthy();
222
- expect(container.querySelector('[data-slot="bar-chart-3d"]')).toBeTruthy();
223
- expect(container.querySelector('[data-slot="pie-chart-3d"]')).toBeTruthy();
224
- expect(container.querySelector('[data-slot="sparkline"] rect')).toBeTruthy();
225
- expect(container.querySelector('[data-slot="scatter-chart"]')).toBeTruthy();
226
- });
227
-
228
- it("highlights a chart bar from a selected grid row", async () => {
229
- const user = userEvent.setup();
230
- const { container } = render(
231
- <ChartDataGrid
232
- height={140}
233
- categoryField="name"
234
- valueField="views"
235
- rows={[
236
- { id: "web", name: "Web", views: 10, trend: [1, 2, 3] },
237
- { id: "ios", name: "iOS", views: 6, trend: [2, 2, 1] },
238
- ]}
239
- columns={[
240
- { field: "name", headerName: "Channel" },
241
- { field: "views", headerName: "Views", type: "number" },
242
- { field: "trend", headerName: "Trend", type: "sparkline" },
243
- ]}
244
- />,
245
- );
246
- expect(container.querySelector('[data-slot="chart-data-grid"]')).toBeTruthy();
247
- const row = container.querySelector(
248
- '[data-slot="chart-data-grid"] tbody [data-slot="table-row"]:nth-child(2)',
249
- );
250
- expect(row).toBeTruthy();
251
- await user.click(row!);
252
- expect(row).toHaveAttribute("data-state", "selected");
253
- });
254
- });
@@ -1,98 +0,0 @@
1
- import { fireEvent, render, screen } from "@testing-library/react";
2
- import { describe, expect, it, vi } from "vitest";
3
- import { FunnelChart, Heatmap } from "./flow-charts";
4
-
5
- describe("FunnelChart", () => {
6
- it("renders a comparison overlay with inside and outside value labels", () => {
7
- render(
8
- <FunnelChart
9
- width={280}
10
- height={200}
11
- valueFormatter={(value) => `${value}%`}
12
- series={[
13
- {
14
- label: "2000",
15
- data: [
16
- { label: "Primary", value: 85 },
17
- { label: "Secondary", value: 50 },
18
- ],
19
- },
20
- {
21
- label: "2020",
22
- data: [
23
- { label: "Primary", value: 90 },
24
- { label: "Secondary", value: 67 },
25
- ],
26
- },
27
- ]}
28
- />,
29
- );
30
- expect(screen.getByRole("img", { name: /funnel chart/i })).toBeInTheDocument();
31
- expect(screen.getByText("85%")).toBeInTheDocument();
32
- expect(screen.getByText("90%")).toBeInTheDocument();
33
- expect(screen.getByText("67%")).toBeInTheDocument();
34
- expect(document.querySelectorAll('[data-slot="funnel-chart"] path').length).toBe(4);
35
- });
36
-
37
- it("keeps stage names on a single funnel when the legend is hidden", () => {
38
- render(
39
- <FunnelChart
40
- width={240}
41
- height={160}
42
- hideLegend
43
- series={[{ data: [{ label: "Visit", value: 10 }, { label: "Paid", value: 3 }] }]}
44
- />,
45
- );
46
- expect(screen.getByText("Visit")).toBeInTheDocument();
47
- expect(screen.getByText("Paid")).toBeInTheDocument();
48
- });
49
- });
50
-
51
- describe("Heatmap", () => {
52
- it("draws a color scale for the value domain", () => {
53
- const { container } = render(
54
- <Heatmap
55
- width={240}
56
- height={160}
57
- xAxis={[{ data: ["A", "B"] }]}
58
- yAxis={[{ data: ["1"] }]}
59
- series={[{ data: [{ x: 0, y: 0, value: 2 }, { x: 1, y: 0, value: 8 }] }]}
60
- />,
61
- );
62
- expect(container.querySelector('[data-slot="heatmap-color-scale"]')).toBeTruthy();
63
- expect(screen.getByText("8")).toBeInTheDocument();
64
- });
65
-
66
- it("hides the color scale when hideLegend is set", () => {
67
- const { container } = render(
68
- <Heatmap
69
- width={240}
70
- height={160}
71
- hideLegend
72
- xAxis={[{ data: ["A"] }]}
73
- yAxis={[{ data: ["1"] }]}
74
- series={[{ data: [{ x: 0, y: 0, value: 4 }] }]}
75
- />,
76
- );
77
- expect(container.querySelector('[data-slot="heatmap-color-scale"]')).toBeNull();
78
- });
79
-
80
- it("fires onItemClick and can label cells", () => {
81
- const onItemClick = vi.fn();
82
- const { container } = render(
83
- <Heatmap
84
- width={240}
85
- height={160}
86
- hideLegend
87
- showCellLabels
88
- xAxis={[{ data: ["A"] }]}
89
- yAxis={[{ data: ["1"] }]}
90
- series={[{ data: [{ x: 0, y: 0, value: 4 }] }]}
91
- onItemClick={onItemClick}
92
- />,
93
- );
94
- expect(screen.getByText("4")).toBeInTheDocument();
95
- fireEvent.click(container.querySelector(".spk-chart-mark--interactive")!);
96
- expect(onItemClick).toHaveBeenCalledWith(expect.objectContaining({ value: 4, category: "A · 1" }));
97
- });
98
- });