@spatika/react 1.1.0 → 1.3.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 +72 -0
  16. package/dist/composites/EventCalendar.d.ts.map +1 -0
  17. package/dist/composites/EventCalendar.js +371 -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 +29 -0
  22. package/dist/composites/EventTimeline.d.ts.map +1 -0
  23. package/dist/composites/EventTimeline.js +186 -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 +44 -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 +68 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +58 -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 +156 -0
  84. package/dist/lib/scheduler.d.ts.map +1 -0
  85. package/dist/lib/scheduler.js +502 -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 +1030 -0
  219. package/src/composites/EventEditor.tsx +268 -0
  220. package/src/composites/EventTimeline.tsx +366 -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 +306 -0
  250. package/src/composites/scheduler.test.tsx +280 -0
  251. package/src/index.test.ts +26 -0
  252. package/src/index.ts +287 -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 +199 -0
  265. package/src/lib/scheduler.ts +693 -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,898 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type PointerEvent as ReactPointerEvent,
11
+ type ReactNode,
12
+ type RefObject,
13
+ } from "react";
14
+ import { Download, Maximize2, Move, SquareDashedMousePointer, ZoomIn, ZoomOut } from "lucide-react";
15
+ import { cn } from "../lib/cn";
16
+ import { exportChart } from "../lib/chart-export";
17
+ import {
18
+ DEFAULT_CHART_MARGIN,
19
+ DEFAULT_ZOOM,
20
+ applyZoom,
21
+ areaPath,
22
+ axisDataFromDataset,
23
+ bandScale,
24
+ chartColor,
25
+ clampZoom,
26
+ extent,
27
+ formatAxisValue,
28
+ formatChartNumber,
29
+ inferScaleType,
30
+ linePath,
31
+ linearScale,
32
+ markOpacity,
33
+ niceTicks,
34
+ panZoom,
35
+ plotBox,
36
+ pointScale,
37
+ rangeAreaPath,
38
+ roundedRectPath,
39
+ seriesDataFromDataset,
40
+ stackSeries,
41
+ wheelZoom,
42
+ zoomFromBrush,
43
+ type ChartAxisConfig,
44
+ type ChartCurve,
45
+ type ChartDataset,
46
+ type ChartInteraction,
47
+ type ChartMargin,
48
+ type ChartZoom,
49
+ type HighlightScope,
50
+ type PlotBox,
51
+ } from "../lib/charts";
52
+ import { IconButton } from "../primitives/IconButton";
53
+ import {
54
+ CartesianGrid,
55
+ CategoryAxis,
56
+ ChartLegend,
57
+ ChartTooltip,
58
+ ValueAxis,
59
+ categoryLabels,
60
+ formatSeriesValue,
61
+ resolveMargin,
62
+ seriesMeta,
63
+ type ChartHover,
64
+ } from "./chart-ui";
65
+
66
+ export type ComposedSeriesType = "bar" | "line" | "area" | "scatter";
67
+
68
+ export type ComposedSeries = {
69
+ id?: string;
70
+ type?: ComposedSeriesType;
71
+ label?: string;
72
+ color?: string;
73
+ data?: Array<number | null>;
74
+ dataKey?: string;
75
+ stack?: string;
76
+ yAxisId?: string;
77
+ curve?: ChartCurve;
78
+ showMark?: boolean;
79
+ area?: boolean;
80
+ scatter?: Array<{ x: number; y: number; id?: string | number }>;
81
+ };
82
+
83
+ type ResolvedSeries = {
84
+ id: string;
85
+ type: ComposedSeriesType;
86
+ label: string;
87
+ color: string;
88
+ data: Array<number | null>;
89
+ stack?: string;
90
+ yAxisId?: string;
91
+ curve: ChartCurve;
92
+ showMark: boolean;
93
+ scatter?: Array<{ x: number; y: number; id?: string | number }>;
94
+ };
95
+
96
+ export type HighlightedItem = { seriesId: string; dataIndex: number } | null;
97
+
98
+ type ChartContextValue = {
99
+ slot: string;
100
+ width: number;
101
+ height: number;
102
+ margin: Required<ChartMargin>;
103
+ plot: PlotBox;
104
+ categories: Array<string | number | Date>;
105
+ series: ResolvedSeries[];
106
+ hidden: Set<string>;
107
+ toggleSeries: (id: string) => void;
108
+ xAt: (index: number) => number;
109
+ yAt: (value: number, yAxisId?: string) => number;
110
+ bandwidth: number;
111
+ ticksLeft: number[];
112
+ ticksRight: number[];
113
+ rightAxis: ChartAxisConfig | undefined;
114
+ leftAxis: ChartAxisConfig | undefined;
115
+ xAxis?: ChartAxisConfig;
116
+ hideGrid?: boolean;
117
+ stacked: boolean;
118
+ barGapRatio: number;
119
+ borderRadius: number;
120
+ zoom: ChartZoom;
121
+ setZoom: (zoom: ChartZoom) => void;
122
+ resetZoom: () => void;
123
+ zoomEnabled: boolean;
124
+ interaction: ChartInteraction;
125
+ setInteraction: (next: ChartInteraction) => void;
126
+ highlightScope: HighlightScope;
127
+ highlighted: HighlightedItem;
128
+ setHighlighted: (item: HighlightedItem) => void;
129
+ hover: ChartHover;
130
+ setHover: (hover: ChartHover) => void;
131
+ svgRef: RefObject<SVGSVGElement | null>;
132
+ clipPathId: string;
133
+ locale?: string;
134
+ animated: boolean;
135
+ brush: { x0: number; x1: number } | null;
136
+ setBrush: (brush: { x0: number; x1: number } | null) => void;
137
+ exportChart: (format: "svg" | "png") => Promise<void>;
138
+ };
139
+
140
+ const ChartContext = createContext<ChartContextValue | null>(null);
141
+
142
+ export function useChartContext(): ChartContextValue {
143
+ const value = useContext(ChartContext);
144
+ if (!value) throw new Error("Chart composition components must be used inside ChartContainer");
145
+ return value;
146
+ }
147
+
148
+ function useOptionalChartContext() {
149
+ return useContext(ChartContext);
150
+ }
151
+
152
+ export type ChartContainerProps = {
153
+ slot?: string;
154
+ className?: string;
155
+ width?: number;
156
+ height?: number;
157
+ margin?: ChartMargin;
158
+ dataset?: ChartDataset;
159
+ xAxis?: ChartAxisConfig[];
160
+ yAxis?: ChartAxisConfig[];
161
+ series: ComposedSeries[];
162
+ colors?: string[];
163
+ hideLegend?: boolean;
164
+ hideGrid?: boolean;
165
+ stacked?: boolean;
166
+ barGapRatio?: number;
167
+ borderRadius?: number;
168
+ zoom?: boolean | ChartZoom;
169
+ defaultZoom?: ChartZoom;
170
+ onZoomChange?: (zoom: ChartZoom) => void;
171
+ showToolbar?: boolean;
172
+ highlightScope?: HighlightScope;
173
+ selected?: HighlightedItem;
174
+ animated?: boolean;
175
+ locale?: string;
176
+ children?: ReactNode;
177
+ "aria-label"?: string;
178
+ };
179
+
180
+ function useElementSize(ref: RefObject<HTMLElement | null>, fallbackWidth: number, fallbackHeight: number) {
181
+ const [size, setSize] = useState({ width: fallbackWidth, height: fallbackHeight });
182
+ useEffect(() => {
183
+ const el = ref.current;
184
+ if (!el) return;
185
+ const apply = () =>
186
+ setSize({
187
+ width: el.clientWidth || fallbackWidth,
188
+ height: el.clientHeight || fallbackHeight,
189
+ });
190
+ apply();
191
+ if (typeof ResizeObserver === "undefined") return;
192
+ const observer = new ResizeObserver(apply);
193
+ observer.observe(el);
194
+ return () => observer.disconnect();
195
+ }, [fallbackHeight, fallbackWidth, ref]);
196
+ return size;
197
+ }
198
+
199
+ function resolveSeries(series: ComposedSeries[], dataset: ChartDataset | undefined, colors?: string[]): ResolvedSeries[] {
200
+ return series.map((item, index) => {
201
+ const data =
202
+ item.data ??
203
+ (dataset && item.dataKey ? seriesDataFromDataset(dataset, item.dataKey) : []);
204
+ const type: ComposedSeriesType = item.type ?? (item.area ? "area" : "bar");
205
+ return {
206
+ id: item.id ?? item.label ?? item.dataKey ?? `series-${index}`,
207
+ type,
208
+ label: item.label ?? item.dataKey ?? item.id ?? `Series ${index + 1}`,
209
+ color: item.color ?? chartColor(index, colors),
210
+ data,
211
+ stack: item.stack,
212
+ yAxisId: item.yAxisId,
213
+ curve: item.curve ?? "monotone",
214
+ showMark: item.showMark ?? type !== "bar",
215
+ scatter: item.scatter,
216
+ };
217
+ });
218
+ }
219
+
220
+ function numericDomain(series: ResolvedSeries[], stacked: boolean, axis?: ChartAxisConfig): [number, number] {
221
+ if (axis?.min != null && axis?.max != null) return [axis.min, axis.max];
222
+ const values: number[] = [];
223
+ const subset = series.filter((item) => (item.yAxisId ?? "left") === (axis?.id ?? item.yAxisId ?? "left") || !axis?.id);
224
+ const relevant = axis?.id ? series.filter((item) => (item.yAxisId ?? leftId(axis)) === axis.id) : series;
225
+ const used = relevant.length ? relevant : subset;
226
+ if (stacked) {
227
+ for (const kind of ["bar", "area"] as const) {
228
+ const group = used.filter((item) => item.type === kind);
229
+ if (!group.length) continue;
230
+ const stacks = stackSeries(group.map((item) => item.data.map((v) => v ?? 0)));
231
+ for (const segs of stacks) for (const seg of segs) values.push(seg.start, seg.end);
232
+ }
233
+ for (const item of used) {
234
+ if (item.type === "bar" || item.type === "area") continue;
235
+ if (item.scatter) for (const p of item.scatter) values.push(p.y);
236
+ else for (const value of item.data) if (value != null) values.push(value);
237
+ }
238
+ } else {
239
+ for (const item of used) {
240
+ if (item.scatter) for (const p of item.scatter) values.push(p.y);
241
+ else for (const value of item.data) if (value != null) values.push(value);
242
+ }
243
+ }
244
+ if (!values.length) values.push(0, 1);
245
+ if (Math.min(...values) > 0) values.push(0);
246
+ const [min, max] = extent(values, 0.04);
247
+ return [axis?.min ?? min, axis?.max ?? max];
248
+ }
249
+
250
+ function leftId(axis?: ChartAxisConfig) {
251
+ return axis?.id ?? "left";
252
+ }
253
+
254
+ export function ChartContainer({
255
+ slot = "chart-container",
256
+ className,
257
+ width,
258
+ height = 280,
259
+ margin,
260
+ dataset,
261
+ xAxis,
262
+ yAxis,
263
+ series,
264
+ colors,
265
+ hideLegend,
266
+ hideGrid,
267
+ stacked = false,
268
+ barGapRatio = 0.15,
269
+ borderRadius = 6,
270
+ zoom,
271
+ defaultZoom,
272
+ onZoomChange,
273
+ showToolbar,
274
+ highlightScope = "item",
275
+ selected,
276
+ animated = true,
277
+ locale,
278
+ children,
279
+ "aria-label": ariaLabel,
280
+ }: ChartContainerProps) {
281
+ const surfaceRef = useRef<HTMLDivElement>(null);
282
+ const svgRef = useRef<SVGSVGElement>(null);
283
+ const clipPathId = `spk-chart-clip-${useId().replace(/:/g, "")}`;
284
+ const measured = useElementSize(surfaceRef, width ?? 320, height);
285
+ const plotWidth = width ?? measured.width;
286
+ const hasRight = yAxis?.some((axis) => axis.position === "right");
287
+ const m = resolveMargin(
288
+ margin ?? (hasRight ? { ...DEFAULT_CHART_MARGIN, right: 48 } : undefined),
289
+ );
290
+ const resolved = useMemo(() => resolveSeries(series, dataset, colors), [series, dataset, colors]);
291
+ const xConfig = xAxis?.[0];
292
+ const categories = useMemo(() => {
293
+ if (dataset && xConfig?.dataKey) return axisDataFromDataset(dataset, xConfig.dataKey);
294
+ return categoryLabels(xConfig, resolved[0]?.data.length ?? resolved[0]?.scatter?.length ?? 0);
295
+ }, [dataset, xConfig, resolved]);
296
+ const leftAxis = yAxis?.find((axis) => (axis.position ?? "left") === "left") ?? yAxis?.[0];
297
+ const rightAxis = yAxis?.find((axis) => axis.position === "right");
298
+ const [hidden, setHidden] = useState<Set<string>>(new Set());
299
+ const toggleSeries = (id: string) => {
300
+ setHidden((current) => {
301
+ const next = new Set(current);
302
+ if (next.has(id)) next.delete(id);
303
+ else next.add(id);
304
+ return next;
305
+ });
306
+ };
307
+ const zoomEnabled = zoom === true || typeof zoom === "object";
308
+ const controlledZoom = typeof zoom === "object" ? zoom : undefined;
309
+ const [innerZoom, setInnerZoom] = useState<ChartZoom>(defaultZoom ?? DEFAULT_ZOOM);
310
+ const currentZoom = clampZoom(controlledZoom ?? innerZoom);
311
+ const setZoom = (next: ChartZoom) => {
312
+ const clamped = clampZoom(next);
313
+ if (!controlledZoom) setInnerZoom(clamped);
314
+ onZoomChange?.(clamped);
315
+ };
316
+ const resetZoom = () => setZoom(DEFAULT_ZOOM);
317
+ const [interaction, setInteraction] = useState<ChartInteraction>("none");
318
+ const [highlighted, setHighlighted] = useState<HighlightedItem>(null);
319
+ const [hover, setHover] = useState<ChartHover>(null);
320
+ const [brush, setBrush] = useState<{ x0: number; x1: number } | null>(null);
321
+ const activeHighlight = selected ?? highlighted;
322
+
323
+ const plot = plotBox(plotWidth, height, m);
324
+ const visible = resolved.filter((item) => !hidden.has(item.id));
325
+ const leftSeries = visible.filter((item) => !item.yAxisId || item.yAxisId === (leftAxis?.id ?? item.yAxisId));
326
+ const rightSeries = visible.filter((item) => rightAxis && item.yAxisId === rightAxis.id);
327
+ const leftDomain = numericDomain(leftSeries.length ? leftSeries : visible, stacked, leftAxis);
328
+ const rightDomain = rightAxis ? numericDomain(rightSeries.length ? rightSeries : visible, false, rightAxis) : leftDomain;
329
+ const ticksLeft = niceTicks(leftDomain[0], leftDomain[1], 5);
330
+ const ticksRight = niceTicks(rightDomain[0], rightDomain[1], 5);
331
+ const scaleType = xConfig?.scaleType ?? inferScaleType(xConfig?.data ?? categories);
332
+ const yLeft = linearScale(leftDomain, [plot.bottom, plot.top]);
333
+ const yRight = linearScale(rightDomain, [plot.bottom, plot.top]);
334
+
335
+ const { xAt, bandwidth } = useMemo(() => {
336
+ let xAtInner = (index: number) =>
337
+ plot.left + plot.iw * (categories.length <= 1 ? 0.5 : index / (categories.length - 1));
338
+ let bw = 0;
339
+ if (scaleType === "band" || scaleType === undefined) {
340
+ const scale = bandScale(categories.map(String), [plot.left, plot.right], 0.24);
341
+ xAtInner = (index) => scale(String(categories[index]));
342
+ bw = scale.bandwidth;
343
+ } else if (scaleType === "point") {
344
+ const scale = pointScale(categories.map(String), [plot.left, plot.right]);
345
+ xAtInner = (index) => scale(String(categories[index]));
346
+ } else if (xConfig?.data?.every((v) => typeof v === "number" || v instanceof Date)) {
347
+ const xs = xConfig.data.map((v) => (v instanceof Date ? v.getTime() : Number(v)));
348
+ const [min, max] = extent(xs);
349
+ const scale = linearScale([min, max], [plot.left, plot.right]);
350
+ xAtInner = (index) => scale(xs[index] ?? min);
351
+ }
352
+ const zoomX = (index: number) => applyZoom(xAtInner(index), plot.left, plot.iw, currentZoom.x);
353
+ return { xAt: zoomX, bandwidth: bw / (currentZoom.x[1] - currentZoom.x[0] || 1) };
354
+ }, [categories, currentZoom.x, plot.iw, plot.left, plot.right, scaleType, xConfig?.data]);
355
+
356
+ const yAt = useCallback(
357
+ (value: number, yAxisId?: string) => {
358
+ const raw = yAxisId && rightAxis && yAxisId === rightAxis.id ? yRight(value) : yLeft(value);
359
+ if (!currentZoom.y) return raw;
360
+ return applyZoom(raw, plot.top, plot.ih, currentZoom.y);
361
+ },
362
+ [currentZoom.y, plot.ih, plot.top, rightAxis, yLeft, yRight],
363
+ );
364
+
365
+ const doExport = useCallback(
366
+ async (format: "svg" | "png") => {
367
+ await exportChart(svgRef.current, format);
368
+ },
369
+ [],
370
+ );
371
+
372
+ const ctx: ChartContextValue = {
373
+ slot,
374
+ width: plotWidth,
375
+ height,
376
+ margin: m,
377
+ plot,
378
+ categories,
379
+ series: resolved,
380
+ hidden,
381
+ toggleSeries,
382
+ xAt,
383
+ yAt,
384
+ bandwidth,
385
+ ticksLeft,
386
+ ticksRight,
387
+ rightAxis,
388
+ leftAxis,
389
+ xAxis: xConfig,
390
+ hideGrid,
391
+ stacked,
392
+ barGapRatio,
393
+ borderRadius,
394
+ zoom: currentZoom,
395
+ setZoom,
396
+ resetZoom,
397
+ zoomEnabled,
398
+ interaction,
399
+ setInteraction,
400
+ highlightScope,
401
+ highlighted: activeHighlight,
402
+ setHighlighted,
403
+ hover,
404
+ setHover,
405
+ svgRef,
406
+ clipPathId,
407
+ locale,
408
+ animated,
409
+ brush,
410
+ setBrush,
411
+ exportChart: doExport,
412
+ };
413
+
414
+ const legend = seriesMeta(resolved, colors);
415
+ const label =
416
+ ariaLabel ??
417
+ `Chart of ${resolved.map((item) => item.label).join(", ")}`;
418
+
419
+ return (
420
+ <ChartContext.Provider value={ctx}>
421
+ <div
422
+ data-slot={slot}
423
+ className={cn("spk-chart", animated && "spk-chart--animated", className)}
424
+ role="img"
425
+ aria-label={label}
426
+ >
427
+ {showToolbar ? <ChartsToolbar /> : null}
428
+ {hideLegend ? null : (
429
+ <ChartLegend items={legend} hiddenIds={hidden} onToggle={toggleSeries} />
430
+ )}
431
+ <div ref={surfaceRef} className="spk-chart-surface" style={{ height }}>
432
+ {plotWidth > 0 ? children ?? <DefaultChartSurface /> : null}
433
+ <ChartTooltip hover={hover} boundsWidth={plotWidth} />
434
+ </div>
435
+ <ChartA11yTable />
436
+ </div>
437
+ </ChartContext.Provider>
438
+ );
439
+ }
440
+
441
+ function DefaultChartSurface() {
442
+ const { rightAxis } = useChartContext();
443
+ return (
444
+ <ChartSurface>
445
+ <ChartsGrid />
446
+ <BarPlot />
447
+ <AreaPlot />
448
+ <LinePlot />
449
+ <ScatterPlot />
450
+ <ChartsXAxis />
451
+ <ChartsYAxis />
452
+ {rightAxis ? <ChartsYAxis position="right" /> : null}
453
+ <ChartsBrush />
454
+ </ChartSurface>
455
+ );
456
+ }
457
+
458
+ export function ChartSurface({ children, className }: { children?: ReactNode; className?: string }) {
459
+ const ctx = useChartContext();
460
+ const drag = useRef<{ x: number; y: number; zoom: ChartZoom } | null>(null);
461
+ const { plot, zoomEnabled, interaction, setZoom, zoom, setBrush, setHover, setHighlighted, svgRef } = ctx;
462
+
463
+ useEffect(() => {
464
+ const svg = svgRef.current;
465
+ if (!svg || !zoomEnabled) return;
466
+ const onWheelNative = (event: WheelEvent) => {
467
+ event.preventDefault();
468
+ const bounds = svg.getBoundingClientRect();
469
+ const x = event.clientX - bounds.left;
470
+ const anchor = (x - plot.left) / plot.iw;
471
+ const factor = event.deltaY > 0 ? 1.12 : 1 / 1.12;
472
+ setZoom(wheelZoom(zoom, clamp01(anchor), factor));
473
+ };
474
+ svg.addEventListener("wheel", onWheelNative, { passive: false });
475
+ return () => svg.removeEventListener("wheel", onWheelNative);
476
+ }, [plot.iw, plot.left, setZoom, svgRef, zoom, zoomEnabled]);
477
+
478
+ const onPointerDown = (event: ReactPointerEvent<SVGSVGElement>) => {
479
+ if (!zoomEnabled) return;
480
+ const svg = event.currentTarget;
481
+ const bounds = svg.getBoundingClientRect();
482
+ const x = event.clientX - bounds.left;
483
+ const y = event.clientY - bounds.top;
484
+ if (x < plot.left || x > plot.right || y < plot.top || y > plot.bottom) return;
485
+ svg.setPointerCapture(event.pointerId);
486
+ if (interaction === "brush") {
487
+ setBrush({ x0: x, x1: x });
488
+ return;
489
+ }
490
+ if (interaction === "pan" || event.shiftKey) {
491
+ drag.current = { x, y, zoom };
492
+ }
493
+ };
494
+
495
+ const onPointerMove = (event: ReactPointerEvent<SVGSVGElement>) => {
496
+ const svg = event.currentTarget;
497
+ const bounds = svg.getBoundingClientRect();
498
+ const x = event.clientX - bounds.left;
499
+ if (ctx.brush) {
500
+ setBrush({ x0: ctx.brush.x0, x1: x });
501
+ return;
502
+ }
503
+ if (!drag.current) return;
504
+ const dx = (x - drag.current.x) / plot.iw;
505
+ setZoom(panZoom(drag.current.zoom, dx));
506
+ };
507
+
508
+ const onPointerUp = () => {
509
+ if (ctx.brush) {
510
+ const next = zoomFromBrush(ctx.brush.x0, ctx.brush.x1, plot.left, plot.iw, zoom);
511
+ setZoom(next);
512
+ setBrush(null);
513
+ }
514
+ drag.current = null;
515
+ };
516
+
517
+ return (
518
+ <svg
519
+ ref={ctx.svgRef}
520
+ className={className}
521
+ width={ctx.width}
522
+ height={ctx.height}
523
+ viewBox={`0 0 ${ctx.width} ${ctx.height}`}
524
+ data-slot="chart-surface"
525
+ onPointerDown={onPointerDown}
526
+ onPointerMove={onPointerMove}
527
+ onPointerUp={onPointerUp}
528
+ onPointerCancel={onPointerUp}
529
+ onMouseLeave={() => {
530
+ setHover(null);
531
+ setHighlighted(null);
532
+ }}
533
+ >
534
+ <defs>
535
+ <clipPath id={ctx.clipPathId}>
536
+ <rect x={plot.left} y={plot.top} width={plot.iw} height={plot.ih} />
537
+ </clipPath>
538
+ </defs>
539
+ {children}
540
+ </svg>
541
+ );
542
+ }
543
+
544
+ function clamp01(value: number) {
545
+ return Math.min(1, Math.max(0, value));
546
+ }
547
+
548
+ export function ChartsGrid() {
549
+ const { hideGrid, ticksLeft, yAt, plot } = useChartContext();
550
+ if (hideGrid) return null;
551
+ return (
552
+ <CartesianGrid
553
+ ticks={ticksLeft}
554
+ scale={(value) => yAt(value)}
555
+ left={plot.left}
556
+ right={plot.right}
557
+ top={plot.top}
558
+ bottom={plot.bottom}
559
+ />
560
+ );
561
+ }
562
+
563
+ export function ChartsXAxis() {
564
+ const { xAxis, categories, xAt, plot } = useChartContext();
565
+ if (xAxis?.hide) return null;
566
+ return <CategoryAxis labels={categories} position={xAt} y={plot.bottom} />;
567
+ }
568
+
569
+ export function ChartsYAxis({ position = "left" }: { position?: "left" | "right" }) {
570
+ const { ticksLeft, ticksRight, yAt, plot, leftAxis, rightAxis, locale } = useChartContext();
571
+ if (position === "right") {
572
+ if (!rightAxis || rightAxis.hide) return null;
573
+ return (
574
+ <g data-slot="chart-y-axis-right">
575
+ {ticksRight.map((tick) => (
576
+ <text
577
+ key={tick}
578
+ className="spk-chart-tick"
579
+ x={plot.right + 8}
580
+ y={yAt(tick, rightAxis.id)}
581
+ dominantBaseline="middle"
582
+ >
583
+ {formatChartNumber(tick, locale)}
584
+ </text>
585
+ ))}
586
+ </g>
587
+ );
588
+ }
589
+ if (leftAxis?.hide) return null;
590
+ return <ValueAxis ticks={ticksLeft} scale={(value) => yAt(value, leftAxis?.id)} left={plot.left} />;
591
+ }
592
+
593
+ function PlotClip({ children, slot }: { children: ReactNode; slot: string }) {
594
+ const { clipPathId } = useChartContext();
595
+ return (
596
+ <g data-slot={slot} clipPath={`url(#${clipPathId})`}>
597
+ {children}
598
+ </g>
599
+ );
600
+ }
601
+
602
+ export function BarPlot() {
603
+ const ctx = useChartContext();
604
+ const bars = ctx.series.filter((item) => item.type === "bar" && !ctx.hidden.has(item.id));
605
+ if (!bars.length) return null;
606
+ const stacks = ctx.stacked ? stackSeries(bars.map((item) => item.data.map((v) => v ?? 0))) : null;
607
+ const n = ctx.stacked ? 1 : Math.max(bars.length, 1);
608
+ const gap = n > 1 ? ctx.bandwidth * ctx.barGapRatio : 0;
609
+ const barW = (ctx.bandwidth - gap * (n - 1)) / n;
610
+ const zero = ctx.yAt(0);
611
+ return (
612
+ <PlotClip slot="bar-plot">
613
+ {bars.map((item, s) =>
614
+ item.data.map((value, i) => {
615
+ if (value == null) return null;
616
+ let x = ctx.xAt(i);
617
+ let y1: number;
618
+ let y2: number;
619
+ if (stacks) {
620
+ const seg = stacks[s]![i]!;
621
+ y1 = ctx.yAt(seg.end, item.yAxisId);
622
+ y2 = ctx.yAt(seg.start, item.yAxisId);
623
+ } else {
624
+ x = ctx.xAt(i) + s * (barW + gap);
625
+ y1 = ctx.yAt(value, item.yAxisId);
626
+ y2 = zero;
627
+ }
628
+ const y = Math.min(y1, y2);
629
+ const h = Math.max(1, Math.abs(y2 - y1));
630
+ const opacity = markOpacity(ctx.highlightScope, ctx.highlighted, item.id, i);
631
+ return (
632
+ <path
633
+ key={`${item.id}-${i}`}
634
+ className="spk-chart-mark"
635
+ d={roundedRectPath(x, y, ctx.stacked ? ctx.bandwidth : barW, h, ctx.borderRadius)}
636
+ fill={item.color}
637
+ opacity={opacity}
638
+ onMouseEnter={(event) => {
639
+ ctx.setHighlighted({ seriesId: item.id, dataIndex: i });
640
+ ctx.setHover({
641
+ x: event.nativeEvent.offsetX,
642
+ y: event.nativeEvent.offsetY,
643
+ title: String(ctx.categories[i]),
644
+ items: [{ color: item.color, label: item.label, value: formatSeriesValue(value) }],
645
+ });
646
+ }}
647
+ onMouseLeave={() => {
648
+ ctx.setHighlighted(null);
649
+ ctx.setHover(null);
650
+ }}
651
+ />
652
+ );
653
+ }),
654
+ )}
655
+ </PlotClip>
656
+ );
657
+ }
658
+
659
+ function LineLikePlot({ types, slot }: { types: ComposedSeriesType[]; slot: string }) {
660
+ const ctx = useChartContext();
661
+ const items = ctx.series.filter((item) => types.includes(item.type) && !ctx.hidden.has(item.id));
662
+ if (!items.length) return null;
663
+ const stackAreas = ctx.stacked && types.includes("area") && items.every((item) => item.type === "area");
664
+ const stacks = stackAreas ? stackSeries(items.map((item) => item.data.map((v) => v ?? 0))) : null;
665
+ return (
666
+ <PlotClip slot={slot}>
667
+ {items.map((item, s) => {
668
+ const fill = item.type === "area";
669
+ const stackedSegs = stacks?.[s];
670
+ const pts = item.data
671
+ .map((value, i) => {
672
+ if (value == null) return null;
673
+ const yValue = stackedSegs ? stackedSegs[i]!.end : value;
674
+ return { x: ctx.xAt(i), y: ctx.yAt(yValue, item.yAxisId), value, i };
675
+ })
676
+ .filter((p): p is { x: number; y: number; value: number; i: number } => p != null);
677
+ const baseline = stackedSegs
678
+ ? pts.map((pt) => ({
679
+ x: pt.x,
680
+ y: ctx.yAt(stackedSegs[pt.i]!.start, item.yAxisId),
681
+ }))
682
+ : null;
683
+ const opacity = markOpacity(ctx.highlightScope, ctx.highlighted, item.id);
684
+ return (
685
+ <g key={item.id} opacity={opacity}>
686
+ {fill ? (
687
+ <path
688
+ d={
689
+ baseline
690
+ ? rangeAreaPath(pts, baseline, item.curve)
691
+ : areaPath(pts, ctx.plot.bottom, item.curve)
692
+ }
693
+ fill={item.color}
694
+ opacity={baseline ? 0.88 : 0.18}
695
+ className="spk-chart-mark"
696
+ />
697
+ ) : null}
698
+ <path
699
+ d={linePath(pts, item.curve)}
700
+ fill="none"
701
+ stroke={item.color}
702
+ strokeWidth={2.25}
703
+ strokeLinejoin="round"
704
+ strokeLinecap="round"
705
+ className="spk-chart-mark"
706
+ />
707
+ {item.showMark
708
+ ? pts.map((pt) => (
709
+ <circle
710
+ key={pt.i}
711
+ cx={pt.x}
712
+ cy={pt.y}
713
+ r={3.4}
714
+ fill={item.color}
715
+ className="spk-chart-mark"
716
+ onMouseEnter={() => {
717
+ ctx.setHighlighted({ seriesId: item.id, dataIndex: pt.i });
718
+ ctx.setHover({
719
+ x: pt.x,
720
+ y: pt.y,
721
+ title: String(ctx.categories[pt.i]),
722
+ items: [{ color: item.color, label: item.label, value: formatSeriesValue(pt.value) }],
723
+ });
724
+ }}
725
+ onMouseLeave={() => {
726
+ ctx.setHighlighted(null);
727
+ ctx.setHover(null);
728
+ }}
729
+ />
730
+ ))
731
+ : null}
732
+ </g>
733
+ );
734
+ })}
735
+ </PlotClip>
736
+ );
737
+ }
738
+
739
+ export function LinePlot() {
740
+ return <LineLikePlot types={["line"]} slot="line-plot" />;
741
+ }
742
+
743
+ export function AreaPlot() {
744
+ return <LineLikePlot types={["area"]} slot="area-plot" />;
745
+ }
746
+
747
+ export function ScatterPlot() {
748
+ const ctx = useChartContext();
749
+ const items = ctx.series.filter((item) => item.type === "scatter" && !ctx.hidden.has(item.id));
750
+ if (!items.length) return null;
751
+ return (
752
+ <PlotClip slot="scatter-plot">
753
+ {items.map((item) => {
754
+ const points =
755
+ item.scatter ??
756
+ item.data
757
+ .map((value, i) => (value == null ? null : { x: i, y: value, id: i }))
758
+ .filter((p): p is { x: number; y: number; id: number } => p != null);
759
+ const xs = points.map((p) => p.x);
760
+ const xDomain = extent(xs, 0.04);
761
+ const xScale = linearScale(xDomain, [ctx.plot.left, ctx.plot.right]);
762
+ return points.map((point, i) => {
763
+ const cx = applyZoom(xScale(point.x), ctx.plot.left, ctx.plot.iw, ctx.zoom.x);
764
+ const cy = ctx.yAt(point.y, item.yAxisId);
765
+ const opacity = markOpacity(ctx.highlightScope, ctx.highlighted, item.id, i);
766
+ return (
767
+ <circle
768
+ key={`${item.id}-${point.id ?? i}`}
769
+ cx={cx}
770
+ cy={cy}
771
+ r={4.5}
772
+ fill={item.color}
773
+ opacity={opacity}
774
+ className="spk-chart-mark"
775
+ onMouseEnter={(event) => {
776
+ ctx.setHighlighted({ seriesId: item.id, dataIndex: i });
777
+ ctx.setHover({
778
+ x: event.nativeEvent.offsetX,
779
+ y: event.nativeEvent.offsetY,
780
+ title: item.label,
781
+ items: [
782
+ { color: item.color, label: "x", value: formatChartNumber(point.x, ctx.locale) },
783
+ { color: item.color, label: "y", value: formatChartNumber(point.y, ctx.locale) },
784
+ ],
785
+ });
786
+ }}
787
+ onMouseLeave={() => {
788
+ ctx.setHighlighted(null);
789
+ ctx.setHover(null);
790
+ }}
791
+ />
792
+ );
793
+ });
794
+ })}
795
+ </PlotClip>
796
+ );
797
+ }
798
+
799
+ export function ChartsBrush() {
800
+ const { brush, plot } = useChartContext();
801
+ if (!brush) return null;
802
+ const x = Math.min(brush.x0, brush.x1);
803
+ const width = Math.abs(brush.x1 - brush.x0);
804
+ return (
805
+ <rect
806
+ data-slot="chart-brush"
807
+ className="spk-chart-brush"
808
+ x={x}
809
+ y={plot.top}
810
+ width={width}
811
+ height={plot.ih}
812
+ />
813
+ );
814
+ }
815
+
816
+ export function ChartsLegend() {
817
+ const { series, hidden, toggleSeries } = useChartContext();
818
+ return (
819
+ <ChartLegend
820
+ items={series.map((item) => ({ id: item.id, label: item.label, color: item.color }))}
821
+ hiddenIds={hidden}
822
+ onToggle={toggleSeries}
823
+ />
824
+ );
825
+ }
826
+
827
+ export function ChartsToolbar({ className }: { className?: string }) {
828
+ const ctx = useOptionalChartContext();
829
+ if (!ctx) return null;
830
+ const zoomIn = () => ctx.setZoom(wheelZoom(ctx.zoom, 0.5, 1 / 1.25));
831
+ const zoomOut = () => ctx.setZoom(wheelZoom(ctx.zoom, 0.5, 1.25));
832
+ return (
833
+ <div className={cn("spk-chart-toolbar", className)} role="toolbar" aria-label="Chart tools">
834
+ <IconButton size="sm" aria-label="Zoom in" onClick={zoomIn}>
835
+ <ZoomIn />
836
+ </IconButton>
837
+ <IconButton size="sm" aria-label="Zoom out" onClick={zoomOut}>
838
+ <ZoomOut />
839
+ </IconButton>
840
+ <IconButton size="sm" aria-label="Reset zoom" onClick={ctx.resetZoom}>
841
+ <Maximize2 />
842
+ </IconButton>
843
+ <IconButton
844
+ size="sm"
845
+ aria-label="Pan"
846
+ aria-pressed={ctx.interaction === "pan"}
847
+ data-active={ctx.interaction === "pan" ? "true" : undefined}
848
+ onClick={() => ctx.setInteraction(ctx.interaction === "pan" ? "none" : "pan")}
849
+ >
850
+ <Move />
851
+ </IconButton>
852
+ <IconButton
853
+ size="sm"
854
+ aria-label="Brush zoom"
855
+ aria-pressed={ctx.interaction === "brush"}
856
+ data-active={ctx.interaction === "brush" ? "true" : undefined}
857
+ onClick={() => ctx.setInteraction(ctx.interaction === "brush" ? "none" : "brush")}
858
+ >
859
+ <SquareDashedMousePointer />
860
+ </IconButton>
861
+ <IconButton size="sm" aria-label="Export SVG" onClick={() => void ctx.exportChart("svg")}>
862
+ <Download />
863
+ </IconButton>
864
+ <IconButton size="sm" aria-label="Export PNG" onClick={() => void ctx.exportChart("png")}>
865
+ <span className="spk-chart-toolbar-png">PNG</span>
866
+ </IconButton>
867
+ </div>
868
+ );
869
+ }
870
+
871
+ function ChartA11yTable() {
872
+ const { series, categories, slot } = useChartContext();
873
+ return (
874
+ <table className="spk-chart-a11y">
875
+ <caption>{slot} data</caption>
876
+ <thead>
877
+ <tr>
878
+ <th>Category</th>
879
+ {series.map((item) => (
880
+ <th key={item.id}>{item.label}</th>
881
+ ))}
882
+ </tr>
883
+ </thead>
884
+ <tbody>
885
+ {categories.map((category, i) => (
886
+ <tr key={`${i}-${String(category)}`}>
887
+ <th scope="row">{formatAxisValue(category)}</th>
888
+ {series.map((item) => (
889
+ <td key={item.id}>{item.data[i] == null ? "—" : formatChartNumber(item.data[i] as number)}</td>
890
+ ))}
891
+ </tr>
892
+ ))}
893
+ </tbody>
894
+ </table>
895
+ );
896
+ }
897
+
898
+ export { ChartContext };