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