@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,1287 +0,0 @@
1
- import { useCallback, useState, type MouseEvent, type ReactNode } from "react";
2
- import {
3
- areaPath,
4
- bandScale,
5
- boxPlotStats,
6
- chartColor,
7
- extent,
8
- formatChartNumber,
9
- inferScaleType,
10
- linePath,
11
- linearScale,
12
- niceTicks,
13
- pointScale,
14
- rangeAreaPath,
15
- roundedRectPath,
16
- stackSeries,
17
- waterfallBars,
18
- type BoxPlotStats,
19
- type ChartAxisConfig,
20
- type ChartCurve,
21
- type ChartMargin,
22
- } from "../lib/charts";
23
- import {
24
- CartesianGrid,
25
- CategoryAxis,
26
- ChartFrame,
27
- ValueAxis,
28
- categoryLabels,
29
- formatSeriesValue,
30
- seriesMeta,
31
- useChartHover,
32
- useHiddenSeries,
33
- type ChartHover,
34
- } from "./chart-ui";
35
- import { ChartContainer } from "./chart-composition";
36
- import { ScatterWebGLLayer } from "./ScatterWebGL";
37
- import {
38
- nearestScatterHit,
39
- shouldUseWebGL,
40
- svgPointerPosition,
41
- type ScatterRenderer,
42
- } from "../lib/webgl-scatter";
43
-
44
- export type { ScatterRenderer };
45
-
46
- export type NumericSeries = {
47
- id?: string;
48
- label?: string;
49
- data?: Array<number | null>;
50
- dataKey?: string;
51
- color?: string;
52
- stack?: string;
53
- area?: boolean;
54
- curve?: ChartCurve;
55
- showMark?: boolean;
56
- yAxisId?: string;
57
- type?: "bar" | "line" | "area";
58
- };
59
-
60
- export type CartesianChartProps = {
61
- xAxis?: ChartAxisConfig[];
62
- yAxis?: ChartAxisConfig[];
63
- series: NumericSeries[];
64
- dataset?: import("../lib/charts").ChartDataset;
65
- height?: number;
66
- width?: number;
67
- colors?: string[];
68
- hideLegend?: boolean;
69
- hideGrid?: boolean;
70
- margin?: ChartMargin;
71
- className?: string;
72
- zoom?: boolean | import("../lib/charts").ChartZoom;
73
- onZoomChange?: (zoom: import("../lib/charts").ChartZoom) => void;
74
- showToolbar?: boolean;
75
- highlightScope?: import("../lib/charts").HighlightScope;
76
- animated?: boolean;
77
- locale?: string;
78
- };
79
-
80
- export type BarChartProps = CartesianChartProps & {
81
- layout?: "vertical" | "horizontal";
82
- stacked?: boolean;
83
- borderRadius?: number;
84
- categoryGapRatio?: number;
85
- barGapRatio?: number;
86
- };
87
-
88
- export type LineChartProps = CartesianChartProps & {
89
- curve?: ChartCurve;
90
- area?: boolean;
91
- showMark?: boolean;
92
- stacked?: boolean;
93
- };
94
-
95
- export type ScatterPoint = {
96
- id?: string | number;
97
- x: number;
98
- y: number;
99
- z?: number;
100
- label?: string;
101
- };
102
-
103
- export type ScatterSeries = {
104
- id?: string;
105
- label?: string;
106
- color?: string;
107
- data: ScatterPoint[];
108
- };
109
-
110
- type PlotBox = { left: number; right: number; top: number; bottom: number; iw: number; ih: number };
111
-
112
- function box(width: number, height: number, m: Required<ChartMargin>): PlotBox {
113
- return {
114
- left: m.left,
115
- right: width - m.right,
116
- top: m.top,
117
- bottom: height - m.bottom,
118
- iw: Math.max(1, width - m.left - m.right),
119
- ih: Math.max(1, height - m.top - m.bottom),
120
- };
121
- }
122
-
123
- function numericDomain(series: NumericSeries[], stacked: boolean, axis?: ChartAxisConfig): [number, number] {
124
- if (axis?.min != null && axis?.max != null) return [axis.min, axis.max];
125
- const values: number[] = [];
126
- if (stacked) {
127
- const stacks = stackSeries(series.map((item) => (item.data ?? []).map((v) => v ?? 0)));
128
- for (const segs of stacks) for (const seg of segs) values.push(seg.start, seg.end);
129
- } else {
130
- for (const item of series) for (const value of item.data ?? []) if (value != null) values.push(value);
131
- }
132
- if (!values.length) values.push(0, 1);
133
- if (Math.min(...values) > 0) values.push(0);
134
- const [min, max] = extent(values, 0.04);
135
- return [axis?.min ?? min, axis?.max ?? max];
136
- }
137
-
138
- function CartesianShell({
139
- slot,
140
- ariaLabel,
141
- series,
142
- colors,
143
- hideLegend,
144
- hideGrid,
145
- xAxis,
146
- yAxis,
147
- width,
148
- height,
149
- margin,
150
- className,
151
- domain,
152
- categories,
153
- categoryMode = "band",
154
- children,
155
- }: {
156
- slot: string;
157
- ariaLabel: string;
158
- series: Array<{ id?: string; label?: string; color?: string }>;
159
- colors?: string[];
160
- hideLegend?: boolean;
161
- hideGrid?: boolean;
162
- xAxis?: ChartAxisConfig[];
163
- yAxis?: ChartAxisConfig[];
164
- width?: number;
165
- height?: number;
166
- margin?: ChartMargin;
167
- className?: string;
168
- domain: [number, number];
169
- categories: Array<string | number | Date>;
170
- categoryMode?: "band" | "point" | "linear";
171
- children: (ctx: {
172
- plot: PlotBox;
173
- xAt: (index: number) => number;
174
- yAt: (value: number) => number;
175
- bandwidth: number;
176
- ticks: number[];
177
- hidden: Set<string>;
178
- meta: ReturnType<typeof seriesMeta>;
179
- setHover: (hover: ChartHover) => void;
180
- clear: () => void;
181
- }) => ReactNode;
182
- }) {
183
- const meta = seriesMeta(series, colors);
184
- const ids = meta.map((item) => item.id);
185
- const { hidden, toggle } = useHiddenSeries(ids);
186
- const { hover, setHover, clear } = useChartHover();
187
- const ticks = niceTicks(domain[0], domain[1], 5);
188
- const xConfig = xAxis?.[0];
189
-
190
- return (
191
- <ChartFrame
192
- slot={slot}
193
- className={className}
194
- width={width}
195
- height={height}
196
- margin={margin}
197
- legend={hideLegend ? undefined : meta}
198
- hiddenIds={hidden}
199
- onToggleSeries={toggle}
200
- hover={hover}
201
- aria-label={ariaLabel}
202
- >
203
- {({ width: w, height: h, m }) => {
204
- const plot = box(w, h, m);
205
- const yAt = linearScale(domain, [plot.bottom, plot.top]);
206
- let xAt = (index: number) => plot.left + plot.iw * (categories.length <= 1 ? 0.5 : index / (categories.length - 1));
207
- let bandwidth = 0;
208
- if (categoryMode === "band") {
209
- const scale = bandScale(
210
- categories.map(String),
211
- [plot.left, plot.right],
212
- 0.24,
213
- );
214
- xAt = (index) => scale(String(categories[index]));
215
- bandwidth = scale.bandwidth;
216
- } else if (categoryMode === "point") {
217
- const scale = pointScale(categories.map(String), [plot.left, plot.right]);
218
- xAt = (index) => scale(String(categories[index]));
219
- } else if (xConfig?.data?.every((v) => typeof v === "number" || v instanceof Date)) {
220
- const xs = xConfig.data.map((v) => (v instanceof Date ? v.getTime() : Number(v)));
221
- const [min, max] = extent(xs);
222
- const scale = linearScale([min, max], [plot.left, plot.right]);
223
- xAt = (index) => scale(xs[index] ?? min);
224
- }
225
- return (
226
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
227
- {hideGrid ? null : (
228
- <CartesianGrid
229
- ticks={ticks}
230
- scale={yAt}
231
- left={plot.left}
232
- right={plot.right}
233
- top={plot.top}
234
- bottom={plot.bottom}
235
- />
236
- )}
237
- {yAxis?.[0]?.hide ? null : <ValueAxis ticks={ticks} scale={yAt} left={plot.left} />}
238
- {xAxis?.[0]?.hide ? null : (
239
- <CategoryAxis labels={categories} position={xAt} y={plot.bottom} />
240
- )}
241
- {children({ plot, xAt, yAt, bandwidth, ticks, hidden, meta, setHover, clear })}
242
- </svg>
243
- );
244
- }}
245
- </ChartFrame>
246
- );
247
- }
248
-
249
- export function BarChart({
250
- xAxis,
251
- yAxis,
252
- series,
253
- dataset,
254
- height,
255
- width,
256
- layout = "vertical",
257
- stacked = false,
258
- borderRadius = 6,
259
- colors,
260
- hideLegend,
261
- hideGrid,
262
- margin,
263
- className,
264
- barGapRatio = 0.15,
265
- zoom,
266
- onZoomChange,
267
- showToolbar,
268
- highlightScope,
269
- animated,
270
- locale,
271
- }: BarChartProps) {
272
- const meta = seriesMeta(series, colors);
273
-
274
- if (layout === "horizontal") {
275
- return (
276
- <HorizontalBarChart
277
- xAxis={xAxis}
278
- yAxis={yAxis}
279
- series={series}
280
- height={height}
281
- width={width}
282
- stacked={stacked}
283
- borderRadius={borderRadius}
284
- colors={colors}
285
- hideLegend={hideLegend}
286
- hideGrid={hideGrid}
287
- margin={margin}
288
- className={className}
289
- barGapRatio={barGapRatio}
290
- />
291
- );
292
- }
293
-
294
- return (
295
- <ChartContainer
296
- slot="bar-chart"
297
- aria-label={`Bar chart of ${meta.map((s) => s.label).join(", ")}`}
298
- className={className}
299
- width={width}
300
- height={height}
301
- margin={margin}
302
- dataset={dataset}
303
- xAxis={xAxis}
304
- yAxis={yAxis}
305
- series={series.map((item) => ({ ...item, type: "bar" as const }))}
306
- colors={colors}
307
- hideLegend={hideLegend}
308
- hideGrid={hideGrid}
309
- stacked={stacked}
310
- barGapRatio={barGapRatio}
311
- borderRadius={borderRadius}
312
- zoom={zoom}
313
- onZoomChange={onZoomChange}
314
- showToolbar={showToolbar}
315
- highlightScope={highlightScope}
316
- animated={animated}
317
- locale={locale}
318
- />
319
- );
320
- }
321
-
322
- function HorizontalBarChart({
323
- xAxis,
324
- yAxis,
325
- series,
326
- height,
327
- width,
328
- stacked,
329
- borderRadius = 6,
330
- colors,
331
- hideLegend,
332
- hideGrid,
333
- margin,
334
- className,
335
- barGapRatio = 0.15,
336
- }: BarChartProps) {
337
- const categories = categoryLabels(xAxis?.[0], series[0]?.data?.length ?? 0);
338
- const domain = numericDomain(series, Boolean(stacked), yAxis?.[0]);
339
- const meta = seriesMeta(series, colors);
340
- const ids = meta.map((item) => item.id);
341
- const { hidden, toggle } = useHiddenSeries(ids);
342
- const { hover, setHover, clear } = useChartHover();
343
- const ticks = niceTicks(domain[0], domain[1], 5);
344
- const stacks = stacked ? stackSeries(series.map((item) => (item.data ?? []).map((v) => v ?? 0))) : null;
345
-
346
- return (
347
- <ChartFrame
348
- slot="bar-chart"
349
- className={className}
350
- width={width}
351
- height={height}
352
- margin={margin ?? { top: 16, right: 16, bottom: 28, left: 72 }}
353
- legend={hideLegend ? undefined : meta}
354
- hiddenIds={hidden}
355
- onToggleSeries={toggle}
356
- hover={hover}
357
- aria-label={`Bar chart of ${meta.map((s) => s.label).join(", ")}`}
358
- >
359
- {({ width: w, height: h, m }) => {
360
- const plot = box(w, h, m);
361
- const xAt = linearScale(domain, [plot.left, plot.right]);
362
- const yScale = bandScale(categories.map(String), [plot.top, plot.bottom], 0.24);
363
- const visible = meta.filter((item) => !hidden.has(item.id));
364
- const n = stacked ? 1 : Math.max(visible.length, 1);
365
- const gap = n > 1 ? yScale.bandwidth * barGapRatio : 0;
366
- const barH = (yScale.bandwidth - gap * (n - 1)) / n;
367
- const zero = xAt(0);
368
- return (
369
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
370
- {hideGrid
371
- ? null
372
- : ticks.map((tick) => (
373
- <line
374
- key={tick}
375
- className="spk-chart-grid"
376
- x1={xAt(tick)}
377
- x2={xAt(tick)}
378
- y1={plot.top}
379
- y2={plot.bottom}
380
- />
381
- ))}
382
- {ticks.map((tick) => (
383
- <text
384
- key={`t-${tick}`}
385
- className="spk-chart-tick"
386
- x={xAt(tick)}
387
- y={plot.bottom + 16}
388
- textAnchor="middle"
389
- >
390
- {formatChartNumber(tick)}
391
- </text>
392
- ))}
393
- {categories.map((label, i) => (
394
- <text
395
- key={String(label)}
396
- className="spk-chart-tick"
397
- x={plot.left - 8}
398
- y={yScale(String(label)) + yScale.bandwidth / 2}
399
- textAnchor="end"
400
- dominantBaseline="middle"
401
- >
402
- {String(label)}
403
- </text>
404
- ))}
405
- {series.map((item, s) => {
406
- const info = meta[s]!;
407
- if (hidden.has(info.id)) return null;
408
- const visIndex = visible.findIndex((v) => v.id === info.id);
409
- return (item.data ?? []).map((value, i) => {
410
- if (value == null) return null;
411
- const catY = yScale(String(categories[i]));
412
- let y = catY;
413
- let x1: number;
414
- let x2: number;
415
- if (stacks) {
416
- const seg = stacks[s]![i]!;
417
- x1 = xAt(seg.start);
418
- x2 = xAt(seg.end);
419
- } else {
420
- y = catY + visIndex * (barH + gap);
421
- x1 = zero;
422
- x2 = xAt(value);
423
- }
424
- const x = Math.min(x1, x2);
425
- const barW = Math.max(1, Math.abs(x2 - x1));
426
- return (
427
- <path
428
- key={`${info.id}-${i}`}
429
- className="spk-chart-mark"
430
- d={roundedRectPath(x, y, barW, stacked ? yScale.bandwidth : barH, borderRadius)}
431
- fill={info.color}
432
- onMouseEnter={(event) =>
433
- setHover({
434
- x: event.nativeEvent.offsetX,
435
- y: event.nativeEvent.offsetY,
436
- title: String(categories[i]),
437
- items: [{ color: info.color, label: info.label, value: formatSeriesValue(value) }],
438
- })
439
- }
440
- onMouseLeave={clear}
441
- />
442
- );
443
- });
444
- })}
445
- </svg>
446
- );
447
- }}
448
- </ChartFrame>
449
- );
450
- }
451
-
452
- export function LineChart({
453
- xAxis,
454
- yAxis,
455
- series,
456
- dataset,
457
- height,
458
- width,
459
- colors,
460
- hideLegend,
461
- hideGrid,
462
- margin,
463
- className,
464
- curve = "monotone",
465
- area = false,
466
- showMark = true,
467
- stacked = false,
468
- zoom,
469
- onZoomChange,
470
- showToolbar,
471
- highlightScope,
472
- animated,
473
- locale,
474
- }: LineChartProps) {
475
- const meta = seriesMeta(series, colors);
476
- const stackedAreas = stacked && (area || series.some((item) => item.area || item.type === "area"));
477
- return (
478
- <ChartContainer
479
- slot={area ? "area-chart" : "line-chart"}
480
- aria-label={`${area ? "Area" : "Line"} chart of ${meta.map((s) => s.label).join(", ")}`}
481
- className={className}
482
- width={width}
483
- height={height}
484
- margin={margin}
485
- dataset={dataset}
486
- xAxis={xAxis}
487
- yAxis={yAxis}
488
- series={series.map((item) => ({
489
- ...item,
490
- type: area || item.area ? "area" : "line",
491
- curve: item.curve ?? curve,
492
- showMark: item.showMark ?? (stackedAreas ? false : showMark),
493
- }))}
494
- colors={colors}
495
- hideLegend={hideLegend}
496
- hideGrid={hideGrid}
497
- stacked={stacked}
498
- zoom={zoom}
499
- onZoomChange={onZoomChange}
500
- showToolbar={showToolbar}
501
- highlightScope={highlightScope}
502
- animated={animated}
503
- locale={locale}
504
- />
505
- );
506
- }
507
-
508
- export function AreaChart(props: LineChartProps) {
509
- return <LineChart {...props} area />;
510
- }
511
-
512
- export type SparkLineChartProps = {
513
- data: number[];
514
- height?: number;
515
- width?: number;
516
- color?: string;
517
- area?: boolean;
518
- curve?: ChartCurve;
519
- className?: string;
520
- showHighlight?: boolean;
521
- plotType?: "line" | "bar";
522
- };
523
-
524
- export function SparkLineChart({
525
- data,
526
- height = 48,
527
- width,
528
- color = "var(--chart-1)",
529
- area = true,
530
- curve = "monotone",
531
- className,
532
- showHighlight = true,
533
- plotType = "line",
534
- }: SparkLineChartProps) {
535
- const [min, max] = extent(data, 0.08);
536
- return (
537
- <ChartFrame
538
- slot="sparkline"
539
- className={className}
540
- width={width}
541
- height={height}
542
- margin={{ top: 4, right: 4, bottom: 4, left: 4 }}
543
- aria-label="Sparkline"
544
- >
545
- {({ width: w, height: h, m }) => {
546
- const plot = box(w, h, m);
547
- const yAt = linearScale([min, max], [plot.bottom, plot.top]);
548
- if (plotType === "bar") {
549
- const n = Math.max(data.length, 1);
550
- const gap = 1;
551
- const barW = Math.max(1, plot.iw / n - gap);
552
- return (
553
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
554
- {data.map((value, i) => {
555
- const x = plot.left + (i / n) * plot.iw + gap / 2;
556
- const y = yAt(value);
557
- return (
558
- <rect
559
- key={i}
560
- x={x}
561
- y={y}
562
- width={barW}
563
- height={Math.max(1, plot.bottom - y)}
564
- fill={color}
565
- rx={1}
566
- />
567
- );
568
- })}
569
- </svg>
570
- );
571
- }
572
- const xAt = (i: number) =>
573
- plot.left + (data.length <= 1 ? plot.iw / 2 : (i / (data.length - 1)) * plot.iw);
574
- const pts = data.map((value, i) => ({ x: xAt(i), y: yAt(value) }));
575
- const last = pts[pts.length - 1];
576
- return (
577
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
578
- {area ? <path d={areaPath(pts, plot.bottom, curve)} fill={color} opacity={0.2} /> : null}
579
- <path
580
- d={linePath(pts, curve)}
581
- fill="none"
582
- stroke={color}
583
- strokeWidth={1.75}
584
- strokeLinejoin="round"
585
- strokeLinecap="round"
586
- />
587
- {showHighlight && last ? <circle cx={last.x} cy={last.y} r={3} fill={color} /> : null}
588
- </svg>
589
- );
590
- }}
591
- </ChartFrame>
592
- );
593
- }
594
-
595
- export type ScatterChartProps = {
596
- series: ScatterSeries[];
597
- xAxis?: ChartAxisConfig[];
598
- yAxis?: ChartAxisConfig[];
599
- height?: number;
600
- width?: number;
601
- colors?: string[];
602
- hideLegend?: boolean;
603
- hideGrid?: boolean;
604
- margin?: ChartMargin;
605
- className?: string;
606
- bubble?: boolean;
607
- renderer?: ScatterRenderer;
608
- };
609
-
610
- export function ScatterChart({
611
- series,
612
- xAxis,
613
- yAxis,
614
- height,
615
- width,
616
- colors,
617
- hideLegend,
618
- hideGrid,
619
- margin,
620
- className,
621
- bubble = false,
622
- renderer = "svg",
623
- }: ScatterChartProps) {
624
- const meta = seriesMeta(series, colors);
625
- const ids = meta.map((item) => item.id);
626
- const { hidden, toggle } = useHiddenSeries(ids);
627
- const { hover, setHover, clear } = useChartHover();
628
- const [glFailed, setGlFailed] = useState(false);
629
- const onGlUnavailable = useCallback(() => setGlFailed(true), []);
630
- const pointCount = series.reduce((n, item) => n + item.data.length, 0);
631
- const useGL = shouldUseWebGL(renderer, pointCount) && !glFailed;
632
- const xs = series.flatMap((item) => item.data.map((p) => p.x));
633
- const ys = series.flatMap((item) => item.data.map((p) => p.y));
634
- const zs = series.flatMap((item) => item.data.map((p) => p.z ?? 1));
635
- const xDomain: [number, number] = [xAxis?.[0]?.min ?? extent(xs, 0.08)[0], xAxis?.[0]?.max ?? extent(xs, 0.08)[1]];
636
- const yDomain: [number, number] = [yAxis?.[0]?.min ?? extent(ys, 0.08)[0], yAxis?.[0]?.max ?? extent(ys, 0.08)[1]];
637
- const zDomain = extent(zs);
638
- const ticks = niceTicks(yDomain[0], yDomain[1], 5);
639
- const xTicks = niceTicks(xDomain[0], xDomain[1], 5);
640
-
641
- return (
642
- <ChartFrame
643
- slot={bubble ? "bubble-chart" : "scatter-chart"}
644
- className={className}
645
- width={width}
646
- height={height}
647
- margin={margin}
648
- legend={hideLegend ? undefined : meta}
649
- hiddenIds={hidden}
650
- onToggleSeries={toggle}
651
- hover={hover}
652
- aria-label={bubble ? "Bubble chart" : "Scatter chart"}
653
- >
654
- {({ width: w, height: h, m }) => {
655
- const plot = box(w, h, m);
656
- const xAt = linearScale(xDomain, [plot.left, plot.right]);
657
- const yAt = linearScale(yDomain, [plot.bottom, plot.top]);
658
- const zAt = (z: number) => 5 + ((z - zDomain[0]) / (zDomain[1] - zDomain[0] || 1)) * 18;
659
- const hoverPoint = (event: MouseEvent<SVGElement>, svg: SVGSVGElement | null) => {
660
- if (!svg) return;
661
- const pos = svgPointerPosition(svg, event.clientX, event.clientY, w, h);
662
- const candidates = series.flatMap((item, s) => {
663
- const info = meta[s]!;
664
- if (hidden.has(info.id)) return [];
665
- return item.data.map((point, i) => ({
666
- x: xAt(point.x),
667
- y: yAt(point.y),
668
- r: bubble ? zAt(point.z ?? 1) : 4.5,
669
- seriesIndex: s,
670
- pointIndex: i,
671
- }));
672
- });
673
- const hit = nearestScatterHit(candidates, pos.x, pos.y);
674
- if (!hit) {
675
- clear();
676
- return;
677
- }
678
- const item = series[hit.seriesIndex]!;
679
- const info = meta[hit.seriesIndex]!;
680
- const point = item.data[hit.pointIndex]!;
681
- setHover({
682
- x: pos.x,
683
- y: pos.y,
684
- title: point.label ?? info.label,
685
- items: [
686
- { color: info.color, label: "x", value: formatChartNumber(point.x) },
687
- { color: info.color, label: "y", value: formatChartNumber(point.y) },
688
- ...(point.z != null
689
- ? [{ color: info.color, label: "size", value: formatChartNumber(point.z) }]
690
- : []),
691
- ],
692
- });
693
- };
694
- return (
695
- <>
696
- {useGL ? (
697
- <ScatterWebGLLayer
698
- width={w}
699
- height={h}
700
- series={series}
701
- meta={meta}
702
- hidden={hidden}
703
- xAt={xAt}
704
- yAt={yAt}
705
- sizeAt={zAt}
706
- bubble={bubble}
707
- onUnavailable={onGlUnavailable}
708
- />
709
- ) : null}
710
- <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
711
- {hideGrid ? null : (
712
- <CartesianGrid
713
- ticks={ticks}
714
- scale={yAt}
715
- left={plot.left}
716
- right={plot.right}
717
- top={plot.top}
718
- bottom={plot.bottom}
719
- />
720
- )}
721
- <ValueAxis ticks={ticks} scale={yAt} left={plot.left} />
722
- {xTicks.map((tick) => (
723
- <text
724
- key={tick}
725
- className="spk-chart-tick"
726
- x={xAt(tick)}
727
- y={plot.bottom + 16}
728
- textAnchor="middle"
729
- >
730
- {formatChartNumber(tick)}
731
- </text>
732
- ))}
733
- {useGL ? (
734
- <rect
735
- x={plot.left}
736
- y={plot.top}
737
- width={plot.iw}
738
- height={plot.ih}
739
- fill="transparent"
740
- onMouseMove={(event) => hoverPoint(event, event.currentTarget.ownerSVGElement)}
741
- onMouseLeave={clear}
742
- />
743
- ) : (
744
- series.map((item, s) => {
745
- const info = meta[s]!;
746
- if (hidden.has(info.id)) return null;
747
- return item.data.map((point, i) => (
748
- <circle
749
- key={`${info.id}-${point.id ?? i}`}
750
- cx={xAt(point.x)}
751
- cy={yAt(point.y)}
752
- r={bubble ? zAt(point.z ?? 1) : 4.5}
753
- fill={info.color}
754
- fillOpacity={bubble ? 0.45 : 0.85}
755
- stroke={info.color}
756
- strokeWidth={bubble ? 1.25 : 0}
757
- className="spk-chart-mark"
758
- onMouseEnter={(event) =>
759
- setHover({
760
- x: event.nativeEvent.offsetX,
761
- y: event.nativeEvent.offsetY,
762
- title: point.label ?? info.label,
763
- items: [
764
- { color: info.color, label: "x", value: formatChartNumber(point.x) },
765
- { color: info.color, label: "y", value: formatChartNumber(point.y) },
766
- ...(point.z != null
767
- ? [{ color: info.color, label: "size", value: formatChartNumber(point.z) }]
768
- : []),
769
- ],
770
- })
771
- }
772
- onMouseLeave={clear}
773
- />
774
- ));
775
- })
776
- )}
777
- </svg>
778
- </>
779
- );
780
- }}
781
- </ChartFrame>
782
- );
783
- }
784
-
785
- export function BubbleChart(props: ScatterChartProps) {
786
- return <ScatterChart {...props} bubble />;
787
- }
788
-
789
- export type RangeSeries = {
790
- id?: string;
791
- label?: string;
792
- color?: string;
793
- data: Array<[number, number] | null>;
794
- };
795
-
796
- export type RangeBarChartProps = Omit<CartesianChartProps, "series"> & {
797
- series: RangeSeries[];
798
- layout?: "vertical" | "horizontal";
799
- };
800
-
801
- export function RangeBarChart({
802
- xAxis,
803
- yAxis,
804
- series,
805
- height,
806
- width,
807
- colors,
808
- hideLegend,
809
- hideGrid,
810
- margin,
811
- className,
812
- }: RangeBarChartProps) {
813
- const categories = categoryLabels(xAxis?.[0], series[0]?.data?.length ?? 0);
814
- const values = series.flatMap((item) => item.data.flatMap((pair) => (pair ? pair : [])));
815
- const domain: [number, number] = [
816
- yAxis?.[0]?.min ?? extent(values, 0.04)[0],
817
- yAxis?.[0]?.max ?? extent(values, 0.04)[1],
818
- ];
819
- const meta = seriesMeta(series, colors);
820
-
821
- return (
822
- <CartesianShell
823
- slot="range-bar-chart"
824
- ariaLabel="Range bar chart"
825
- series={series}
826
- colors={colors}
827
- hideLegend={hideLegend}
828
- hideGrid={hideGrid}
829
- xAxis={xAxis}
830
- yAxis={yAxis}
831
- width={width}
832
- height={height}
833
- margin={margin}
834
- className={className}
835
- domain={domain}
836
- categories={categories}
837
- categoryMode="band"
838
- >
839
- {({ xAt, yAt, bandwidth, hidden, meta: items, setHover, clear }) => (
840
- <g>
841
- {series.map((item, s) => {
842
- const info = items[s]!;
843
- if (hidden.has(info.id)) return null;
844
- const barW = bandwidth / Math.max(series.length, 1);
845
- return item.data.map((pair, i) => {
846
- if (!pair) return null;
847
- const [lo, hi] = pair;
848
- const y1 = yAt(hi);
849
- const y2 = yAt(lo);
850
- const y = Math.min(y1, y2);
851
- const h = Math.max(1, Math.abs(y2 - y1));
852
- const x = xAt(i) + s * barW;
853
- return (
854
- <rect
855
- key={`${info.id}-${i}`}
856
- x={x}
857
- y={y}
858
- width={barW * 0.86}
859
- height={h}
860
- rx={5}
861
- fill={info.color}
862
- className="spk-chart-mark"
863
- onMouseEnter={(event) =>
864
- setHover({
865
- x: event.nativeEvent.offsetX,
866
- y: event.nativeEvent.offsetY,
867
- title: String(categories[i]),
868
- items: [{ color: info.color, label: info.label, value: formatSeriesValue(pair) }],
869
- })
870
- }
871
- onMouseLeave={clear}
872
- />
873
- );
874
- });
875
- })}
876
- </g>
877
- )}
878
- </CartesianShell>
879
- );
880
- }
881
-
882
- export type RangeAreaChartProps = Omit<CartesianChartProps, "series"> & {
883
- series: RangeSeries[];
884
- curve?: ChartCurve;
885
- };
886
-
887
- export function RangeAreaChart({
888
- xAxis,
889
- yAxis,
890
- series,
891
- height,
892
- width,
893
- colors,
894
- hideLegend,
895
- hideGrid,
896
- margin,
897
- className,
898
- curve = "monotone",
899
- }: RangeAreaChartProps) {
900
- const categories = categoryLabels(xAxis?.[0], series[0]?.data?.length ?? 0);
901
- const values = series.flatMap((item) => item.data.flatMap((pair) => (pair ? pair : [])));
902
- const domain: [number, number] = [
903
- yAxis?.[0]?.min ?? extent(values, 0.06)[0],
904
- yAxis?.[0]?.max ?? extent(values, 0.06)[1],
905
- ];
906
-
907
- return (
908
- <CartesianShell
909
- slot="range-area-chart"
910
- ariaLabel="Range area chart"
911
- series={series}
912
- colors={colors}
913
- hideLegend={hideLegend}
914
- hideGrid={hideGrid}
915
- xAxis={xAxis}
916
- yAxis={yAxis}
917
- width={width}
918
- height={height}
919
- margin={margin}
920
- className={className}
921
- domain={domain}
922
- categories={categories}
923
- categoryMode="point"
924
- >
925
- {({ xAt, yAt, hidden, meta, setHover, clear }) => (
926
- <g>
927
- {series.map((item, s) => {
928
- const info = meta[s]!;
929
- if (hidden.has(info.id)) return null;
930
- const high = item.data.map((pair, i) => ({ x: xAt(i), y: yAt(pair ? pair[1] : 0) }));
931
- const low = item.data.map((pair, i) => ({ x: xAt(i), y: yAt(pair ? pair[0] : 0) }));
932
- return (
933
- <g key={info.id}>
934
- <path d={rangeAreaPath(high, low, curve)} fill={info.color} opacity={0.22} className="spk-chart-mark" />
935
- <path d={linePath(high, curve)} fill="none" stroke={info.color} strokeWidth={2} />
936
- <path d={linePath(low, curve)} fill="none" stroke={info.color} strokeWidth={2} />
937
- {high.map((pt, i) => (
938
- <circle
939
- key={i}
940
- cx={pt.x}
941
- cy={pt.y}
942
- r={3}
943
- fill={info.color}
944
- onMouseEnter={() =>
945
- setHover({
946
- x: pt.x,
947
- y: pt.y,
948
- title: String(categories[i]),
949
- items: [{ color: info.color, label: info.label, value: formatSeriesValue(item.data[i] ?? undefined) }],
950
- })
951
- }
952
- onMouseLeave={clear}
953
- />
954
- ))}
955
- </g>
956
- );
957
- })}
958
- </g>
959
- )}
960
- </CartesianShell>
961
- );
962
- }
963
-
964
- export type WaterfallChartProps = CartesianChartProps;
965
-
966
- export function WaterfallChart({
967
- xAxis,
968
- yAxis,
969
- series,
970
- height,
971
- width,
972
- colors,
973
- hideLegend = true,
974
- hideGrid,
975
- margin,
976
- className,
977
- }: WaterfallChartProps) {
978
- const item = series[0];
979
- const categories = categoryLabels(xAxis?.[0], item?.data?.length ?? 0);
980
- const bars = waterfallBars(item?.data ?? []);
981
- const values = bars.flatMap((bar) => [bar.start, bar.end]);
982
- const domain: [number, number] = [
983
- yAxis?.[0]?.min ?? Math.min(0, ...values),
984
- yAxis?.[0]?.max ?? extent(values, 0.06)[1],
985
- ];
986
- const color = item?.color ?? chartColor(0, colors);
987
-
988
- return (
989
- <CartesianShell
990
- slot="waterfall-chart"
991
- ariaLabel="Waterfall chart"
992
- series={series}
993
- colors={colors}
994
- hideLegend={hideLegend}
995
- hideGrid={hideGrid}
996
- xAxis={xAxis}
997
- yAxis={yAxis}
998
- width={width}
999
- height={height}
1000
- margin={margin}
1001
- className={className}
1002
- domain={domain}
1003
- categories={categories}
1004
- categoryMode="band"
1005
- >
1006
- {({ xAt, yAt, bandwidth, setHover, clear }) => (
1007
- <g>
1008
- {bars.map((bar, i) => {
1009
- const y1 = yAt(bar.end);
1010
- const y2 = yAt(bar.start);
1011
- const y = Math.min(y1, y2);
1012
- const h = Math.max(1, Math.abs(y2 - y1));
1013
- const fill = bar.total
1014
- ? "var(--chart-2)"
1015
- : bar.value >= 0
1016
- ? color
1017
- : "var(--chart-4)";
1018
- const next = bars[i + 1];
1019
- return (
1020
- <g key={bar.index}>
1021
- {next && !next.total ? (
1022
- <line
1023
- x1={xAt(i) + bandwidth}
1024
- x2={xAt(i + 1)}
1025
- y1={yAt(bar.end)}
1026
- y2={yAt(bar.end)}
1027
- className="spk-chart-grid"
1028
- />
1029
- ) : null}
1030
- <rect
1031
- x={xAt(i)}
1032
- y={y}
1033
- width={bandwidth}
1034
- height={h}
1035
- rx={5}
1036
- fill={fill}
1037
- className="spk-chart-mark"
1038
- onMouseEnter={(event) =>
1039
- setHover({
1040
- x: event.nativeEvent.offsetX,
1041
- y: event.nativeEvent.offsetY,
1042
- title: String(categories[i]),
1043
- items: [
1044
- {
1045
- color: fill,
1046
- label: bar.total ? "Total" : bar.value >= 0 ? "Increase" : "Decrease",
1047
- value: formatChartNumber(bar.value),
1048
- },
1049
- ],
1050
- })
1051
- }
1052
- onMouseLeave={clear}
1053
- />
1054
- </g>
1055
- );
1056
- })}
1057
- </g>
1058
- )}
1059
- </CartesianShell>
1060
- );
1061
- }
1062
-
1063
- export type BoxPlotSeries = {
1064
- id?: string;
1065
- label?: string;
1066
- color?: string;
1067
- data: Array<BoxPlotStats | number[] | null>;
1068
- };
1069
-
1070
- export type BoxPlotChartProps = Omit<CartesianChartProps, "series"> & {
1071
- series: BoxPlotSeries[];
1072
- };
1073
-
1074
- export function BoxPlotChart({
1075
- xAxis,
1076
- yAxis,
1077
- series,
1078
- height,
1079
- width,
1080
- colors,
1081
- hideLegend,
1082
- hideGrid,
1083
- margin,
1084
- className,
1085
- }: BoxPlotChartProps) {
1086
- const stats = series.map((item) =>
1087
- item.data.map((entry) => (Array.isArray(entry) ? boxPlotStats(entry) : entry)),
1088
- );
1089
- const categories = categoryLabels(xAxis?.[0], stats[0]?.length ?? 0);
1090
- const values = stats.flatMap((row) =>
1091
- row.flatMap((box) => (box ? [box.min, box.max, ...(box.outliers ?? [])] : [])),
1092
- );
1093
- const domain: [number, number] = [
1094
- yAxis?.[0]?.min ?? extent(values, 0.06)[0],
1095
- yAxis?.[0]?.max ?? extent(values, 0.06)[1],
1096
- ];
1097
-
1098
- return (
1099
- <CartesianShell
1100
- slot="boxplot-chart"
1101
- ariaLabel="Box plot"
1102
- series={series}
1103
- colors={colors}
1104
- hideLegend={hideLegend}
1105
- hideGrid={hideGrid}
1106
- xAxis={xAxis}
1107
- yAxis={yAxis}
1108
- width={width}
1109
- height={height}
1110
- margin={margin}
1111
- className={className}
1112
- domain={domain}
1113
- categories={categories}
1114
- categoryMode="band"
1115
- >
1116
- {({ xAt, yAt, bandwidth, hidden, meta, setHover, clear }) => (
1117
- <g>
1118
- {series.map((item, s) => {
1119
- const info = meta[s]!;
1120
- if (hidden.has(info.id)) return null;
1121
- const boxW = Math.min(36, bandwidth * 0.55);
1122
- return stats[s]!.map((boxStat, i) => {
1123
- if (!boxStat) return null;
1124
- const cx = xAt(i) + bandwidth / 2;
1125
- const x = cx - boxW / 2;
1126
- return (
1127
- <g
1128
- key={`${info.id}-${i}`}
1129
- onMouseEnter={(event) =>
1130
- setHover({
1131
- x: event.nativeEvent.offsetX,
1132
- y: event.nativeEvent.offsetY,
1133
- title: String(categories[i]),
1134
- items: [
1135
- { color: info.color, label: "min", value: formatChartNumber(boxStat.min) },
1136
- { color: info.color, label: "q1", value: formatChartNumber(boxStat.q1) },
1137
- { color: info.color, label: "median", value: formatChartNumber(boxStat.median) },
1138
- { color: info.color, label: "q3", value: formatChartNumber(boxStat.q3) },
1139
- { color: info.color, label: "max", value: formatChartNumber(boxStat.max) },
1140
- ],
1141
- })
1142
- }
1143
- onMouseLeave={clear}
1144
- >
1145
- <line
1146
- x1={cx}
1147
- x2={cx}
1148
- y1={yAt(boxStat.min)}
1149
- y2={yAt(boxStat.max)}
1150
- stroke={info.color}
1151
- strokeWidth={1.5}
1152
- />
1153
- <line x1={x} x2={x + boxW} y1={yAt(boxStat.min)} y2={yAt(boxStat.min)} stroke={info.color} />
1154
- <line x1={x} x2={x + boxW} y1={yAt(boxStat.max)} y2={yAt(boxStat.max)} stroke={info.color} />
1155
- <rect
1156
- x={x}
1157
- y={yAt(boxStat.q3)}
1158
- width={boxW}
1159
- height={Math.max(1, yAt(boxStat.q1) - yAt(boxStat.q3))}
1160
- fill={info.color}
1161
- fillOpacity={0.22}
1162
- stroke={info.color}
1163
- rx={3}
1164
- />
1165
- <line
1166
- x1={x}
1167
- x2={x + boxW}
1168
- y1={yAt(boxStat.median)}
1169
- y2={yAt(boxStat.median)}
1170
- stroke={info.color}
1171
- strokeWidth={2}
1172
- />
1173
- {boxStat.outliers.map((value, oi) => (
1174
- <circle key={oi} cx={cx} cy={yAt(value)} r={2.4} fill={info.color} />
1175
- ))}
1176
- </g>
1177
- );
1178
- });
1179
- })}
1180
- </g>
1181
- )}
1182
- </CartesianShell>
1183
- );
1184
- }
1185
-
1186
- export type CandleDatum = {
1187
- open: number;
1188
- high: number;
1189
- low: number;
1190
- close: number;
1191
- };
1192
-
1193
- export type CandlestickChartProps = Omit<CartesianChartProps, "series"> & {
1194
- series: Array<{ id?: string; label?: string; color?: string; data: Array<CandleDatum | null> }>;
1195
- variant?: "candle" | "ohlc";
1196
- };
1197
-
1198
- export function CandlestickChart({
1199
- xAxis,
1200
- yAxis,
1201
- series,
1202
- height,
1203
- width,
1204
- colors,
1205
- hideLegend = true,
1206
- hideGrid,
1207
- margin,
1208
- className,
1209
- variant = "candle",
1210
- }: CandlestickChartProps) {
1211
- const item = series[0];
1212
- const categories = categoryLabels(xAxis?.[0], item?.data?.length ?? 0);
1213
- const values = (item?.data ?? []).flatMap((candle) => (candle ? [candle.high, candle.low] : []));
1214
- const domain: [number, number] = [
1215
- yAxis?.[0]?.min ?? extent(values, 0.06)[0],
1216
- yAxis?.[0]?.max ?? extent(values, 0.06)[1],
1217
- ];
1218
-
1219
- return (
1220
- <CartesianShell
1221
- slot={variant === "ohlc" ? "ohlc-chart" : "candlestick-chart"}
1222
- ariaLabel={variant === "ohlc" ? "OHLC chart" : "Candlestick chart"}
1223
- series={series}
1224
- colors={colors}
1225
- hideLegend={hideLegend}
1226
- hideGrid={hideGrid}
1227
- xAxis={xAxis}
1228
- yAxis={yAxis}
1229
- width={width}
1230
- height={height}
1231
- margin={margin}
1232
- className={className}
1233
- domain={domain}
1234
- categories={categories}
1235
- categoryMode="band"
1236
- >
1237
- {({ xAt, yAt, bandwidth, setHover, clear }) => (
1238
- <g>
1239
- {(item?.data ?? []).map((candle, i) => {
1240
- if (!candle) return null;
1241
- const up = candle.close >= candle.open;
1242
- const color = up ? "var(--chart-3)" : "var(--chart-4)";
1243
- const cx = xAt(i) + bandwidth / 2;
1244
- const bodyTop = yAt(Math.max(candle.open, candle.close));
1245
- const bodyBot = yAt(Math.min(candle.open, candle.close));
1246
- const bodyH = Math.max(1, bodyBot - bodyTop);
1247
- const wickW = Math.max(1.2, bandwidth * 0.08);
1248
- const bodyW = Math.min(18, bandwidth * 0.55);
1249
- return (
1250
- <g
1251
- key={i}
1252
- onMouseEnter={(event) =>
1253
- setHover({
1254
- x: event.nativeEvent.offsetX,
1255
- y: event.nativeEvent.offsetY,
1256
- title: String(categories[i]),
1257
- items: [
1258
- { color, label: "open", value: formatChartNumber(candle.open) },
1259
- { color, label: "high", value: formatChartNumber(candle.high) },
1260
- { color, label: "low", value: formatChartNumber(candle.low) },
1261
- { color, label: "close", value: formatChartNumber(candle.close) },
1262
- ],
1263
- })
1264
- }
1265
- onMouseLeave={clear}
1266
- >
1267
- <line x1={cx} x2={cx} y1={yAt(candle.high)} y2={yAt(candle.low)} stroke={color} strokeWidth={wickW} />
1268
- {variant === "ohlc" ? (
1269
- <>
1270
- <line x1={cx - bodyW / 2} x2={cx} y1={yAt(candle.open)} y2={yAt(candle.open)} stroke={color} strokeWidth={2} />
1271
- <line x1={cx} x2={cx + bodyW / 2} y1={yAt(candle.close)} y2={yAt(candle.close)} stroke={color} strokeWidth={2} />
1272
- </>
1273
- ) : (
1274
- <rect x={cx - bodyW / 2} y={bodyTop} width={bodyW} height={bodyH} fill={color} rx={2} />
1275
- )}
1276
- </g>
1277
- );
1278
- })}
1279
- </g>
1280
- )}
1281
- </CartesianShell>
1282
- );
1283
- }
1284
-
1285
- export function OhlcChart(props: CandlestickChartProps) {
1286
- return <CandlestickChart {...props} variant="ohlc" />;
1287
- }