@spatika/react 1.1.0 → 1.2.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 (311) hide show
  1. package/AGENTS.md +71 -0
  2. package/README.md +4 -3
  3. package/dist/composites/Card.d.ts +7 -1
  4. package/dist/composites/Card.d.ts.map +1 -1
  5. package/dist/composites/Card.js +10 -0
  6. package/dist/composites/ChartDataGrid.d.ts +33 -0
  7. package/dist/composites/ChartDataGrid.d.ts.map +1 -0
  8. package/dist/composites/ChartDataGrid.js +77 -0
  9. package/dist/composites/CoverHero.d.ts +1 -1
  10. package/dist/composites/CoverHero.d.ts.map +1 -1
  11. package/dist/composites/CoverHero.js +2 -2
  12. package/dist/composites/EntityMediaCard.d.ts +1 -1
  13. package/dist/composites/EntityMediaCard.d.ts.map +1 -1
  14. package/dist/composites/EntityMediaCard.js +12 -4
  15. package/dist/composites/EventCalendar.d.ts +35 -0
  16. package/dist/composites/EventCalendar.d.ts.map +1 -0
  17. package/dist/composites/EventCalendar.js +339 -0
  18. package/dist/composites/EventEditor.d.ts +22 -0
  19. package/dist/composites/EventEditor.d.ts.map +1 -0
  20. package/dist/composites/EventEditor.js +86 -0
  21. package/dist/composites/EventTimeline.d.ts +24 -0
  22. package/dist/composites/EventTimeline.d.ts.map +1 -0
  23. package/dist/composites/EventTimeline.js +184 -0
  24. package/dist/composites/MapChart.d.ts +24 -0
  25. package/dist/composites/MapChart.d.ts.map +1 -0
  26. package/dist/composites/MapChart.js +58 -0
  27. package/dist/composites/ProfileHero.d.ts +6 -4
  28. package/dist/composites/ProfileHero.d.ts.map +1 -1
  29. package/dist/composites/ProfileHero.js +8 -8
  30. package/dist/composites/ScatterWebGL.d.ts +22 -0
  31. package/dist/composites/ScatterWebGL.d.ts.map +1 -0
  32. package/dist/composites/ScatterWebGL.js +45 -0
  33. package/dist/composites/SiteFooter.d.ts +1 -1
  34. package/dist/composites/SiteFooter.js +1 -1
  35. package/dist/composites/SiteNav.d.ts +7 -2
  36. package/dist/composites/SiteNav.d.ts.map +1 -1
  37. package/dist/composites/SiteNav.js +9 -5
  38. package/dist/composites/StatCard.d.ts +2 -1
  39. package/dist/composites/StatCard.d.ts.map +1 -1
  40. package/dist/composites/StatCard.js +3 -3
  41. package/dist/composites/StatusDot.d.ts +1 -1
  42. package/dist/composites/Wordmark.d.ts +2 -2
  43. package/dist/composites/Wordmark.d.ts.map +1 -1
  44. package/dist/composites/cartesian-charts.d.ts +138 -0
  45. package/dist/composites/cartesian-charts.d.ts.map +1 -0
  46. package/dist/composites/cartesian-charts.js +401 -0
  47. package/dist/composites/chart-composition.d.ts +139 -0
  48. package/dist/composites/chart-composition.d.ts.map +1 -0
  49. package/dist/composites/chart-composition.js +486 -0
  50. package/dist/composites/chart-ui.d.ts +83 -0
  51. package/dist/composites/chart-ui.d.ts.map +1 -0
  52. package/dist/composites/chart-ui.js +108 -0
  53. package/dist/composites/charts-3d.d.ts +37 -0
  54. package/dist/composites/charts-3d.d.ts.map +1 -0
  55. package/dist/composites/charts-3d.js +121 -0
  56. package/dist/composites/flow-charts.d.ts +89 -0
  57. package/dist/composites/flow-charts.d.ts.map +1 -0
  58. package/dist/composites/flow-charts.js +186 -0
  59. package/dist/composites/radial-charts.d.ts +109 -0
  60. package/dist/composites/radial-charts.d.ts.map +1 -0
  61. package/dist/composites/radial-charts.js +176 -0
  62. package/dist/composites/scheduler-ui.d.ts +43 -0
  63. package/dist/composites/scheduler-ui.d.ts.map +1 -0
  64. package/dist/composites/scheduler-ui.js +71 -0
  65. package/dist/index.d.ts +67 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +57 -7
  68. package/dist/lib/chart-export.d.ts +10 -0
  69. package/dist/lib/chart-export.d.ts.map +1 -0
  70. package/dist/lib/chart-export.js +102 -0
  71. package/dist/lib/charts.d.ts +235 -0
  72. package/dist/lib/charts.d.ts.map +1 -0
  73. package/dist/lib/charts.js +744 -0
  74. package/dist/lib/geo.d.ts +39 -0
  75. package/dist/lib/geo.d.ts.map +1 -0
  76. package/dist/lib/geo.js +101 -0
  77. package/dist/lib/iso.d.ts +21 -0
  78. package/dist/lib/iso.d.ts.map +1 -0
  79. package/dist/lib/iso.js +56 -0
  80. package/dist/lib/rrule.d.ts +20 -0
  81. package/dist/lib/rrule.d.ts.map +1 -0
  82. package/dist/lib/rrule.js +236 -0
  83. package/dist/lib/scheduler.d.ts +152 -0
  84. package/dist/lib/scheduler.d.ts.map +1 -0
  85. package/dist/lib/scheduler.js +501 -0
  86. package/dist/lib/webgl-scatter.d.ts +44 -0
  87. package/dist/lib/webgl-scatter.d.ts.map +1 -0
  88. package/dist/lib/webgl-scatter.js +185 -0
  89. package/dist/primitives/Accordion.d.ts +44 -20
  90. package/dist/primitives/Accordion.d.ts.map +1 -1
  91. package/dist/primitives/Accordion.js +82 -22
  92. package/dist/primitives/AppBar.d.ts +13 -0
  93. package/dist/primitives/AppBar.d.ts.map +1 -0
  94. package/dist/primitives/AppBar.js +27 -0
  95. package/dist/primitives/Autocomplete.d.ts +28 -0
  96. package/dist/primitives/Autocomplete.d.ts.map +1 -0
  97. package/dist/primitives/Autocomplete.js +118 -0
  98. package/dist/primitives/Avatar.d.ts +13 -1
  99. package/dist/primitives/Avatar.d.ts.map +1 -1
  100. package/dist/primitives/Avatar.js +17 -2
  101. package/dist/primitives/Backdrop.d.ts +17 -0
  102. package/dist/primitives/Backdrop.d.ts.map +1 -0
  103. package/dist/primitives/Backdrop.js +17 -0
  104. package/dist/primitives/BottomNavigation.d.ts +23 -0
  105. package/dist/primitives/BottomNavigation.d.ts.map +1 -0
  106. package/dist/primitives/BottomNavigation.js +30 -0
  107. package/dist/primitives/ButtonBase.d.ts +14 -0
  108. package/dist/primitives/ButtonBase.d.ts.map +1 -0
  109. package/dist/primitives/ButtonBase.js +13 -0
  110. package/dist/primitives/ButtonGroup.d.ts +25 -0
  111. package/dist/primitives/ButtonGroup.d.ts.map +1 -0
  112. package/dist/primitives/ButtonGroup.js +44 -0
  113. package/dist/primitives/CircularProgress.d.ts +16 -0
  114. package/dist/primitives/CircularProgress.d.ts.map +1 -0
  115. package/dist/primitives/CircularProgress.js +17 -0
  116. package/dist/primitives/ClickAwayListener.d.ts +16 -0
  117. package/dist/primitives/ClickAwayListener.d.ts.map +1 -0
  118. package/dist/primitives/ClickAwayListener.js +33 -0
  119. package/dist/primitives/Collapse.d.ts +19 -0
  120. package/dist/primitives/Collapse.d.ts.map +1 -0
  121. package/dist/primitives/Collapse.js +14 -0
  122. package/dist/primitives/Container.d.ts +13 -0
  123. package/dist/primitives/Container.d.ts.map +1 -0
  124. package/dist/primitives/Container.js +26 -0
  125. package/dist/primitives/Fab.d.ts +22 -0
  126. package/dist/primitives/Fab.d.ts.map +1 -0
  127. package/dist/primitives/Fab.js +42 -0
  128. package/dist/primitives/FormControl.d.ts +27 -0
  129. package/dist/primitives/FormControl.d.ts.map +1 -0
  130. package/dist/primitives/FormControl.js +24 -0
  131. package/dist/primitives/FormControlLabel.d.ts +18 -0
  132. package/dist/primitives/FormControlLabel.d.ts.map +1 -0
  133. package/dist/primitives/FormControlLabel.js +19 -0
  134. package/dist/primitives/Grid.d.ts +32 -0
  135. package/dist/primitives/Grid.d.ts.map +1 -0
  136. package/dist/primitives/Grid.js +50 -0
  137. package/dist/primitives/IconButton.d.ts +22 -0
  138. package/dist/primitives/IconButton.d.ts.map +1 -0
  139. package/dist/primitives/IconButton.js +39 -0
  140. package/dist/primitives/ImageList.d.ts +31 -0
  141. package/dist/primitives/ImageList.d.ts.map +1 -0
  142. package/dist/primitives/ImageList.js +33 -0
  143. package/dist/primitives/InputAdornment.d.ts +28 -0
  144. package/dist/primitives/InputAdornment.d.ts.map +1 -0
  145. package/dist/primitives/InputAdornment.js +13 -0
  146. package/dist/primitives/Link.d.ts +17 -0
  147. package/dist/primitives/Link.d.ts.map +1 -0
  148. package/dist/primitives/Link.js +29 -0
  149. package/dist/primitives/List.d.ts +41 -0
  150. package/dist/primitives/List.d.ts.map +1 -0
  151. package/dist/primitives/List.js +22 -0
  152. package/dist/primitives/Masonry.d.ts +14 -0
  153. package/dist/primitives/Masonry.d.ts.map +1 -0
  154. package/dist/primitives/Masonry.js +15 -0
  155. package/dist/primitives/Menu.d.ts +28 -0
  156. package/dist/primitives/Menu.d.ts.map +1 -0
  157. package/dist/primitives/Menu.js +54 -0
  158. package/dist/primitives/MobileStepper.d.ts +15 -0
  159. package/dist/primitives/MobileStepper.d.ts.map +1 -0
  160. package/dist/primitives/MobileStepper.js +8 -0
  161. package/dist/primitives/Modal.d.ts +18 -0
  162. package/dist/primitives/Modal.d.ts.map +1 -0
  163. package/dist/primitives/Modal.js +29 -0
  164. package/dist/primitives/NoSsr.d.ts +15 -0
  165. package/dist/primitives/NoSsr.d.ts.map +1 -0
  166. package/dist/primitives/NoSsr.js +19 -0
  167. package/dist/primitives/Paper.d.ts +22 -0
  168. package/dist/primitives/Paper.d.ts.map +1 -0
  169. package/dist/primitives/Paper.js +33 -0
  170. package/dist/primitives/Progress.d.ts +6 -2
  171. package/dist/primitives/Progress.d.ts.map +1 -1
  172. package/dist/primitives/Progress.js +8 -3
  173. package/dist/primitives/Snackbar.d.ts +23 -0
  174. package/dist/primitives/Snackbar.d.ts.map +1 -0
  175. package/dist/primitives/Snackbar.js +33 -0
  176. package/dist/primitives/SpeedDial.d.ts +26 -0
  177. package/dist/primitives/SpeedDial.d.ts.map +1 -0
  178. package/dist/primitives/SpeedDial.js +26 -0
  179. package/dist/primitives/Stack.d.ts +15 -0
  180. package/dist/primitives/Stack.d.ts.map +1 -0
  181. package/dist/primitives/Stack.js +45 -0
  182. package/dist/primitives/StarRating.d.ts +19 -1
  183. package/dist/primitives/StarRating.d.ts.map +1 -1
  184. package/dist/primitives/StarRating.js +36 -6
  185. package/dist/primitives/Stepper.d.ts +36 -0
  186. package/dist/primitives/Stepper.d.ts.map +1 -0
  187. package/dist/primitives/Stepper.js +53 -0
  188. package/dist/primitives/TablePagination.d.ts +16 -0
  189. package/dist/primitives/TablePagination.d.ts.map +1 -0
  190. package/dist/primitives/TablePagination.js +12 -0
  191. package/dist/primitives/TextField.d.ts +24 -0
  192. package/dist/primitives/TextField.d.ts.map +1 -0
  193. package/dist/primitives/TextField.js +19 -0
  194. package/dist/primitives/TextareaAutosize.d.ts +11 -0
  195. package/dist/primitives/TextareaAutosize.d.ts.map +1 -0
  196. package/dist/primitives/TextareaAutosize.js +26 -0
  197. package/dist/primitives/ToggleButton.d.ts +28 -0
  198. package/dist/primitives/ToggleButton.d.ts.map +1 -0
  199. package/dist/primitives/ToggleButton.js +56 -0
  200. package/dist/primitives/TransferList.d.ts +21 -0
  201. package/dist/primitives/TransferList.d.ts.map +1 -0
  202. package/dist/primitives/TransferList.js +40 -0
  203. package/dist/primitives/Typography.d.ts +23 -0
  204. package/dist/primitives/Typography.d.ts.map +1 -0
  205. package/dist/primitives/Typography.js +69 -0
  206. package/dist/primitives/transitions.d.ts +29 -0
  207. package/dist/primitives/transitions.d.ts.map +1 -0
  208. package/dist/primitives/transitions.js +52 -0
  209. package/package.json +28 -4
  210. package/skills/spatika-ui/SKILL.md +96 -0
  211. package/src/composites/Card.tsx +51 -1
  212. package/src/composites/ChartDataGrid.test.tsx +57 -0
  213. package/src/composites/ChartDataGrid.tsx +196 -0
  214. package/src/composites/CoverHero.test.tsx +38 -0
  215. package/src/composites/CoverHero.tsx +29 -22
  216. package/src/composites/EntityMediaCard.test.tsx +58 -0
  217. package/src/composites/EntityMediaCard.tsx +31 -13
  218. package/src/composites/EventCalendar.tsx +928 -0
  219. package/src/composites/EventEditor.tsx +268 -0
  220. package/src/composites/EventTimeline.tsx +357 -0
  221. package/src/composites/MapChart.test.tsx +39 -0
  222. package/src/composites/MapChart.tsx +130 -0
  223. package/src/composites/ProfileHero.test.tsx +33 -0
  224. package/src/composites/ProfileHero.tsx +96 -79
  225. package/src/composites/ScatterWebGL.test.tsx +25 -0
  226. package/src/composites/ScatterWebGL.tsx +79 -0
  227. package/src/composites/SiteFooter.test.tsx +37 -0
  228. package/src/composites/SiteFooter.tsx +1 -1
  229. package/src/composites/SiteNav.test.tsx +46 -0
  230. package/src/composites/SiteNav.tsx +14 -4
  231. package/src/composites/StatCard.test.tsx +30 -0
  232. package/src/composites/StatCard.tsx +4 -0
  233. package/src/composites/Wordmark.test.tsx +23 -0
  234. package/src/composites/Wordmark.tsx +2 -2
  235. package/src/composites/cartesian-charts.test.tsx +52 -0
  236. package/src/composites/cartesian-charts.tsx +1287 -0
  237. package/src/composites/chart-composition.test.tsx +53 -0
  238. package/src/composites/chart-composition.tsx +898 -0
  239. package/src/composites/chart-ui.tsx +328 -0
  240. package/src/composites/charts-3d.test.tsx +25 -0
  241. package/src/composites/charts-3d.tsx +318 -0
  242. package/src/composites/charts.test.tsx +254 -0
  243. package/src/composites/chrome.test.tsx +29 -0
  244. package/src/composites/flow-charts.test.tsx +79 -0
  245. package/src/composites/flow-charts.tsx +607 -0
  246. package/src/composites/marketing.test.tsx +38 -34
  247. package/src/composites/radial-charts.test.tsx +74 -0
  248. package/src/composites/radial-charts.tsx +617 -0
  249. package/src/composites/scheduler-ui.tsx +304 -0
  250. package/src/composites/scheduler.test.tsx +194 -0
  251. package/src/index.test.ts +22 -0
  252. package/src/index.ts +277 -5
  253. package/src/lib/chart-export.test.ts +20 -0
  254. package/src/lib/chart-export.ts +99 -0
  255. package/src/lib/charts-platform.test.ts +93 -0
  256. package/src/lib/charts.test.ts +132 -0
  257. package/src/lib/charts.ts +1021 -0
  258. package/src/lib/geo.test.ts +42 -0
  259. package/src/lib/geo.ts +138 -0
  260. package/src/lib/iso.test.ts +22 -0
  261. package/src/lib/iso.ts +102 -0
  262. package/src/lib/rrule.test.ts +53 -0
  263. package/src/lib/rrule.ts +281 -0
  264. package/src/lib/scheduler.test.ts +186 -0
  265. package/src/lib/scheduler.ts +688 -0
  266. package/src/lib/tokens.test.ts +2 -2
  267. package/src/lib/webgl-scatter.test.ts +36 -0
  268. package/src/lib/webgl-scatter.ts +230 -0
  269. package/src/primitives/Accordion.tsx +218 -63
  270. package/src/primitives/AppBar.tsx +39 -0
  271. package/src/primitives/Autocomplete.tsx +236 -0
  272. package/src/primitives/Avatar.tsx +52 -1
  273. package/src/primitives/Backdrop.tsx +48 -0
  274. package/src/primitives/BottomNavigation.tsx +102 -0
  275. package/src/primitives/ButtonBase.tsx +34 -0
  276. package/src/primitives/ButtonGroup.tsx +90 -0
  277. package/src/primitives/CircularProgress.tsx +104 -0
  278. package/src/primitives/ClickAwayListener.tsx +46 -0
  279. package/src/primitives/Collapse.tsx +39 -0
  280. package/src/primitives/Container.tsx +40 -0
  281. package/src/primitives/Fab.tsx +62 -0
  282. package/src/primitives/FormControl.tsx +90 -0
  283. package/src/primitives/FormControlLabel.tsx +51 -0
  284. package/src/primitives/Grid.tsx +101 -0
  285. package/src/primitives/IconButton.tsx +59 -0
  286. package/src/primitives/ImageList.test.tsx +48 -0
  287. package/src/primitives/ImageList.tsx +112 -0
  288. package/src/primitives/InputAdornment.tsx +86 -0
  289. package/src/primitives/Link.tsx +48 -0
  290. package/src/primitives/List.tsx +121 -0
  291. package/src/primitives/Masonry.tsx +32 -0
  292. package/src/primitives/Menu.tsx +112 -0
  293. package/src/primitives/MobileStepper.tsx +64 -0
  294. package/src/primitives/Modal.tsx +70 -0
  295. package/src/primitives/NoSsr.tsx +28 -0
  296. package/src/primitives/Paper.tsx +55 -0
  297. package/src/primitives/Progress.tsx +25 -7
  298. package/src/primitives/Snackbar.tsx +83 -0
  299. package/src/primitives/SpeedDial.tsx +110 -0
  300. package/src/primitives/Stack.tsx +88 -0
  301. package/src/primitives/StarRating.tsx +87 -28
  302. package/src/primitives/Stepper.tsx +184 -0
  303. package/src/primitives/TablePagination.tsx +58 -0
  304. package/src/primitives/TextField.tsx +105 -0
  305. package/src/primitives/TextareaAutosize.tsx +49 -0
  306. package/src/primitives/ToggleButton.tsx +148 -0
  307. package/src/primitives/TransferList.tsx +110 -0
  308. package/src/primitives/Typography.tsx +90 -0
  309. package/src/primitives/mui-more.test.tsx +76 -0
  310. package/src/primitives/mui.test.tsx +134 -0
  311. package/src/primitives/transitions.tsx +120 -0
@@ -0,0 +1,1287 @@
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
+ }