@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,328 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ type ReactNode,
6
+ type RefObject,
7
+ } from "react";
8
+ import { cn } from "../lib/cn";
9
+ import {
10
+ DEFAULT_CHART_MARGIN,
11
+ chartColor,
12
+ formatAxisValue,
13
+ formatChartNumber,
14
+ type ChartAxisConfig,
15
+ type ChartMargin,
16
+ } from "../lib/charts";
17
+
18
+ export type ChartTooltipItem = {
19
+ color: string;
20
+ label: string;
21
+ value: string;
22
+ };
23
+
24
+ export type ChartLegendItem = {
25
+ id: string;
26
+ label: string;
27
+ color: string;
28
+ };
29
+
30
+ export type ChartHover = {
31
+ x: number;
32
+ y: number;
33
+ title?: string;
34
+ items: ChartTooltipItem[];
35
+ } | null;
36
+
37
+ export type ChartFrameProps = {
38
+ slot: string;
39
+ className?: string;
40
+ width?: number;
41
+ height?: number;
42
+ margin?: ChartMargin;
43
+ legend?: ChartLegendItem[];
44
+ hiddenIds?: Set<string>;
45
+ onToggleSeries?: (id: string) => void;
46
+ hover?: ChartHover;
47
+ "aria-label"?: string;
48
+ children: (plot: { width: number; height: number; m: Required<ChartMargin> }) => ReactNode;
49
+ };
50
+
51
+ export function resolveMargin(margin?: ChartMargin): Required<ChartMargin> {
52
+ return {
53
+ top: margin?.top ?? DEFAULT_CHART_MARGIN.top,
54
+ right: margin?.right ?? DEFAULT_CHART_MARGIN.right,
55
+ bottom: margin?.bottom ?? DEFAULT_CHART_MARGIN.bottom,
56
+ left: margin?.left ?? DEFAULT_CHART_MARGIN.left,
57
+ };
58
+ }
59
+
60
+ function useElementSize(
61
+ ref: RefObject<HTMLElement | null>,
62
+ fallbackWidth: number,
63
+ fallbackHeight: number,
64
+ ) {
65
+ const [size, setSize] = useState({ width: fallbackWidth, height: fallbackHeight });
66
+
67
+ useEffect(() => {
68
+ const el = ref.current;
69
+ if (!el) return;
70
+ const apply = () => {
71
+ setSize({
72
+ width: el.clientWidth || fallbackWidth,
73
+ height: el.clientHeight || fallbackHeight,
74
+ });
75
+ };
76
+ apply();
77
+ if (typeof ResizeObserver === "undefined") return;
78
+ const observer = new ResizeObserver(apply);
79
+ observer.observe(el);
80
+ return () => observer.disconnect();
81
+ }, [fallbackWidth, fallbackHeight, ref]);
82
+
83
+ return size;
84
+ }
85
+
86
+ export function ChartFrame({
87
+ slot,
88
+ className,
89
+ width,
90
+ height = 280,
91
+ margin,
92
+ legend,
93
+ hiddenIds,
94
+ onToggleSeries,
95
+ hover,
96
+ "aria-label": ariaLabel,
97
+ children,
98
+ }: ChartFrameProps) {
99
+ const surfaceRef = useRef<HTMLDivElement>(null);
100
+ const measured = useElementSize(surfaceRef, width ?? 320, height);
101
+ const plotWidth = width ?? measured.width;
102
+ const plotHeight = height;
103
+ const m = resolveMargin(margin);
104
+
105
+ return (
106
+ <div
107
+ data-slot={slot}
108
+ className={cn("spk-chart", className)}
109
+ role="img"
110
+ aria-label={ariaLabel}
111
+ >
112
+ {legend?.length ? (
113
+ <ChartLegend items={legend} hiddenIds={hiddenIds} onToggle={onToggleSeries} />
114
+ ) : null}
115
+ <div
116
+ ref={surfaceRef}
117
+ className="spk-chart-surface"
118
+ style={{ height: plotHeight }}
119
+ >
120
+ {plotWidth > 0 ? children({ width: plotWidth, height: plotHeight, m }) : null}
121
+ <ChartTooltip hover={hover ?? null} boundsWidth={plotWidth} />
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ export function ChartLegend({
128
+ items,
129
+ hiddenIds,
130
+ onToggle,
131
+ }: {
132
+ items: ChartLegendItem[];
133
+ hiddenIds?: Set<string>;
134
+ onToggle?: (id: string) => void;
135
+ }) {
136
+ return (
137
+ <div className="spk-chart-legend">
138
+ {items.map((item) => (
139
+ <button
140
+ key={item.id}
141
+ type="button"
142
+ className="spk-chart-legend-item"
143
+ data-hidden={hiddenIds?.has(item.id) ? "true" : undefined}
144
+ onClick={() => onToggle?.(item.id)}
145
+ >
146
+ <span className="spk-chart-swatch" style={{ background: item.color }} />
147
+ {item.label}
148
+ </button>
149
+ ))}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ export function ChartTooltip({
155
+ hover,
156
+ boundsWidth,
157
+ }: {
158
+ hover: ChartHover;
159
+ boundsWidth: number;
160
+ }) {
161
+ if (!hover || !hover.items.length) return null;
162
+ const left = clampTooltip(hover.x, boundsWidth);
163
+ return (
164
+ <div className="spk-chart-tooltip" style={{ left, top: hover.y }} role="tooltip">
165
+ {hover.title ? <p className="spk-chart-tooltip-title">{hover.title}</p> : null}
166
+ <ul>
167
+ {hover.items.map((item) => (
168
+ <li key={item.label}>
169
+ <span className="spk-chart-swatch" style={{ background: item.color }} />
170
+ <span>{item.label}</span>
171
+ <strong>{item.value}</strong>
172
+ </li>
173
+ ))}
174
+ </ul>
175
+ </div>
176
+ );
177
+ }
178
+
179
+ function clampTooltip(x: number, width: number) {
180
+ return clampNum(x, 72, Math.max(72, width - 72));
181
+ }
182
+
183
+ function clampNum(value: number, min: number, max: number) {
184
+ return Math.min(max, Math.max(min, value));
185
+ }
186
+
187
+ export function CartesianGrid({
188
+ ticks,
189
+ scale,
190
+ left,
191
+ right,
192
+ top,
193
+ bottom,
194
+ }: {
195
+ ticks: number[];
196
+ scale: (value: number) => number;
197
+ left: number;
198
+ right: number;
199
+ top: number;
200
+ bottom: number;
201
+ }) {
202
+ return (
203
+ <g data-slot="chart-grid">
204
+ {ticks.map((tick) => (
205
+ <line
206
+ key={tick}
207
+ className="spk-chart-grid"
208
+ x1={left}
209
+ x2={right}
210
+ y1={scale(tick)}
211
+ y2={scale(tick)}
212
+ />
213
+ ))}
214
+ <line className="spk-chart-axis" x1={left} x2={right} y1={bottom} y2={bottom} />
215
+ <line className="spk-chart-axis" x1={left} x2={left} y1={top} y2={bottom} />
216
+ </g>
217
+ );
218
+ }
219
+
220
+ export function ValueAxis({
221
+ ticks,
222
+ scale,
223
+ left,
224
+ width,
225
+ }: {
226
+ ticks: number[];
227
+ scale: (value: number) => number;
228
+ left: number;
229
+ width?: number;
230
+ }) {
231
+ return (
232
+ <g data-slot="chart-y-axis">
233
+ {ticks.map((tick) => (
234
+ <text
235
+ key={tick}
236
+ className="spk-chart-tick"
237
+ x={left - 8}
238
+ y={scale(tick)}
239
+ textAnchor="end"
240
+ dominantBaseline="middle"
241
+ >
242
+ {formatChartNumber(tick)}
243
+ </text>
244
+ ))}
245
+ {width ? (
246
+ <text
247
+ className="spk-chart-axis-label"
248
+ x={12}
249
+ y={width / 2}
250
+ transform={`rotate(-90 12 ${width / 2})`}
251
+ textAnchor="middle"
252
+ />
253
+ ) : null}
254
+ </g>
255
+ );
256
+ }
257
+
258
+ export function CategoryAxis({
259
+ labels,
260
+ position,
261
+ y,
262
+ }: {
263
+ labels: Array<string | number | Date>;
264
+ position: (index: number) => number;
265
+ y: number;
266
+ }) {
267
+ const maxLabels = labels.length > 12 ? Math.ceil(labels.length / 8) : 1;
268
+ return (
269
+ <g data-slot="chart-x-axis">
270
+ {labels.map((label, index) => {
271
+ if (index % maxLabels !== 0) return null;
272
+ return (
273
+ <text
274
+ key={`${index}-${String(label)}`}
275
+ className="spk-chart-tick"
276
+ x={position(index)}
277
+ y={y + 16}
278
+ textAnchor="middle"
279
+ >
280
+ {formatAxisValue(label)}
281
+ </text>
282
+ );
283
+ })}
284
+ </g>
285
+ );
286
+ }
287
+
288
+ export function useHiddenSeries(ids: string[]) {
289
+ const [hidden, setHidden] = useState<Set<string>>(new Set());
290
+ const toggle = (id: string) => {
291
+ setHidden((current) => {
292
+ const next = new Set(current);
293
+ if (next.has(id)) next.delete(id);
294
+ else next.add(id);
295
+ return next;
296
+ });
297
+ };
298
+ const visible = ids.filter((id) => !hidden.has(id));
299
+ return { hidden, toggle, visible };
300
+ }
301
+
302
+ export function seriesMeta(
303
+ series: Array<{ id?: string; label?: string; color?: string }>,
304
+ colors?: string[],
305
+ ): ChartLegendItem[] {
306
+ return series.map((item, index) => ({
307
+ id: item.id ?? item.label ?? `series-${index}`,
308
+ label: item.label ?? item.id ?? `Series ${index + 1}`,
309
+ color: item.color ?? chartColor(index, colors),
310
+ }));
311
+ }
312
+
313
+ export function useChartHover() {
314
+ const [hover, setHover] = useState<ChartHover>(null);
315
+ return { hover, setHover, clear: () => setHover(null) };
316
+ }
317
+
318
+ export function categoryLabels(axis?: ChartAxisConfig, length = 0): Array<string | number | Date> {
319
+ if (axis?.data?.length) return axis.data;
320
+ return Array.from({ length }, (_, i) => i + 1);
321
+ }
322
+
323
+ export function formatSeriesValue(value: number | [number, number] | undefined) {
324
+ if (value == null) return "—";
325
+ if (Array.isArray(value)) return `${formatChartNumber(value[0])} – ${formatChartNumber(value[1])}`;
326
+ return formatChartNumber(value);
327
+ }
328
+
@@ -0,0 +1,25 @@
1
+ import { render } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { BarChart3D, PieChart3D } from "./charts-3d";
4
+
5
+ describe("charts-3d", () => {
6
+ it("renders isometric bars and an extruded pie", () => {
7
+ const { container } = render(
8
+ <div>
9
+ <BarChart3D
10
+ width={240}
11
+ height={160}
12
+ xAxis={[{ data: ["A", "B"] }]}
13
+ series={[{ label: "Views", data: [3, 5] }]}
14
+ />
15
+ <PieChart3D
16
+ width={200}
17
+ height={160}
18
+ series={[{ data: [{ value: 2, label: "A" }, { value: 3, label: "B" }] }]}
19
+ />
20
+ </div>,
21
+ );
22
+ expect(container.querySelector('[data-slot="bar-chart-3d"]')).toBeTruthy();
23
+ expect(container.querySelector('[data-slot="pie-chart-3d"]')).toBeTruthy();
24
+ });
25
+ });
@@ -0,0 +1,318 @@
1
+ import {
2
+ extent,
3
+ formatChartNumber,
4
+ niceTicks,
5
+ pieSlices,
6
+ type ChartAxisConfig,
7
+ type ChartMargin,
8
+ } from "../lib/charts";
9
+ import { ellipseSlicePath, ellipseWallPath, isoBoxFaces, isoPoint } from "../lib/iso";
10
+ import {
11
+ CartesianGrid,
12
+ ChartFrame,
13
+ categoryLabels,
14
+ formatSeriesValue,
15
+ seriesMeta,
16
+ useChartHover,
17
+ useHiddenSeries,
18
+ } from "./chart-ui";
19
+ import type { NumericSeries } from "./cartesian-charts";
20
+
21
+ function box(width: number, height: number, m: Required<ChartMargin>) {
22
+ return {
23
+ left: m.left,
24
+ right: width - m.right,
25
+ top: m.top,
26
+ bottom: height - m.bottom,
27
+ iw: Math.max(1, width - m.left - m.right),
28
+ ih: Math.max(1, height - m.top - m.bottom),
29
+ };
30
+ }
31
+
32
+ export type BarChart3DProps = {
33
+ series: NumericSeries[];
34
+ xAxis?: ChartAxisConfig[];
35
+ yAxis?: ChartAxisConfig[];
36
+ height?: number;
37
+ width?: number;
38
+ colors?: string[];
39
+ hideLegend?: boolean;
40
+ hideGrid?: boolean;
41
+ margin?: ChartMargin;
42
+ className?: string;
43
+ depth?: number;
44
+ };
45
+
46
+ export function BarChart3D({
47
+ series,
48
+ xAxis,
49
+ yAxis,
50
+ height = 280,
51
+ width,
52
+ colors,
53
+ hideLegend,
54
+ hideGrid,
55
+ margin,
56
+ className,
57
+ depth = 14,
58
+ }: BarChart3DProps) {
59
+ const meta = seriesMeta(series, colors);
60
+ const ids = meta.map((item) => item.id);
61
+ const { hidden, toggle } = useHiddenSeries(ids);
62
+ const { hover, setHover, clear } = useChartHover();
63
+ const categories = categoryLabels(xAxis?.[0], series[0]?.data?.length ?? 0);
64
+ const values = series.flatMap((item) => (item.data ?? []).map((v) => v ?? 0));
65
+ const domain: [number, number] = [
66
+ yAxis?.[0]?.min ?? Math.min(0, extent(values, 0.08)[0]),
67
+ yAxis?.[0]?.max ?? extent(values, 0.08)[1],
68
+ ];
69
+ const ticks = niceTicks(domain[0], domain[1], 4);
70
+
71
+ return (
72
+ <ChartFrame
73
+ slot="bar-chart-3d"
74
+ className={className}
75
+ width={width}
76
+ height={height}
77
+ margin={margin ?? { top: 24, right: 28, bottom: 40, left: 48 }}
78
+ legend={hideLegend ? undefined : meta}
79
+ hiddenIds={hidden}
80
+ onToggleSeries={toggle}
81
+ hover={hover}
82
+ aria-label="3D bar chart"
83
+ >
84
+ {({ width: w, height: h, m }) => {
85
+ const plot = box(w, h, m);
86
+ const originX = plot.left + plot.iw * 0.16;
87
+ const originY = plot.bottom - 6;
88
+ const visible = series
89
+ .map((item, s) => ({ item, info: meta[s]!, s }))
90
+ .filter(({ info }) => !hidden.has(info.id));
91
+ const n = Math.max(categories.length, 1);
92
+ const groupW = Math.max(18, (plot.iw * 0.72) / n);
93
+ const barW = Math.max(8, (groupW * 0.78) / Math.max(visible.length, 1));
94
+ const gap = Math.max(2, (groupW - barW * visible.length) / Math.max(visible.length + 1, 1));
95
+ const maxH = plot.ih * 0.78;
96
+ const yScale = (value: number) =>
97
+ ((value - domain[0]) / (domain[1] - domain[0] || 1)) * maxH;
98
+ const yAt = (value: number) => originY - yScale(value);
99
+
100
+ return (
101
+ <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
102
+ {hideGrid ? null : (
103
+ <CartesianGrid
104
+ ticks={ticks}
105
+ scale={yAt}
106
+ left={plot.left}
107
+ right={plot.right}
108
+ top={plot.top}
109
+ bottom={plot.bottom}
110
+ />
111
+ )}
112
+ {ticks.map((tick) => (
113
+ <text key={tick} className="spk-chart-tick" x={plot.left - 8} y={yAt(tick)} textAnchor="end">
114
+ {formatChartNumber(tick)}
115
+ </text>
116
+ ))}
117
+ {categories.map((label, i) => {
118
+ const x0 = i * groupW;
119
+ const foot = isoPoint(x0 + groupW / 2, 0, depth, originX, originY);
120
+ return (
121
+ <text
122
+ key={String(label)}
123
+ className="spk-chart-tick"
124
+ x={foot.x}
125
+ y={plot.bottom + 16}
126
+ textAnchor="middle"
127
+ >
128
+ {String(label)}
129
+ </text>
130
+ );
131
+ })}
132
+ {categories.flatMap((_, i) =>
133
+ visible.map(({ item, info }, s) => {
134
+ const value = item.data?.[i] ?? 0;
135
+ const barH = Math.max(2, yScale(value));
136
+ const x = i * groupW + gap + s * (barW + gap * 0.2);
137
+ const faces = isoBoxFaces(x, 0, 0, barW, barH, depth, originX, originY);
138
+ const color = info.color;
139
+ return (
140
+ <g
141
+ key={`${info.id}-${i}`}
142
+ className="spk-chart-mark"
143
+ onMouseEnter={(event) =>
144
+ setHover({
145
+ x: event.nativeEvent.offsetX,
146
+ y: event.nativeEvent.offsetY,
147
+ title: String(categories[i]),
148
+ items: [
149
+ {
150
+ color,
151
+ label: info.label,
152
+ value: formatSeriesValue(value),
153
+ },
154
+ ],
155
+ })
156
+ }
157
+ onMouseLeave={clear}
158
+ >
159
+ <path d={faces.side} fill={`color-mix(in srgb, ${color} 78%, black)`} />
160
+ <path d={faces.top} fill={`color-mix(in srgb, ${color} 72%, white)`} />
161
+ <path d={faces.front} fill={color} />
162
+ </g>
163
+ );
164
+ }),
165
+ )}
166
+ </svg>
167
+ );
168
+ }}
169
+ </ChartFrame>
170
+ );
171
+ }
172
+
173
+ export type PieChart3DProps = {
174
+ series: Array<{
175
+ data: Array<{ id?: string | number; value: number; label?: string; color?: string }>;
176
+ innerRadius?: number;
177
+ paddingAngle?: number;
178
+ }>;
179
+ height?: number;
180
+ width?: number;
181
+ colors?: string[];
182
+ hideLegend?: boolean;
183
+ margin?: ChartMargin;
184
+ className?: string;
185
+ thickness?: number;
186
+ };
187
+
188
+ export function PieChart3D({
189
+ series,
190
+ height = 280,
191
+ width,
192
+ colors,
193
+ hideLegend,
194
+ margin,
195
+ className,
196
+ thickness = 18,
197
+ }: PieChart3DProps) {
198
+ const first = series[0] ?? { data: [] };
199
+ const meta = seriesMeta(
200
+ first.data.map((item, i) => ({
201
+ id: String(item.id ?? item.label ?? i),
202
+ label: item.label ?? `Slice ${i + 1}`,
203
+ color: item.color,
204
+ })),
205
+ colors,
206
+ );
207
+ const ids = meta.map((item) => item.id);
208
+ const { hidden, toggle } = useHiddenSeries(ids);
209
+ const { hover, setHover, clear } = useChartHover();
210
+
211
+ return (
212
+ <ChartFrame
213
+ slot="pie-chart-3d"
214
+ className={className}
215
+ width={width}
216
+ height={height}
217
+ margin={margin ?? { top: 12, right: 12, bottom: 20, left: 12 }}
218
+ legend={hideLegend ? undefined : meta}
219
+ hiddenIds={hidden}
220
+ onToggleSeries={toggle}
221
+ hover={hover}
222
+ aria-label="3D pie chart"
223
+ >
224
+ {({ width: w, height: h, m }) => {
225
+ const cx = w / 2;
226
+ const cy = (h - thickness) / 2;
227
+ const rx = Math.min(w - m.left - m.right, (h - m.top - m.bottom - thickness) * 1.7) / 2;
228
+ const ry = rx * 0.42;
229
+ const visible = first.data.map((item, i) => (hidden.has(meta[i]!.id) ? 0 : item.value));
230
+ const slices = pieSlices(visible, {
231
+ cx,
232
+ cy,
233
+ innerRadius: 0,
234
+ outerRadius: rx,
235
+ paddingAngle: first.paddingAngle,
236
+ });
237
+ const ordered = slices
238
+ .map((slice, i) => ({ slice, info: meta[i]!, item: first.data[i]! }))
239
+ .filter(({ info, slice }) => !hidden.has(info.id) && slice.path)
240
+ .sort((a, b) => ellipsePointY(cx, cy, rx, ry, a.slice.midAngle) - ellipsePointY(cx, cy, rx, ry, b.slice.midAngle));
241
+
242
+ return (
243
+ <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
244
+ {ordered.map(({ slice, info, item }) => (
245
+ <g
246
+ key={`${info.id}-wall`}
247
+ className="spk-chart-mark"
248
+ onMouseEnter={(event) =>
249
+ setHover({
250
+ x: event.nativeEvent.offsetX,
251
+ y: event.nativeEvent.offsetY,
252
+ title: info.label,
253
+ items: [
254
+ {
255
+ color: info.color,
256
+ label: info.label,
257
+ value: `${formatChartNumber(item.value)} (${Math.round(slice.percent * 100)}%)`,
258
+ },
259
+ ],
260
+ })
261
+ }
262
+ onMouseLeave={clear}
263
+ >
264
+ <path
265
+ d={ellipseWallPath(cx, cy, rx, ry, slice.startAngle, slice.endAngle, thickness)}
266
+ fill={`color-mix(in srgb, ${info.color} 70%, black)`}
267
+ />
268
+ </g>
269
+ ))}
270
+ {ordered.map(({ slice, info, item }) => (
271
+ <g
272
+ key={info.id}
273
+ className="spk-chart-mark"
274
+ onMouseEnter={(event) =>
275
+ setHover({
276
+ x: event.nativeEvent.offsetX,
277
+ y: event.nativeEvent.offsetY,
278
+ title: info.label,
279
+ items: [
280
+ {
281
+ color: info.color,
282
+ label: info.label,
283
+ value: `${formatChartNumber(item.value)} (${Math.round(slice.percent * 100)}%)`,
284
+ },
285
+ ],
286
+ })
287
+ }
288
+ onMouseLeave={clear}
289
+ >
290
+ <path
291
+ d={ellipseSlicePath(cx, cy, rx, ry, slice.startAngle, slice.endAngle)}
292
+ fill={info.color}
293
+ />
294
+ {slice.percent > 0.08 ? (
295
+ <text
296
+ className="spk-chart-label"
297
+ x={cx + Math.cos(((slice.midAngle - 90) * Math.PI) / 180) * rx * 0.55}
298
+ y={cy + Math.sin(((slice.midAngle - 90) * Math.PI) / 180) * ry * 0.55}
299
+ textAnchor="middle"
300
+ dominantBaseline="middle"
301
+ fill="var(--primary-foreground)"
302
+ >
303
+ {Math.round(slice.percent * 100)}%
304
+ </text>
305
+ ) : null}
306
+ </g>
307
+ ))}
308
+ </svg>
309
+ );
310
+ }}
311
+ </ChartFrame>
312
+ );
313
+ }
314
+
315
+ function ellipsePointY(cx: number, cy: number, rx: number, ry: number, angleDeg: number) {
316
+ const rad = ((angleDeg - 90) * Math.PI) / 180;
317
+ return cy + ry * Math.sin(rad);
318
+ }