@cdx-ui/components 0.3.5 → 0.3.6

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 (278) hide show
  1. package/README.md +1 -1
  2. package/coverage.svg +1 -1
  3. package/docs/components/bar-chart/content.md +703 -0
  4. package/docs/components/content.md +9 -1
  5. package/docs/components/line-chart/content.md +567 -0
  6. package/docs/components/pie-chart/content.md +493 -0
  7. package/docs/components/progress-segmented/content.md +1 -1
  8. package/docs/components/stepper/content.md +599 -0
  9. package/docs/components/system-alert/content.md +533 -0
  10. package/docs/foundations/styles/tokens/all-tokens/content.md +2 -1
  11. package/docs/llms.txt +9 -0
  12. package/docs/patterns/content.md +1 -0
  13. package/docs/patterns/dashboard/content.md +423 -19
  14. package/docs/patterns/insights/content.md +590 -0
  15. package/docs/resources/release-notes/0.3.5/content.md +7 -0
  16. package/docs/resources/release-notes/content.md +1 -0
  17. package/lib/commonjs/components/BarChart/geometry.js +1130 -0
  18. package/lib/commonjs/components/BarChart/geometry.js.map +1 -0
  19. package/lib/commonjs/components/BarChart/index.js +942 -0
  20. package/lib/commonjs/components/BarChart/index.js.map +1 -0
  21. package/lib/commonjs/components/BarChart/styles.js +26 -0
  22. package/lib/commonjs/components/BarChart/styles.js.map +1 -0
  23. package/lib/commonjs/components/Button/index.js +2 -2
  24. package/lib/commonjs/components/Button/index.js.map +1 -1
  25. package/lib/commonjs/components/LineChart/geometry.js +698 -0
  26. package/lib/commonjs/components/LineChart/geometry.js.map +1 -0
  27. package/lib/commonjs/components/LineChart/index.js +747 -0
  28. package/lib/commonjs/components/LineChart/index.js.map +1 -0
  29. package/lib/commonjs/components/LineChart/styles.js +25 -0
  30. package/lib/commonjs/components/LineChart/styles.js.map +1 -0
  31. package/lib/commonjs/components/PieChart/geometry.js +216 -0
  32. package/lib/commonjs/components/PieChart/geometry.js.map +1 -0
  33. package/lib/commonjs/components/PieChart/index.js +476 -0
  34. package/lib/commonjs/components/PieChart/index.js.map +1 -0
  35. package/lib/commonjs/components/PieChart/styles.js +88 -0
  36. package/lib/commonjs/components/PieChart/styles.js.map +1 -0
  37. package/lib/commonjs/components/ProgressCircle/ProgressCircleRing.js +9 -31
  38. package/lib/commonjs/components/ProgressCircle/ProgressCircleRing.js.map +1 -1
  39. package/lib/commonjs/components/ProgressCircle/ProgressRing.js +52 -0
  40. package/lib/commonjs/components/ProgressCircle/ProgressRing.js.map +1 -0
  41. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js +8 -7
  42. package/lib/commonjs/components/ScrollShadow/useScrollShadow.js.map +1 -1
  43. package/lib/commonjs/components/Stepper/index.js +369 -0
  44. package/lib/commonjs/components/Stepper/index.js.map +1 -0
  45. package/lib/commonjs/components/Stepper/styles.js +211 -0
  46. package/lib/commonjs/components/Stepper/styles.js.map +1 -0
  47. package/lib/commonjs/components/Stepper/useStepperLabelFit.js +22 -0
  48. package/lib/commonjs/components/Stepper/useStepperLabelFit.js.map +1 -0
  49. package/lib/commonjs/components/Stepper/useStepperLabelFit.web.js +52 -0
  50. package/lib/commonjs/components/Stepper/useStepperLabelFit.web.js.map +1 -0
  51. package/lib/commonjs/components/Stepper/useStepperRingColors.js +31 -0
  52. package/lib/commonjs/components/Stepper/useStepperRingColors.js.map +1 -0
  53. package/lib/commonjs/components/SystemAlert/animation.js +218 -0
  54. package/lib/commonjs/components/SystemAlert/animation.js.map +1 -0
  55. package/lib/commonjs/components/SystemAlert/index.js +525 -0
  56. package/lib/commonjs/components/SystemAlert/index.js.map +1 -0
  57. package/lib/commonjs/components/SystemAlert/styles.js +178 -0
  58. package/lib/commonjs/components/SystemAlert/styles.js.map +1 -0
  59. package/lib/commonjs/components/SystemAlert/useSystemAlertStatusBarStyle.js +53 -0
  60. package/lib/commonjs/components/SystemAlert/useSystemAlertStatusBarStyle.js.map +1 -0
  61. package/lib/commonjs/components/Tabs/index.js +4 -0
  62. package/lib/commonjs/components/Tabs/index.js.map +1 -1
  63. package/lib/commonjs/components/Tabs/styles.js +1 -1
  64. package/lib/commonjs/components/Tabs/styles.js.map +1 -1
  65. package/lib/commonjs/components/chart/ChartInspector.js +610 -0
  66. package/lib/commonjs/components/chart/ChartInspector.js.map +1 -0
  67. package/lib/commonjs/components/chart/format.js +93 -0
  68. package/lib/commonjs/components/chart/format.js.map +1 -0
  69. package/lib/commonjs/components/chart/index.js +2 -0
  70. package/lib/commonjs/components/chart/index.js.map +1 -0
  71. package/lib/commonjs/components/chart/motion.js +97 -0
  72. package/lib/commonjs/components/chart/motion.js.map +1 -0
  73. package/lib/commonjs/components/chart/palette.js +178 -0
  74. package/lib/commonjs/components/chart/palette.js.map +1 -0
  75. package/lib/commonjs/components/chart/size.js +34 -0
  76. package/lib/commonjs/components/chart/size.js.map +1 -0
  77. package/lib/commonjs/components/chart/ticks.js +59 -0
  78. package/lib/commonjs/components/chart/ticks.js.map +1 -0
  79. package/lib/commonjs/components/chart/types.js +6 -0
  80. package/lib/commonjs/components/chart/types.js.map +1 -0
  81. package/lib/commonjs/components/chart/useChartHostWidth.js +27 -0
  82. package/lib/commonjs/components/chart/useChartHostWidth.js.map +1 -0
  83. package/lib/commonjs/components/index.js +60 -0
  84. package/lib/commonjs/components/index.js.map +1 -1
  85. package/lib/commonjs/figma/SystemAlert.Button.figma.js +28 -0
  86. package/lib/commonjs/figma/SystemAlert.Button.figma.js.map +1 -0
  87. package/lib/commonjs/figma/SystemAlert.Close.figma.js +22 -0
  88. package/lib/commonjs/figma/SystemAlert.Close.figma.js.map +1 -0
  89. package/lib/commonjs/figma/SystemAlert.Description.figma.js +20 -0
  90. package/lib/commonjs/figma/SystemAlert.Description.figma.js.map +1 -0
  91. package/lib/commonjs/figma/SystemAlert.IconButton.figma.js +31 -0
  92. package/lib/commonjs/figma/SystemAlert.IconButton.figma.js.map +1 -0
  93. package/lib/commonjs/figma/SystemAlert.figma.js +48 -0
  94. package/lib/commonjs/figma/SystemAlert.figma.js.map +1 -0
  95. package/lib/module/components/BarChart/geometry.js +1110 -0
  96. package/lib/module/components/BarChart/geometry.js.map +1 -0
  97. package/lib/module/components/BarChart/index.js +936 -0
  98. package/lib/module/components/BarChart/index.js.map +1 -0
  99. package/lib/module/components/BarChart/styles.js +22 -0
  100. package/lib/module/components/BarChart/styles.js.map +1 -0
  101. package/lib/module/components/Button/index.js +2 -2
  102. package/lib/module/components/Button/index.js.map +1 -1
  103. package/lib/module/components/LineChart/geometry.js +685 -0
  104. package/lib/module/components/LineChart/geometry.js.map +1 -0
  105. package/lib/module/components/LineChart/index.js +741 -0
  106. package/lib/module/components/LineChart/index.js.map +1 -0
  107. package/lib/module/components/LineChart/styles.js +21 -0
  108. package/lib/module/components/LineChart/styles.js.map +1 -0
  109. package/lib/module/components/PieChart/geometry.js +209 -0
  110. package/lib/module/components/PieChart/geometry.js.map +1 -0
  111. package/lib/module/components/PieChart/index.js +471 -0
  112. package/lib/module/components/PieChart/index.js.map +1 -0
  113. package/lib/module/components/PieChart/styles.js +84 -0
  114. package/lib/module/components/PieChart/styles.js.map +1 -0
  115. package/lib/module/components/ProgressCircle/ProgressCircleRing.js +10 -32
  116. package/lib/module/components/ProgressCircle/ProgressCircleRing.js.map +1 -1
  117. package/lib/module/components/ProgressCircle/ProgressRing.js +48 -0
  118. package/lib/module/components/ProgressCircle/ProgressRing.js.map +1 -0
  119. package/lib/module/components/ScrollShadow/useScrollShadow.js +8 -7
  120. package/lib/module/components/ScrollShadow/useScrollShadow.js.map +1 -1
  121. package/lib/module/components/Stepper/index.js +360 -0
  122. package/lib/module/components/Stepper/index.js.map +1 -0
  123. package/lib/module/components/Stepper/styles.js +206 -0
  124. package/lib/module/components/Stepper/styles.js.map +1 -0
  125. package/lib/module/components/Stepper/useStepperLabelFit.js +18 -0
  126. package/lib/module/components/Stepper/useStepperLabelFit.js.map +1 -0
  127. package/lib/module/components/Stepper/useStepperLabelFit.web.js +48 -0
  128. package/lib/module/components/Stepper/useStepperLabelFit.web.js.map +1 -0
  129. package/lib/module/components/Stepper/useStepperRingColors.js +28 -0
  130. package/lib/module/components/Stepper/useStepperRingColors.js.map +1 -0
  131. package/lib/module/components/SystemAlert/animation.js +213 -0
  132. package/lib/module/components/SystemAlert/animation.js.map +1 -0
  133. package/lib/module/components/SystemAlert/index.js +515 -0
  134. package/lib/module/components/SystemAlert/index.js.map +1 -0
  135. package/lib/module/components/SystemAlert/styles.js +174 -0
  136. package/lib/module/components/SystemAlert/styles.js.map +1 -0
  137. package/lib/module/components/SystemAlert/useSystemAlertStatusBarStyle.js +49 -0
  138. package/lib/module/components/SystemAlert/useSystemAlertStatusBarStyle.js.map +1 -0
  139. package/lib/module/components/Tabs/index.js +4 -0
  140. package/lib/module/components/Tabs/index.js.map +1 -1
  141. package/lib/module/components/Tabs/styles.js +1 -1
  142. package/lib/module/components/Tabs/styles.js.map +1 -1
  143. package/lib/module/components/chart/ChartInspector.js +600 -0
  144. package/lib/module/components/chart/ChartInspector.js.map +1 -0
  145. package/lib/module/components/chart/format.js +84 -0
  146. package/lib/module/components/chart/format.js.map +1 -0
  147. package/lib/module/components/chart/index.js +2 -0
  148. package/lib/module/components/chart/index.js.map +1 -0
  149. package/lib/module/components/chart/motion.js +89 -0
  150. package/lib/module/components/chart/motion.js.map +1 -0
  151. package/lib/module/components/chart/palette.js +168 -0
  152. package/lib/module/components/chart/palette.js.map +1 -0
  153. package/lib/module/components/chart/size.js +29 -0
  154. package/lib/module/components/chart/size.js.map +1 -0
  155. package/lib/module/components/chart/ticks.js +55 -0
  156. package/lib/module/components/chart/ticks.js.map +1 -0
  157. package/lib/module/components/chart/types.js +4 -0
  158. package/lib/module/components/chart/types.js.map +1 -0
  159. package/lib/module/components/chart/useChartHostWidth.js +23 -0
  160. package/lib/module/components/chart/useChartHostWidth.js.map +1 -0
  161. package/lib/module/components/index.js +5 -0
  162. package/lib/module/components/index.js.map +1 -1
  163. package/lib/module/figma/SystemAlert.Button.figma.js +22 -0
  164. package/lib/module/figma/SystemAlert.Button.figma.js.map +1 -0
  165. package/lib/module/figma/SystemAlert.Close.figma.js +18 -0
  166. package/lib/module/figma/SystemAlert.Close.figma.js.map +1 -0
  167. package/lib/module/figma/SystemAlert.Description.figma.js +14 -0
  168. package/lib/module/figma/SystemAlert.Description.figma.js.map +1 -0
  169. package/lib/module/figma/SystemAlert.IconButton.figma.js +25 -0
  170. package/lib/module/figma/SystemAlert.IconButton.figma.js.map +1 -0
  171. package/lib/module/figma/SystemAlert.figma.js +42 -0
  172. package/lib/module/figma/SystemAlert.figma.js.map +1 -0
  173. package/lib/typescript/components/BarChart/geometry.d.ts +236 -0
  174. package/lib/typescript/components/BarChart/geometry.d.ts.map +1 -0
  175. package/lib/typescript/components/BarChart/index.d.ts +193 -0
  176. package/lib/typescript/components/BarChart/index.d.ts.map +1 -0
  177. package/lib/typescript/components/BarChart/styles.d.ts +32 -0
  178. package/lib/typescript/components/BarChart/styles.d.ts.map +1 -0
  179. package/lib/typescript/components/LineChart/geometry.d.ts +248 -0
  180. package/lib/typescript/components/LineChart/geometry.d.ts.map +1 -0
  181. package/lib/typescript/components/LineChart/index.d.ts +105 -0
  182. package/lib/typescript/components/LineChart/index.d.ts.map +1 -0
  183. package/lib/typescript/components/LineChart/styles.d.ts +12 -0
  184. package/lib/typescript/components/LineChart/styles.d.ts.map +1 -0
  185. package/lib/typescript/components/PieChart/geometry.d.ts +43 -0
  186. package/lib/typescript/components/PieChart/geometry.d.ts.map +1 -0
  187. package/lib/typescript/components/PieChart/index.d.ts +147 -0
  188. package/lib/typescript/components/PieChart/index.d.ts.map +1 -0
  189. package/lib/typescript/components/PieChart/styles.d.ts +27 -0
  190. package/lib/typescript/components/PieChart/styles.d.ts.map +1 -0
  191. package/lib/typescript/components/ProgressCircle/ProgressCircleRing.d.ts.map +1 -1
  192. package/lib/typescript/components/ProgressCircle/ProgressRing.d.ts +17 -0
  193. package/lib/typescript/components/ProgressCircle/ProgressRing.d.ts.map +1 -0
  194. package/lib/typescript/components/ScrollShadow/useScrollShadow.d.ts.map +1 -1
  195. package/lib/typescript/components/Stepper/index.d.ts +68 -0
  196. package/lib/typescript/components/Stepper/index.d.ts.map +1 -0
  197. package/lib/typescript/components/Stepper/styles.d.ts +46 -0
  198. package/lib/typescript/components/Stepper/styles.d.ts.map +1 -0
  199. package/lib/typescript/components/Stepper/useStepperLabelFit.d.ts +15 -0
  200. package/lib/typescript/components/Stepper/useStepperLabelFit.d.ts.map +1 -0
  201. package/lib/typescript/components/Stepper/useStepperLabelFit.web.d.ts +10 -0
  202. package/lib/typescript/components/Stepper/useStepperLabelFit.web.d.ts.map +1 -0
  203. package/lib/typescript/components/Stepper/useStepperRingColors.d.ts +6 -0
  204. package/lib/typescript/components/Stepper/useStepperRingColors.d.ts.map +1 -0
  205. package/lib/typescript/components/SystemAlert/animation.d.ts +56 -0
  206. package/lib/typescript/components/SystemAlert/animation.d.ts.map +1 -0
  207. package/lib/typescript/components/SystemAlert/index.d.ts +110 -0
  208. package/lib/typescript/components/SystemAlert/index.d.ts.map +1 -0
  209. package/lib/typescript/components/SystemAlert/styles.d.ts +38 -0
  210. package/lib/typescript/components/SystemAlert/styles.d.ts.map +1 -0
  211. package/lib/typescript/components/SystemAlert/useSystemAlertStatusBarStyle.d.ts +14 -0
  212. package/lib/typescript/components/SystemAlert/useSystemAlertStatusBarStyle.d.ts.map +1 -0
  213. package/lib/typescript/components/Tabs/index.d.ts.map +1 -1
  214. package/lib/typescript/components/chart/ChartInspector.d.ts +101 -0
  215. package/lib/typescript/components/chart/ChartInspector.d.ts.map +1 -0
  216. package/lib/typescript/components/chart/format.d.ts +16 -0
  217. package/lib/typescript/components/chart/format.d.ts.map +1 -0
  218. package/lib/typescript/components/chart/index.d.ts +3 -0
  219. package/lib/typescript/components/chart/index.d.ts.map +1 -0
  220. package/lib/typescript/components/chart/motion.d.ts +27 -0
  221. package/lib/typescript/components/chart/motion.d.ts.map +1 -0
  222. package/lib/typescript/components/chart/palette.d.ts +44 -0
  223. package/lib/typescript/components/chart/palette.d.ts.map +1 -0
  224. package/lib/typescript/components/chart/size.d.ts +25 -0
  225. package/lib/typescript/components/chart/size.d.ts.map +1 -0
  226. package/lib/typescript/components/chart/ticks.d.ts +3 -0
  227. package/lib/typescript/components/chart/ticks.d.ts.map +1 -0
  228. package/lib/typescript/components/chart/types.d.ts +163 -0
  229. package/lib/typescript/components/chart/types.d.ts.map +1 -0
  230. package/lib/typescript/components/chart/useChartHostWidth.d.ts +9 -0
  231. package/lib/typescript/components/chart/useChartHostWidth.d.ts.map +1 -0
  232. package/lib/typescript/components/index.d.ts +5 -0
  233. package/lib/typescript/components/index.d.ts.map +1 -1
  234. package/lib/typescript/figma/SystemAlert.Button.figma.d.ts +11 -0
  235. package/lib/typescript/figma/SystemAlert.Button.figma.d.ts.map +1 -0
  236. package/lib/typescript/figma/SystemAlert.Close.figma.d.ts +11 -0
  237. package/lib/typescript/figma/SystemAlert.Close.figma.d.ts.map +1 -0
  238. package/lib/typescript/figma/SystemAlert.Description.figma.d.ts +8 -0
  239. package/lib/typescript/figma/SystemAlert.Description.figma.d.ts.map +1 -0
  240. package/lib/typescript/figma/SystemAlert.IconButton.figma.d.ts +11 -0
  241. package/lib/typescript/figma/SystemAlert.IconButton.figma.d.ts.map +1 -0
  242. package/lib/typescript/figma/SystemAlert.figma.d.ts +8 -0
  243. package/lib/typescript/figma/SystemAlert.figma.d.ts.map +1 -0
  244. package/package.json +5 -5
  245. package/src/components/BarChart/geometry.ts +1469 -0
  246. package/src/components/BarChart/index.tsx +1424 -0
  247. package/src/components/BarChart/styles.ts +31 -0
  248. package/src/components/Button/index.tsx +1 -1
  249. package/src/components/LineChart/geometry.ts +957 -0
  250. package/src/components/LineChart/index.tsx +1109 -0
  251. package/src/components/LineChart/styles.ts +26 -0
  252. package/src/components/PieChart/geometry.ts +295 -0
  253. package/src/components/PieChart/index.tsx +715 -0
  254. package/src/components/PieChart/styles.ts +101 -0
  255. package/src/components/ProgressCircle/ProgressCircleRing.tsx +10 -39
  256. package/src/components/ProgressCircle/ProgressRing.tsx +60 -0
  257. package/src/components/ScrollShadow/useScrollShadow.ts +12 -11
  258. package/src/components/Stepper/index.tsx +452 -0
  259. package/src/components/Stepper/styles.ts +234 -0
  260. package/src/components/Stepper/useStepperLabelFit.ts +21 -0
  261. package/src/components/Stepper/useStepperLabelFit.web.ts +53 -0
  262. package/src/components/Stepper/useStepperRingColors.ts +29 -0
  263. package/src/components/SystemAlert/animation.ts +252 -0
  264. package/src/components/SystemAlert/index.tsx +684 -0
  265. package/src/components/SystemAlert/styles.ts +212 -0
  266. package/src/components/SystemAlert/useSystemAlertStatusBarStyle.ts +57 -0
  267. package/src/components/Tabs/index.tsx +4 -0
  268. package/src/components/Tabs/styles.ts +1 -1
  269. package/src/components/chart/ChartInspector.tsx +726 -0
  270. package/src/components/chart/format.ts +95 -0
  271. package/src/components/chart/index.ts +24 -0
  272. package/src/components/chart/motion.ts +98 -0
  273. package/src/components/chart/palette.tsx +247 -0
  274. package/src/components/chart/size.ts +21 -0
  275. package/src/components/chart/ticks.ts +53 -0
  276. package/src/components/chart/types.ts +183 -0
  277. package/src/components/chart/useChartHostWidth.ts +26 -0
  278. package/src/components/index.ts +5 -0
@@ -0,0 +1,1424 @@
1
+ /* SVG `x`/`y` are paint coordinates. RN View `x`/`y` layout props are deprecated and share the name. */
2
+ /* eslint-disable @typescript-eslint/no-deprecated */
3
+ import {
4
+ forwardRef,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ForwardedRef,
10
+ type ReactElement,
11
+ type ReactNode,
12
+ } from 'react';
13
+ import {
14
+ ScrollView,
15
+ Text,
16
+ View,
17
+ type NativeScrollEvent,
18
+ type NativeSyntheticEvent,
19
+ type ViewProps,
20
+ } from 'react-native';
21
+ import Animated, { useAnimatedProps } from 'react-native-reanimated';
22
+ import { G, Line, Path, Svg, Text as SvgText } from 'react-native-svg';
23
+ import { dataAttributes, mergeDataAttributes } from '@cdx-ui/primitives';
24
+ import { cn, useControllableState } from '@cdx-ui/utils';
25
+ import type { AnimationRootDisableAll } from '../../animation';
26
+ import {
27
+ ChartInspector,
28
+ ChartTooltipLayer,
29
+ expandHitBox,
30
+ type ChartInspectorTarget,
31
+ } from '../chart/ChartInspector';
32
+ import {
33
+ defaultChartValueFormatter,
34
+ formatChartPercent,
35
+ resolveChartLocaleText,
36
+ resolveValueFormatter,
37
+ } from '../chart/format';
38
+ import type {
39
+ ChartAxis,
40
+ ChartGrid,
41
+ ChartLocaleText,
42
+ ChartReferenceLine,
43
+ ChartSelectedItem,
44
+ ChartSeriesBase,
45
+ ChartSizeProps,
46
+ ChartSummaryCategory,
47
+ ChartTooltipContent,
48
+ ChartTooltipProps,
49
+ ChartValueFormatter,
50
+ ChartValueLocation,
51
+ } from '../chart/types';
52
+ import { useChartHostWidth } from '../chart/useChartHostWidth';
53
+ import {
54
+ CHART_MOTION_DURATION,
55
+ useChartMorph,
56
+ useChartMotionEnabled,
57
+ lerpVector,
58
+ } from '../chart/motion';
59
+ import {
60
+ ChartLinearFills,
61
+ chartFillUrl,
62
+ chartSeriesColor,
63
+ resolveChartColor,
64
+ useChartChrome,
65
+ useChartColorPaints,
66
+ useChartGradientId,
67
+ useChartPalette,
68
+ useChartSimpleColor,
69
+ type ChartChrome,
70
+ type ChartPalette,
71
+ } from '../chart/palette';
72
+ import {
73
+ BAR_GAP_RATIO,
74
+ CATEGORY_GAP_RATIO,
75
+ barKeyboardNeighbor,
76
+ barPath,
77
+ barValueAnchor,
78
+ DEFAULT_STACK_GAP,
79
+ formatWaterfallBarValue,
80
+ isExpandAxis,
81
+ layoutBarChart,
82
+ overlayDelta,
83
+ readBarValue,
84
+ resolveBarChartContentSize,
85
+ resolveBarChartGutters,
86
+ resolveBarGroups,
87
+ stackedTotalAnchors,
88
+ waterfallValueAnchor,
89
+ type BarChartKind,
90
+ type BarChartLayout,
91
+ type BarChartOrientation,
92
+ type BarGeometrySeries,
93
+ type BarRect,
94
+ type BarStackOffset,
95
+ } from './geometry';
96
+ import {
97
+ BAR_RADIUS,
98
+ barChartLegendItemVariants,
99
+ barChartLegendLabelVariants,
100
+ barChartLegendVariants,
101
+ barChartRootVariants,
102
+ resolveBarChartBox,
103
+ type BarChartSize,
104
+ } from './styles';
105
+
106
+ const INACTIVE_OPACITY = 0.4;
107
+ const DASH = '6 4';
108
+ const SVG_OVERFLOW_VISIBLE = { overflow: 'visible' } as const;
109
+ const LABEL_FADE_DURATION = 220;
110
+
111
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
112
+
113
+ export type * from '../chart';
114
+ export type { BarChartSize, BarStackOffset };
115
+
116
+ /** Keys of `TRow` whose value is a boolean. */
117
+ export type BarChartBooleanKey<TRow> = {
118
+ [K in keyof TRow]-?: TRow[K] extends boolean | undefined ? K : never;
119
+ }[keyof TRow] &
120
+ string;
121
+
122
+ /** Running-total series. Each row is a change, except rows flagged as totals. */
123
+ export interface BarChartWaterfall<TRow> {
124
+ /**
125
+ * Row key whose `true` value marks a total bar, drawn from zero to the running total. A total
126
+ * row with a value on the first row sets the starting total; otherwise its value is ignored.
127
+ */
128
+ totalKey: BarChartBooleanKey<TRow>;
129
+ }
130
+
131
+ /** Draws this series as a thinner comparison bar beside another series' bar. */
132
+ export interface BarChartOverlay {
133
+ /** `id` of the series this one is compared against. */
134
+ seriesId: string;
135
+ }
136
+
137
+ export interface BarChartSeries<TRow> extends ChartSeriesBase<TRow> {
138
+ /** Series type. Reserved for mixed charts; bar is the only type today. */
139
+ type?: 'bar';
140
+ /**
141
+ * Stack group id. Series with the same `stack` draw as one bar per category; different ids
142
+ * draw side by side. Omit to draw the series on its own.
143
+ */
144
+ stack?: string;
145
+ /**
146
+ * How the stack group piles its values. Set it on any one series in the group.
147
+ * `expand` scales each category to 100%.
148
+ * @default 'diverging'
149
+ */
150
+ stackOffset?: BarStackOffset;
151
+ /**
152
+ * Draws the series as a waterfall (running total). A waterfall series draws alone: other
153
+ * series are not plotted.
154
+ */
155
+ waterfall?: BarChartWaterfall<TRow>;
156
+ /**
157
+ * Draws the series as a thin comparison bar beside the series it names (this month versus last
158
+ * month). The pair draws alone, and the named series is announced with the change.
159
+ */
160
+ overlay?: BarChartOverlay;
161
+ }
162
+
163
+ export interface BarChartProps<TRow extends object = object> extends ViewProps, ChartSizeProps {
164
+ /** One object per category. Series and axes read it through `dataKey`. */
165
+ dataset: readonly TRow[];
166
+ /** Bars to draw. Each reads one numeric column through `dataKey`. */
167
+ series: readonly BarChartSeries<TRow>[];
168
+ /**
169
+ * Horizontal axis. In `vertical` layout it is the category (band) axis: `dataKey` names the
170
+ * category column, `position` (`'bottom'`, `'top'`, `'none'`) places its labels, and
171
+ * `tickLabelInterval` picks which labels draw. In `horizontal` layout it is the value axis.
172
+ * Only the first axis is used.
173
+ */
174
+ xAxis?: readonly ChartAxis<TRow>[];
175
+ /**
176
+ * Vertical axis. In `vertical` layout it is the value axis (`min`, `max`, `tickNumber`,
177
+ * `valueFormatter`; `position: 'none'` hides its ticks). In `horizontal` layout it is the
178
+ * category axis (`position` `'left'`, `'right'`, or `'none'`). Only the first axis is used.
179
+ */
180
+ yAxis?: readonly ChartAxis<TRow>[];
181
+ /**
182
+ * `vertical` grows bars up from the baseline. `horizontal` grows them to the right.
183
+ * @default 'vertical'
184
+ */
185
+ layout?: BarChartOrientation;
186
+ /**
187
+ * Hides the series legend under the chart. Waterfall charts never show one.
188
+ * @default false
189
+ */
190
+ hideLegend?: boolean;
191
+ /**
192
+ * Grid lines along the value axis: `horizontal` in `vertical` layout, `vertical` in
193
+ * `horizontal` layout. The other direction is not drawn. Omit to draw the value-axis lines;
194
+ * pass an object without that key set to `true` to hide them and keep only the baseline.
195
+ * @default { horizontal: true } (vertical layout), { vertical: true } (horizontal layout)
196
+ */
197
+ grid?: ChartGrid;
198
+ /**
199
+ * Default series colors. `auto` uses the FI brand family (`simple-primary`, `-secondary`,
200
+ * `-tertiary`) for up to three series and switches the whole chart to `categorical-1`…`6`
201
+ * past that. `categorical` always uses the categorical slots. A series `color` wins.
202
+ * @default 'auto'
203
+ */
204
+ palette?: ChartPalette;
205
+ /**
206
+ * Dashed guides at a value: horizontal on a vertical chart, vertical on a horizontal chart.
207
+ */
208
+ referenceLines?: readonly ChartReferenceLine[];
209
+ /**
210
+ * Keeps this `dataset` row full color and mutes the rest. Use with `barLabel` to label only
211
+ * that bar, like "this week" in a spending insight.
212
+ */
213
+ emphasizedIndex?: number;
214
+ /**
215
+ * Draws formatted values at the end of each bar (each stack's total when stacked).
216
+ * @default false
217
+ */
218
+ barLabel?: boolean;
219
+ /**
220
+ * Corner radius in pixels. `0` draws square bars.
221
+ * @default 6
222
+ */
223
+ borderRadius?: number;
224
+ /**
225
+ * Pixel gap between stacked segments. With a gap every segment is rounded; `0` joins them and
226
+ * rounds only the outer cap.
227
+ * @default 3
228
+ */
229
+ stackGap?: number;
230
+ /**
231
+ * Share of each category slot left empty around its bars, from `0` to `0.95`. Higher is
232
+ * thinner bars with more space between categories.
233
+ * @default 0.36
234
+ */
235
+ categoryGapRatio?: number;
236
+ /**
237
+ * Gap between side-by-side bars in one category, as a share of one bar's thickness.
238
+ * @default 0.5
239
+ */
240
+ barGapRatio?: number;
241
+ /**
242
+ * When the categories need more room than the visible box, scroll instead of
243
+ * squeezing bars. Vertical charts scroll sideways; horizontal charts scroll vertically.
244
+ * The value axis stays pinned. Turn off to pack every category into `width` / `height`.
245
+ * @default true
246
+ */
247
+ scrollable?: boolean;
248
+ /**
249
+ * How many categories fit in the viewport when `scrollable` is on. Omit to use a
250
+ * readable minimum band (~48px).
251
+ */
252
+ visiblePoints?: number;
253
+ /**
254
+ * Controlled selected bar: `seriesId` and the `dataset` row. `null` clears the selection while
255
+ * staying controlled.
256
+ */
257
+ selectedItem?: ChartSelectedItem;
258
+ /**
259
+ * Initial selected bar when `selectedItem` is omitted.
260
+ */
261
+ defaultSelectedItem?: ChartSelectedItem;
262
+ /**
263
+ * Called when a bar is pressed, with that bar, or with `null` when the selected bar is pressed
264
+ * again. Providing this, `selectedItem`, or `defaultSelectedItem` makes the chart selectable.
265
+ */
266
+ onSelectedItemChange?: (item: ChartSelectedItem) => void;
267
+ /**
268
+ * Tooltip on hover, keyboard focus, or tap. `axis` lists every series in the category, `item`
269
+ * only the bar under the pointer, `none` hides it. On web the plot is one tab stop. Tab /
270
+ * Shift+Tab move between categories and leave the chart after the last (or before the first)
271
+ * group. Arrow keys move along the category axis (and between series). Home and End jump to
272
+ * the first or last category. Enter or Space selects. Escape hides the tooltip. Each bar is
273
+ * announced to screen readers.
274
+ * @default { trigger: 'axis' }
275
+ */
276
+ tooltip?: ChartTooltipProps;
277
+ /** BCP 47 locale for default number and percent formatting. */
278
+ locale?: string;
279
+ /**
280
+ * Overrides for generated strings: summary, trend, waterfall step names, legend fallback, and
281
+ * no-value text. Keys you omit keep the English defaults.
282
+ */
283
+ localeText?: Partial<ChartLocaleText>;
284
+ /**
285
+ * Bars grow from the baseline on mount and tween to new values when `dataset` changes. Pass
286
+ * `'disable-all'` to draw the final state. Reduce Motion and `GlobalAnimationSettingsProvider`
287
+ * `disable-all` also turn it off.
288
+ */
289
+ animation?: AnimationRootDisableAll;
290
+ /**
291
+ * Accessible summary. Defaults to `localeText.barChartSummary` over every category.
292
+ */
293
+ accessibilityLabel?: string;
294
+ /** Additional Uniwind/Tailwind classes, merged after the root's own styles. */
295
+ className?: string;
296
+ }
297
+
298
+ interface BarSelection {
299
+ readonly seriesId: string;
300
+ readonly dataIndex: number;
301
+ }
302
+
303
+ interface BarPlan<TRow> {
304
+ readonly kind: BarChartKind;
305
+ readonly series: readonly BarChartSeries<TRow>[];
306
+ readonly geometry: readonly BarGeometrySeries[];
307
+ readonly totalKey: string | undefined;
308
+ }
309
+
310
+ function resolveBarPlan<TRow>(series: readonly BarChartSeries<TRow>[]): BarPlan<TRow> {
311
+ const toGeometry = (item: BarChartSeries<TRow>): BarGeometrySeries => ({
312
+ id: item.id,
313
+ dataKey: item.dataKey,
314
+ label: item.label ?? '',
315
+ stack: item.stack,
316
+ stackOffset: item.stackOffset,
317
+ });
318
+ const waterfall = series.find((item) => item.waterfall !== undefined);
319
+ if (waterfall !== undefined) {
320
+ return {
321
+ kind: 'waterfall',
322
+ series: [waterfall],
323
+ geometry: [toGeometry(waterfall)],
324
+ totalKey: waterfall.waterfall?.totalKey,
325
+ };
326
+ }
327
+ for (const comparison of series) {
328
+ const primary =
329
+ comparison.overlay === undefined
330
+ ? undefined
331
+ : series.find((item) => item.id === comparison.overlay?.seriesId && item !== comparison);
332
+ if (primary !== undefined) {
333
+ const pair = [primary, comparison];
334
+ return {
335
+ kind: 'overlay',
336
+ series: pair,
337
+ geometry: pair.map((item) => ({ ...toGeometry(item), stack: undefined })),
338
+ totalKey: undefined,
339
+ };
340
+ }
341
+ }
342
+ return { kind: 'bars', series, geometry: series.map(toGeometry), totalKey: undefined };
343
+ }
344
+
345
+ function sameItem(a: ChartSelectedItem | undefined, b: BarSelection): boolean {
346
+ return a?.seriesId === b.seriesId && a.dataIndex === b.dataIndex;
347
+ }
348
+
349
+ function barSelection(bar: BarRect): BarSelection {
350
+ return { seriesId: bar.seriesId, dataIndex: bar.index };
351
+ }
352
+
353
+ function barAccessibilityName(
354
+ label: string,
355
+ seriesLabel: string,
356
+ formatted: string,
357
+ change: string | null = null,
358
+ ): string {
359
+ const base =
360
+ seriesLabel.length > 0 ? `${label}, ${seriesLabel}, ${formatted}` : `${label}, ${formatted}`;
361
+ return change === null ? base : `${base}, ${change}`;
362
+ }
363
+
364
+ type BarFormatter = (bar: BarRect, location: ChartValueLocation) => string;
365
+
366
+ function BarChartInner<TRow extends object>(
367
+ {
368
+ dataset,
369
+ series: seriesProp,
370
+ xAxis,
371
+ yAxis,
372
+ size = 'lg',
373
+ width: widthProp,
374
+ height: heightProp,
375
+ layout: orientation = 'vertical',
376
+ hideLegend = false,
377
+ grid,
378
+ palette: paletteProp = 'auto',
379
+ barLabel = false,
380
+ borderRadius = BAR_RADIUS,
381
+ stackGap = DEFAULT_STACK_GAP,
382
+ categoryGapRatio = CATEGORY_GAP_RATIO,
383
+ barGapRatio = BAR_GAP_RATIO,
384
+ referenceLines,
385
+ emphasizedIndex,
386
+ scrollable = true,
387
+ visiblePoints,
388
+ selectedItem,
389
+ defaultSelectedItem,
390
+ onSelectedItemChange,
391
+ tooltip: tooltipProp,
392
+ locale,
393
+ localeText: localeTextProp,
394
+ animation,
395
+ accessibilityLabel,
396
+ className,
397
+ style,
398
+ onLayout,
399
+ ...props
400
+ }: BarChartProps<TRow>,
401
+ ref: ForwardedRef<View>,
402
+ ) {
403
+ const animated = useChartMotionEnabled(animation);
404
+ const simpleColor = useChartSimpleColor();
405
+ const gradientId = useChartGradientId('bar-fill');
406
+ const chrome = useChartChrome();
407
+ const palette = useChartPalette();
408
+ const paints = useChartColorPaints();
409
+ const localeText = useMemo(() => resolveChartLocaleText(localeTextProp), [localeTextProp]);
410
+ const plan = useMemo(() => resolveBarPlan(seriesProp), [seriesProp]);
411
+ const { kind, geometry } = plan;
412
+ const overlay = kind === 'overlay';
413
+ const waterfall = kind === 'waterfall';
414
+ const expandAxis = useMemo(
415
+ () => isExpandAxis(kind, resolveBarGroups(geometry)),
416
+ [kind, geometry],
417
+ );
418
+ const categoryAxis = (orientation === 'vertical' ? xAxis : yAxis)?.[0];
419
+ const valueAxis = (orientation === 'vertical' ? yAxis : xAxis)?.[0];
420
+ const categoryKey = categoryAxis?.dataKey;
421
+ const categoryLabelPosition = categoryAxis?.position;
422
+ const showTicks = valueAxis?.position !== 'none';
423
+ const showGrid =
424
+ grid === undefined
425
+ ? true
426
+ : (orientation === 'vertical' ? grid.horizontal : grid.vertical) === true;
427
+ const tickLabelInterval = categoryAxis?.tickLabelInterval;
428
+ const categories = useMemo(
429
+ () =>
430
+ dataset.map((row, index) => {
431
+ if (categoryKey === undefined) {
432
+ return String(index);
433
+ }
434
+ const raw: unknown = row[categoryKey];
435
+ return raw === null || raw === undefined ? '' : String(raw as string | number | boolean);
436
+ }),
437
+ [dataset, categoryKey],
438
+ );
439
+ const hasNegative = useMemo(
440
+ () =>
441
+ dataset.some((row) => geometry.some((item) => (readBarValue(row, item.dataKey) ?? 0) < 0)),
442
+ [dataset, geometry],
443
+ );
444
+ const sourceById = useMemo(
445
+ () => new Map(plan.series.map((item) => [item.id, item] as const)),
446
+ [plan.series],
447
+ );
448
+ const valueAxisMin = valueAxis?.min;
449
+ const valueAxisMax = valueAxis?.max;
450
+ const valueAxisTicks = valueAxis?.tickNumber;
451
+ const valueAxisFormatter = valueAxis?.valueFormatter;
452
+ const valueAxisConfig = useMemo(
453
+ () => ({ min: valueAxisMin, max: valueAxisMax, tickNumber: valueAxisTicks }),
454
+ [valueAxisMin, valueAxisMax, valueAxisTicks],
455
+ );
456
+ const percentFormatter: ChartValueFormatter = useMemo(
457
+ () => (value) => (value === null ? '' : formatChartPercent(value, locale)),
458
+ [locale],
459
+ );
460
+ const formatTick = useMemo(() => {
461
+ const formatter =
462
+ valueAxisFormatter ?? (expandAxis ? percentFormatter : defaultChartValueFormatter(locale));
463
+ return (value: number) => formatter(value, { location: 'tick' });
464
+ }, [valueAxisFormatter, expandAxis, percentFormatter, locale]);
465
+ const formatBar: BarFormatter = (bar, location) => {
466
+ const source = sourceById.get(bar.seriesId);
467
+ const context = { location, seriesId: bar.seriesId, dataIndex: bar.index };
468
+ if (bar.expand === true && source?.valueFormatter === undefined) {
469
+ return (valueAxisFormatter ?? percentFormatter)(bar.plotValue / 100, context);
470
+ }
471
+ const formatter = resolveValueFormatter(source, valueAxis, locale);
472
+ const format = (value: number) => formatter(value, context);
473
+ return bar.waterfallRole === undefined
474
+ ? format(bar.value)
475
+ : formatWaterfallBarValue(bar, format);
476
+ };
477
+ const seriesLabelOf = (bar: BarRect): string => {
478
+ if (bar.waterfallRole === 'increase') {
479
+ return localeText.waterfallIncrease;
480
+ }
481
+ if (bar.waterfallRole === 'decrease') {
482
+ return localeText.waterfallDecrease;
483
+ }
484
+ return bar.seriesLabel;
485
+ };
486
+
487
+ const box = resolveBarChartBox(size, widthProp, heightProp);
488
+ const [hostWidth, handleHostLayout] = useChartHostWidth(onLayout);
489
+ const viewportWidth = hostWidth > 0 ? Math.min(hostWidth, box.width) : box.width;
490
+ const viewportHeight = box.height;
491
+ const gutters = useMemo(
492
+ () =>
493
+ resolveBarChartGutters({
494
+ categories,
495
+ hasNegative,
496
+ orientation,
497
+ barLabel,
498
+ categoryLabelPosition,
499
+ showTicks,
500
+ overlay,
501
+ }),
502
+ [categories, hasNegative, orientation, barLabel, categoryLabelPosition, showTicks, overlay],
503
+ );
504
+ const content = useMemo(
505
+ () =>
506
+ resolveBarChartContentSize({
507
+ viewportWidth,
508
+ viewportHeight,
509
+ categoryCount: dataset.length,
510
+ orientation,
511
+ gutters,
512
+ scrollable,
513
+ visiblePoints,
514
+ }),
515
+ [
516
+ viewportWidth,
517
+ viewportHeight,
518
+ dataset.length,
519
+ orientation,
520
+ gutters,
521
+ scrollable,
522
+ visiblePoints,
523
+ ],
524
+ );
525
+ const showCategoryLabel = useMemo(() => {
526
+ if (typeof tickLabelInterval !== 'function') {
527
+ return undefined;
528
+ }
529
+ return (index: number) => {
530
+ const row = dataset.at(index);
531
+ const value: unknown =
532
+ row === undefined || categoryKey === undefined ? index : row[categoryKey];
533
+ return tickLabelInterval(value, index);
534
+ };
535
+ }, [tickLabelInterval, dataset, categoryKey]);
536
+ const layout = useMemo(
537
+ () =>
538
+ layoutBarChart({
539
+ dataset,
540
+ categories,
541
+ series: geometry,
542
+ kind,
543
+ waterfallTotalKey: plan.totalKey,
544
+ width: content.width,
545
+ height: content.height,
546
+ orientation,
547
+ barLabel,
548
+ categoryLabelPosition,
549
+ showTicks,
550
+ referenceLines,
551
+ valueAxis: valueAxisConfig,
552
+ formatTick,
553
+ stackGap,
554
+ categoryGapRatio,
555
+ barGapRatio,
556
+ showCategoryLabel,
557
+ }),
558
+ [
559
+ dataset,
560
+ categories,
561
+ geometry,
562
+ kind,
563
+ plan.totalKey,
564
+ content.width,
565
+ content.height,
566
+ orientation,
567
+ barLabel,
568
+ categoryLabelPosition,
569
+ showTicks,
570
+ referenceLines,
571
+ valueAxisConfig,
572
+ formatTick,
573
+ stackGap,
574
+ categoryGapRatio,
575
+ barGapRatio,
576
+ showCategoryLabel,
577
+ ],
578
+ );
579
+ const overflowsX = content.width > viewportWidth + 0.5;
580
+ const overflowsY = content.height > viewportHeight + 0.5;
581
+ const fills = waterfall
582
+ ? dataset.map((_, index) => palette[index % palette.length] ?? simpleColor)
583
+ : plan.series.map((item, seriesIndex) => {
584
+ const custom = resolveChartColor(item.color, paints);
585
+ if (custom !== undefined) {
586
+ return custom;
587
+ }
588
+ if (overlay) {
589
+ return seriesIndex === 0 ? simpleColor : chrome.grid;
590
+ }
591
+ return paints[chartSeriesColor(seriesIndex, plan.series.length, paletteProp, 'bar')];
592
+ });
593
+ const summaryCategories: ChartSummaryCategory[] = dataset.map((row, dataIndex) => ({
594
+ label: categories[dataIndex] ?? '',
595
+ values: plan.series.flatMap((item) => {
596
+ const value = readBarValue(row, item.dataKey);
597
+ if (value === null) {
598
+ return [];
599
+ }
600
+ // An expand axis formats 0–1 shares; the summary reads raw values.
601
+ const formatter = resolveValueFormatter(item, expandAxis ? null : valueAxis, locale);
602
+ return [
603
+ {
604
+ label: item.label ?? '',
605
+ formattedValue: formatter(value, { location: 'a11y', seriesId: item.id, dataIndex }),
606
+ },
607
+ ];
608
+ }),
609
+ }));
610
+ const summary = accessibilityLabel ?? localeText.barChartSummary(summaryCategories);
611
+ const trigger = tooltipProp?.trigger ?? 'axis';
612
+ const tooltip = trigger !== 'none';
613
+ const interactive =
614
+ onSelectedItemChange !== undefined ||
615
+ selectedItem !== undefined ||
616
+ defaultSelectedItem !== undefined;
617
+ const [selectedState, setSelectedItem] = useControllableState<ChartSelectedItem>({
618
+ prop: selectedItem,
619
+ defaultProp: defaultSelectedItem ?? null,
620
+ onChange: onSelectedItemChange,
621
+ });
622
+ const selected = selectedState ?? null;
623
+ const visibleBars = layout.bars.filter((bar) =>
624
+ bar.stackId !== undefined
625
+ ? bar.width > 0 && bar.height > 0
626
+ : orientation === 'vertical'
627
+ ? bar.width > 0
628
+ : bar.height > 0,
629
+ );
630
+ const inspectable = (interactive || tooltip) && visibleBars.length > 0;
631
+ const [activeIndex, setActiveIndex] = useState<number | null>(null);
632
+ const [dismissed, setDismissed] = useState(false);
633
+ const [scrubbing, setScrubbing] = useState(false);
634
+ const scrollOffset = useRef({ x: 0, y: 0 });
635
+ const selectedIndex = selected
636
+ ? visibleBars.findIndex((bar) => sameItem(selected, barSelection(bar)))
637
+ : -1;
638
+ const activeBar = activeIndex === null ? undefined : visibleBars.at(activeIndex);
639
+ const highlightBar: BarSelection | null = activeBar
640
+ ? barSelection(activeBar)
641
+ : selectedIndex >= 0 && selected !== null
642
+ ? selected
643
+ : null;
644
+ const tooltipIndex = tooltip && !dismissed && activeIndex !== null ? activeIndex : null;
645
+ const activate = (index: number) => {
646
+ setDismissed(false);
647
+ setActiveIndex((prev) => (prev === index ? prev : index));
648
+ };
649
+ const deactivate = (index: number) => {
650
+ setActiveIndex((prev) => (prev === index ? null : prev));
651
+ };
652
+
653
+ const showLegend = !waterfall && plan.series.length > 0 && !hideLegend;
654
+
655
+ const plot = (
656
+ <BarPlot
657
+ layout={layout}
658
+ orientation={orientation}
659
+ overlay={overlay}
660
+ waterfall={waterfall}
661
+ showGrid={showGrid}
662
+ barLabel={barLabel}
663
+ formatBar={formatBar}
664
+ formatStackTotal={(anchor) => {
665
+ const context = { location: 'label' as const, dataIndex: anchor.index };
666
+ if (anchor.expand) {
667
+ return (valueAxisFormatter ?? percentFormatter)(anchor.total / 100, context);
668
+ }
669
+ return resolveValueFormatter(
670
+ sourceById.get(anchor.seriesId),
671
+ valueAxis,
672
+ locale,
673
+ )(anchor.total, context);
674
+ }}
675
+ fills={fills}
676
+ gradientId={gradientId}
677
+ chrome={chrome}
678
+ accent={simpleColor}
679
+ borderRadius={Math.max(borderRadius, 0)}
680
+ animated={animated}
681
+ highlightBar={highlightBar}
682
+ emphasizedIndex={emphasizedIndex}
683
+ />
684
+ );
685
+
686
+ const vertical = orientation === 'vertical';
687
+ const targets = inspectable
688
+ ? barInspectorTargets({
689
+ bars: visibleBars,
690
+ layout,
691
+ orientation,
692
+ overlay,
693
+ waterfall,
694
+ trigger,
695
+ offsetX: vertical ? layout.plot.x : 0,
696
+ fills,
697
+ fallbackFill: chrome.grid,
698
+ formatBar,
699
+ seriesLabelOf,
700
+ localeText,
701
+ selectedIndex: selectedIndex >= 0 ? selectedIndex : null,
702
+ })
703
+ : [];
704
+ const tip = tooltipIndex === null ? undefined : targets.at(tooltipIndex);
705
+ const dismissOnScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
706
+ scrollOffset.current = event.nativeEvent.contentOffset;
707
+ if (tooltipIndex !== null) {
708
+ setDismissed(true);
709
+ }
710
+ };
711
+
712
+ const inspector = inspectable ? (
713
+ <BarInspector
714
+ bars={visibleBars}
715
+ targets={targets}
716
+ orientation={orientation}
717
+ summary={summary}
718
+ selectable={interactive}
719
+ activeIndex={activeIndex}
720
+ selectedIndex={selectedIndex >= 0 ? selectedIndex : null}
721
+ holdToScrub={overflowsX || overflowsY}
722
+ onScrubLockChange={setScrubbing}
723
+ onActivate={activate}
724
+ onDeactivate={deactivate}
725
+ onDismiss={() => {
726
+ setDismissed(true);
727
+ }}
728
+ onSelectIndex={(index) => {
729
+ const bar = visibleBars.at(index);
730
+ if (bar) {
731
+ setDismissed(false);
732
+ const next = barSelection(bar);
733
+ setSelectedItem(sameItem(selected, next) ? null : next);
734
+ }
735
+ }}
736
+ />
737
+ ) : null;
738
+ const tooltipLayer =
739
+ tip !== undefined ? (
740
+ <ChartTooltipLayer
741
+ content={tip.tooltip}
742
+ noValueText={localeText.noValue}
743
+ slot="bar-chart"
744
+ anchorX={vertical ? layout.plot.x + tip.anchorX - scrollOffset.current.x : tip.anchorX}
745
+ anchorY={vertical ? tip.anchorY : tip.anchorY - scrollOffset.current.y}
746
+ anchorRadius={tip.anchorRadius}
747
+ width={viewportWidth}
748
+ height={viewportHeight}
749
+ />
750
+ ) : null;
751
+
752
+ const ticks = showTicks ? (
753
+ <>
754
+ {layout.valueTicks.map((tick) => (
755
+ <SvgText
756
+ key={`tick-${String(tick.value)}`}
757
+ x={tick.x}
758
+ y={tick.y}
759
+ fill={chrome.label}
760
+ fontFamily={chrome.fontFamily}
761
+ fontSize={chrome.labelSize}
762
+ textAnchor={orientation === 'vertical' ? 'end' : 'middle'}
763
+ alignmentBaseline="middle"
764
+ >
765
+ {tick.text}
766
+ </SvgText>
767
+ ))}
768
+ </>
769
+ ) : null;
770
+
771
+ const chart = (
772
+ <View
773
+ style={{ width: '100%', maxWidth: viewportWidth, height: viewportHeight }}
774
+ {...dataAttributes({ slot: 'bar-chart-plot' })}
775
+ >
776
+ {orientation === 'vertical' ? (
777
+ <View className="w-full flex-row" style={{ height: viewportHeight }}>
778
+ <Svg
779
+ width={layout.plot.x}
780
+ height={viewportHeight}
781
+ viewBox={`0 0 ${String(layout.plot.x)} ${String(content.height)}`}
782
+ accessible={false}
783
+ accessibilityElementsHidden
784
+ importantForAccessibility="no-hide-descendants"
785
+ >
786
+ {ticks}
787
+ </Svg>
788
+ <ScrollView
789
+ horizontal
790
+ nestedScrollEnabled
791
+ scrollEnabled={overflowsX && !scrubbing}
792
+ showsHorizontalScrollIndicator={overflowsX}
793
+ scrollEventThrottle={16}
794
+ onScroll={dismissOnScroll}
795
+ className="flex-1"
796
+ style={{ height: viewportHeight }}
797
+ >
798
+ <View style={{ width: content.width - layout.plot.x, height: viewportHeight }}>
799
+ <Svg
800
+ width={content.width - layout.plot.x}
801
+ height={viewportHeight}
802
+ viewBox={`0 0 ${String(content.width - layout.plot.x)} ${String(content.height)}`}
803
+ style={SVG_OVERFLOW_VISIBLE}
804
+ pointerEvents="none"
805
+ accessible={false}
806
+ accessibilityElementsHidden
807
+ importantForAccessibility="no-hide-descendants"
808
+ >
809
+ <G transform={`translate(${String(-layout.plot.x)}, 0)`}>{plot}</G>
810
+ </Svg>
811
+ {inspector}
812
+ </View>
813
+ </ScrollView>
814
+ </View>
815
+ ) : (
816
+ <View style={{ width: viewportWidth, height: viewportHeight }}>
817
+ <ScrollView
818
+ nestedScrollEnabled
819
+ scrollEnabled={overflowsY && !scrubbing}
820
+ showsVerticalScrollIndicator={overflowsY}
821
+ scrollEventThrottle={16}
822
+ onScroll={dismissOnScroll}
823
+ style={{ height: viewportHeight - gutters.paddingBottom }}
824
+ >
825
+ <View style={{ width: viewportWidth, height: content.height - gutters.paddingBottom }}>
826
+ <Svg
827
+ width={viewportWidth}
828
+ height={content.height - gutters.paddingBottom}
829
+ viewBox={`0 0 ${String(content.width)} ${String(content.height - gutters.paddingBottom)}`}
830
+ pointerEvents="none"
831
+ accessible={false}
832
+ accessibilityElementsHidden
833
+ importantForAccessibility="no-hide-descendants"
834
+ >
835
+ {plot}
836
+ </Svg>
837
+ {inspector}
838
+ </View>
839
+ </ScrollView>
840
+ <Svg
841
+ width={viewportWidth}
842
+ height={gutters.paddingBottom}
843
+ viewBox={`0 0 ${String(content.width)} ${String(gutters.paddingBottom)}`}
844
+ accessible={false}
845
+ accessibilityElementsHidden
846
+ importantForAccessibility="no-hide-descendants"
847
+ >
848
+ <G transform={`translate(0, ${String(-(content.height - gutters.paddingBottom))})`}>
849
+ {ticks}
850
+ </G>
851
+ </Svg>
852
+ </View>
853
+ )}
854
+ {tooltipLayer}
855
+ </View>
856
+ );
857
+
858
+ return (
859
+ <View
860
+ ref={ref}
861
+ className={cn(barChartRootVariants(), className)}
862
+ style={style}
863
+ accessible={!inspectable && !showLegend}
864
+ accessibilityRole={!inspectable && !showLegend ? 'image' : undefined}
865
+ accessibilityLabel={!inspectable && !showLegend ? summary : undefined}
866
+ {...props}
867
+ onLayout={handleHostLayout}
868
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'bar-chart' }))}
869
+ >
870
+ {chart}
871
+ {showLegend ? (
872
+ <View
873
+ className={barChartLegendVariants()}
874
+ {...dataAttributes({ slot: 'bar-chart-legend' })}
875
+ >
876
+ {plan.series.map((item, seriesIndex) => {
877
+ const isSelected = highlightBar?.seriesId === item.id;
878
+ return (
879
+ <View
880
+ key={item.id}
881
+ className={barChartLegendItemVariants()}
882
+ {...dataAttributes({ slot: 'bar-chart-legend-item' })}
883
+ >
884
+ <View
885
+ className="h-2 w-3 rounded-sm"
886
+ style={{ backgroundColor: fills[seriesIndex] }}
887
+ />
888
+ <Text className={barChartLegendLabelVariants({ selected: isSelected })}>
889
+ {item.label !== undefined && item.label.length > 0
890
+ ? item.label
891
+ : localeText.defaultSeriesLabel}
892
+ </Text>
893
+ </View>
894
+ );
895
+ })}
896
+ </View>
897
+ ) : null}
898
+ </View>
899
+ );
900
+ }
901
+
902
+ /**
903
+ * Category bars from `dataset` rows. MUI X Charts–shaped: `series[].dataKey`, axis arrays,
904
+ * `layout`, `stack` groups, and `selectedItem`.
905
+ */
906
+ export const BarChart = forwardRef(BarChartInner) as <TRow extends object>(
907
+ props: BarChartProps<TRow> & { ref?: ForwardedRef<View> },
908
+ ) => ReactElement | null;
909
+
910
+ (BarChart as { displayName?: string }).displayName = 'BarChart';
911
+
912
+ function BarPlot({
913
+ layout,
914
+ orientation,
915
+ overlay,
916
+ waterfall,
917
+ showGrid,
918
+ barLabel,
919
+ formatBar,
920
+ formatStackTotal,
921
+ fills,
922
+ gradientId,
923
+ chrome,
924
+ accent,
925
+ borderRadius,
926
+ animated,
927
+ highlightBar,
928
+ emphasizedIndex,
929
+ }: {
930
+ layout: BarChartLayout;
931
+ orientation: BarChartOrientation;
932
+ overlay: boolean;
933
+ waterfall: boolean;
934
+ showGrid: boolean;
935
+ barLabel: boolean;
936
+ formatBar: BarFormatter;
937
+ formatStackTotal: (anchor: ReturnType<typeof stackedTotalAnchors>[number]) => string;
938
+ fills: readonly string[];
939
+ gradientId: string;
940
+ chrome: ChartChrome;
941
+ accent: string;
942
+ borderRadius: number;
943
+ animated: boolean;
944
+ highlightBar: BarSelection | null;
945
+ emphasizedIndex: number | undefined;
946
+ }) {
947
+ const labeledBars = layout.bars.filter((bar) => {
948
+ if (bar.stackId !== undefined) {
949
+ return false;
950
+ }
951
+ if (emphasizedIndex !== undefined && bar.index !== emphasizedIndex) {
952
+ return false;
953
+ }
954
+ return !overlay || bar.seriesIndex === 0;
955
+ });
956
+ const stackLabels = stackedTotalAnchors(layout.bars, orientation).filter(
957
+ (anchor) => emphasizedIndex === undefined || anchor.index === emphasizedIndex,
958
+ );
959
+ const paintedBars = overlay
960
+ ? [...layout.bars].sort((a, b) => b.seriesIndex - a.seriesIndex)
961
+ : layout.bars;
962
+
963
+ return (
964
+ <>
965
+ <ChartLinearFills
966
+ idPrefix={gradientId}
967
+ colors={fills}
968
+ x2={orientation === 'vertical' ? '0%' : '100%'}
969
+ y2={orientation === 'vertical' ? '100%' : '0%'}
970
+ />
971
+ {showGrid ? (
972
+ layout.gridLines.map((line) => (
973
+ <Line
974
+ key={`grid-${String(line.x1)}-${String(line.y1)}`}
975
+ x1={line.x1}
976
+ y1={line.y1}
977
+ x2={line.x2}
978
+ y2={line.y2}
979
+ stroke={chrome.grid}
980
+ strokeWidth={1}
981
+ />
982
+ ))
983
+ ) : (
984
+ <Line
985
+ x1={orientation === 'vertical' ? layout.plot.x : layout.baseline}
986
+ y1={orientation === 'vertical' ? layout.baseline : layout.plot.y}
987
+ x2={orientation === 'vertical' ? layout.plot.x + layout.plot.width : layout.baseline}
988
+ y2={orientation === 'vertical' ? layout.baseline : layout.plot.y + layout.plot.height}
989
+ stroke={chrome.grid}
990
+ strokeWidth={1}
991
+ />
992
+ )}
993
+ {layout.referenceLines.map((line, index) => (
994
+ <G key={`ref-${String(line.x1)}-${String(line.y1)}-${String(index)}`}>
995
+ <Line
996
+ x1={line.x1}
997
+ y1={line.y1}
998
+ x2={line.x2}
999
+ y2={line.y2}
1000
+ stroke={accent}
1001
+ strokeWidth={1}
1002
+ strokeDasharray={DASH}
1003
+ />
1004
+ {line.label !== undefined && line.label.length > 0 ? (
1005
+ <SvgText
1006
+ x={line.x1 + 4}
1007
+ y={orientation === 'vertical' ? line.y1 - 4 : line.y1 + 12}
1008
+ fill={accent}
1009
+ fontFamily={chrome.fontFamily}
1010
+ fontSize={chrome.captionSize}
1011
+ textAnchor="start"
1012
+ >
1013
+ {line.label}
1014
+ </SvgText>
1015
+ ) : null}
1016
+ </G>
1017
+ ))}
1018
+ {layout.connectors.map((line, index) => (
1019
+ <Line
1020
+ key={`wf-${String(line.x1)}-${String(line.y1)}-${String(index)}`}
1021
+ x1={line.x1}
1022
+ y1={line.y1}
1023
+ x2={line.x2}
1024
+ y2={line.y2}
1025
+ stroke={chrome.grid}
1026
+ strokeWidth={1}
1027
+ strokeDasharray={DASH}
1028
+ />
1029
+ ))}
1030
+ {paintedBars.map((bar) => {
1031
+ if (bar.width <= 0 || bar.height <= 0) {
1032
+ return null;
1033
+ }
1034
+ const dimmed =
1035
+ highlightBar !== null &&
1036
+ (bar.stackId !== undefined
1037
+ ? !sameItem(highlightBar, barSelection(bar))
1038
+ : highlightBar.dataIndex !== bar.index);
1039
+ const muted = emphasizedIndex !== undefined && bar.index !== emphasizedIndex;
1040
+ const comparison = overlay && bar.seriesIndex > 0;
1041
+ return (
1042
+ <AnimatedBar
1043
+ key={`${String(bar.index)}-${bar.seriesId}`}
1044
+ bar={bar}
1045
+ orientation={orientation}
1046
+ baseline={layout.baseline}
1047
+ animated={animated}
1048
+ fill={
1049
+ muted || (waterfall && dimmed)
1050
+ ? chrome.grid
1051
+ : comparison || waterfall
1052
+ ? (fills[bar.seriesIndex] ?? chrome.grid)
1053
+ : chartFillUrl(gradientId, bar.seriesIndex)
1054
+ }
1055
+ opacity={waterfall ? 1 : dimmed ? INACTIVE_OPACITY : 1}
1056
+ radius={borderRadius}
1057
+ />
1058
+ );
1059
+ })}
1060
+ {layout.categoryLabels.map((item, index) => (
1061
+ <SvgText
1062
+ key={`cat-${String(index)}-${item.label}`}
1063
+ x={item.x}
1064
+ y={item.y}
1065
+ fill={chrome.label}
1066
+ fontFamily={chrome.fontFamily}
1067
+ fontSize={chrome.labelSize}
1068
+ textAnchor={item.textAnchor}
1069
+ alignmentBaseline="middle"
1070
+ >
1071
+ {item.label}
1072
+ </SvgText>
1073
+ ))}
1074
+ {barLabel ? (
1075
+ <ValueLabels
1076
+ animated={animated}
1077
+ motionKey={`${orientation}-${overlay ? '1' : '0'}-${layout.bars
1078
+ .map((bar) => [bar.x, bar.y, bar.width, bar.height].join(','))
1079
+ .join(';')}`}
1080
+ >
1081
+ {(labelOpacity) => (
1082
+ <>
1083
+ {stackLabels.map((label) => (
1084
+ <SvgText
1085
+ key={`stack-val-${label.key}`}
1086
+ x={label.x}
1087
+ y={label.y}
1088
+ fill={chrome.label}
1089
+ fillOpacity={labelOpacity}
1090
+ fontFamily={chrome.fontFamily}
1091
+ fontSize={chrome.captionSize}
1092
+ textAnchor={label.textAnchor}
1093
+ alignmentBaseline="middle"
1094
+ >
1095
+ {formatStackTotal(label)}
1096
+ </SvgText>
1097
+ ))}
1098
+ {labeledBars.map((bar) => {
1099
+ const anchor = waterfall
1100
+ ? waterfallValueAnchor(bar, orientation)
1101
+ : barValueAnchor(bar, orientation);
1102
+ return (
1103
+ <G key={`val-${String(bar.index)}-${bar.seriesId}`}>
1104
+ <SvgText
1105
+ x={anchor.x}
1106
+ y={anchor.y}
1107
+ fill={chrome.label}
1108
+ fillOpacity={labelOpacity}
1109
+ fontFamily={chrome.fontFamily}
1110
+ fontSize={chrome.captionSize}
1111
+ textAnchor={anchor.textAnchor}
1112
+ alignmentBaseline="middle"
1113
+ >
1114
+ {formatBar(bar, 'label')}
1115
+ </SvgText>
1116
+ </G>
1117
+ );
1118
+ })}
1119
+ </>
1120
+ )}
1121
+ </ValueLabels>
1122
+ ) : null}
1123
+ </>
1124
+ );
1125
+ }
1126
+
1127
+ function barTooltipContent(
1128
+ group: readonly BarRect[],
1129
+ bar: BarRect,
1130
+ trigger: 'item' | 'axis',
1131
+ fills: readonly string[],
1132
+ fallbackFill: string,
1133
+ formatBar: BarFormatter,
1134
+ seriesLabelOf: (bar: BarRect) => string,
1135
+ ): ChartTooltipContent {
1136
+ const shown = trigger === 'axis' ? group : [bar];
1137
+ return {
1138
+ title: bar.label,
1139
+ items: shown.map((item) => ({
1140
+ seriesId: item.seriesId,
1141
+ label: seriesLabelOf(item),
1142
+ color: fills[item.seriesIndex] ?? fallbackFill,
1143
+ formattedValue: formatBar(item, 'tooltip'),
1144
+ })),
1145
+ };
1146
+ }
1147
+
1148
+ /**
1149
+ * One hit target per drawn bar. Grouped bars get the full value-axis span of their slot so short
1150
+ * bars stay easy to hover. Stacked segments keep their own rect, expanded to a 44px minimum
1151
+ * (WCAG 2.5.8 / HIG) so a 9px-tall bar is still a usable touch target.
1152
+ */
1153
+ /** Targets in scroll-content coordinates, shifted left by `offsetX`. */
1154
+ function barInspectorTargets({
1155
+ bars,
1156
+ layout,
1157
+ orientation,
1158
+ overlay,
1159
+ waterfall,
1160
+ trigger,
1161
+ offsetX,
1162
+ fills,
1163
+ fallbackFill,
1164
+ formatBar,
1165
+ seriesLabelOf,
1166
+ localeText,
1167
+ selectedIndex,
1168
+ }: {
1169
+ bars: readonly BarRect[];
1170
+ layout: BarChartLayout;
1171
+ orientation: BarChartOrientation;
1172
+ overlay: boolean;
1173
+ waterfall: boolean;
1174
+ trigger: 'item' | 'axis' | 'none';
1175
+ offsetX: number;
1176
+ fills: readonly string[];
1177
+ fallbackFill: string;
1178
+ formatBar: BarFormatter;
1179
+ seriesLabelOf: (bar: BarRect) => string;
1180
+ localeText: ChartLocaleText;
1181
+ selectedIndex: number | null;
1182
+ }): ChartInspectorTarget[] {
1183
+ const { plot } = layout;
1184
+ const vertical = orientation === 'vertical';
1185
+ const groups = new Map<number, BarRect[]>();
1186
+ bars.forEach((bar) => {
1187
+ const group = groups.get(bar.index);
1188
+ if (group === undefined) {
1189
+ groups.set(bar.index, [bar]);
1190
+ } else {
1191
+ group.push(bar);
1192
+ }
1193
+ });
1194
+ const outlines = new Map(
1195
+ [...groups].map(([categoryIndex, group]) => {
1196
+ const start = Math.min(...group.map((item) => (vertical ? item.x : item.y)));
1197
+ const end = Math.max(
1198
+ ...group.map((item) => (vertical ? item.x + item.width : item.y + item.height)),
1199
+ );
1200
+ const outline = vertical
1201
+ ? { left: start - offsetX - 4, top: plot.y, width: end - start + 8, height: plot.height }
1202
+ : { left: plot.x - offsetX, top: start - 4, width: plot.width, height: end - start + 8 };
1203
+ return [categoryIndex, outline] as const;
1204
+ }),
1205
+ );
1206
+ const trendText = (bar: BarRect): string | null => {
1207
+ if (!overlay || bar.seriesIndex !== 0) {
1208
+ return null;
1209
+ }
1210
+ const group = groups.get(bar.index) ?? [];
1211
+ const delta = overlayDelta(group, bar.index);
1212
+ if (delta === null) {
1213
+ return null;
1214
+ }
1215
+ const comparison = group.find((item) => item.seriesIndex === 1);
1216
+ return localeText.trend({
1217
+ direction: delta > 0 ? 'up' : delta < 0 ? 'down' : 'none',
1218
+ formattedDelta: formatBar({ ...bar, value: Math.abs(delta) }, 'a11y'),
1219
+ comparisonLabel: comparison?.seriesLabel ?? '',
1220
+ });
1221
+ };
1222
+ return bars.map((bar, index): ChartInspectorTarget => {
1223
+ const x = bar.x - offsetX;
1224
+ const raw =
1225
+ bar.stackId !== undefined || waterfall || !vertical
1226
+ ? { left: x, top: bar.y, width: bar.width, height: bar.height }
1227
+ : { left: x, top: plot.y, width: bar.width, height: plot.height };
1228
+ const box = expandHitBox(raw.left, raw.top, raw.width, raw.height);
1229
+ const group = groups.get(bar.index) ?? [bar];
1230
+ const outline = outlines.get(bar.index);
1231
+ return {
1232
+ key: `hit-${String(bar.index)}-${bar.seriesId}`,
1233
+ ...box,
1234
+ outline,
1235
+ anchorX: vertical ? x + bar.width / 2 : x + bar.width,
1236
+ anchorY: bar.y,
1237
+ anchorRadius: vertical ? bar.width / 2 : 0,
1238
+ tooltip: barTooltipContent(
1239
+ group,
1240
+ bar,
1241
+ trigger === 'item' ? 'item' : 'axis',
1242
+ fills,
1243
+ fallbackFill,
1244
+ formatBar,
1245
+ seriesLabelOf,
1246
+ ),
1247
+ accessibilityLabel: barAccessibilityName(
1248
+ bar.label,
1249
+ seriesLabelOf(bar),
1250
+ formatBar(bar, 'a11y'),
1251
+ trendText(bar),
1252
+ ),
1253
+ selected: selectedIndex === index,
1254
+ };
1255
+ });
1256
+ }
1257
+
1258
+ function BarInspector({
1259
+ bars,
1260
+ targets,
1261
+ orientation,
1262
+ summary,
1263
+ selectable,
1264
+ activeIndex,
1265
+ selectedIndex,
1266
+ onActivate,
1267
+ onDeactivate,
1268
+ onDismiss,
1269
+ onSelectIndex,
1270
+ holdToScrub,
1271
+ onScrubLockChange,
1272
+ }: {
1273
+ onScrubLockChange: (locked: boolean) => void;
1274
+ bars: readonly BarRect[];
1275
+ targets: readonly ChartInspectorTarget[];
1276
+ orientation: BarChartOrientation;
1277
+ summary: string;
1278
+ selectable: boolean;
1279
+ activeIndex: number | null;
1280
+ selectedIndex: number | null;
1281
+ holdToScrub: boolean;
1282
+ onActivate: (index: number) => void;
1283
+ onDeactivate: (index: number) => void;
1284
+ onDismiss: () => void;
1285
+ onSelectIndex: (index: number) => void;
1286
+ }) {
1287
+ return (
1288
+ <ChartInspector
1289
+ targets={targets}
1290
+ summary={summary}
1291
+ selectable={selectable}
1292
+ activeIndex={activeIndex}
1293
+ selectedIndex={selectedIndex}
1294
+ slot="bar-chart"
1295
+ holdToScrub={holdToScrub}
1296
+ onScrubLockChange={onScrubLockChange}
1297
+ hitTest={(x, y) => {
1298
+ let best: number | null = null;
1299
+ let bestDist = Number.POSITIVE_INFINITY;
1300
+ targets.forEach((target, index) => {
1301
+ if (
1302
+ x < target.left ||
1303
+ x > target.left + target.width ||
1304
+ y < target.top ||
1305
+ y > target.top + target.height
1306
+ ) {
1307
+ return;
1308
+ }
1309
+ const dist = Math.hypot(
1310
+ x - (target.left + target.width / 2),
1311
+ y - (target.top + target.height / 2),
1312
+ );
1313
+ if (dist < bestDist) {
1314
+ bestDist = dist;
1315
+ best = index;
1316
+ }
1317
+ });
1318
+ return best;
1319
+ }}
1320
+ onActivate={onActivate}
1321
+ onDeactivate={onDeactivate}
1322
+ onDismiss={onDismiss}
1323
+ onSelectIndex={onSelectIndex}
1324
+ neighborIndex={(index, key) => barKeyboardNeighbor(bars, index, key, orientation)}
1325
+ />
1326
+ );
1327
+ }
1328
+
1329
+ function collapsedBar(bar: BarRect, orientation: BarChartOrientation, baseline: number): number[] {
1330
+ if (orientation === 'vertical') {
1331
+ const onBaseline = Math.abs((bar.plotValue >= 0 ? bar.y + bar.height : bar.y) - baseline) < 1;
1332
+ return onBaseline ? [bar.x, baseline, bar.width, 0] : [bar.x, bar.y + bar.height, bar.width, 0];
1333
+ }
1334
+ const onBaseline = Math.abs((bar.plotValue >= 0 ? bar.x : bar.x + bar.width) - baseline) < 1;
1335
+ return onBaseline ? [baseline, bar.y, 0, bar.height] : [bar.x, bar.y, 0, bar.height];
1336
+ }
1337
+
1338
+ function AnimatedBar({
1339
+ bar,
1340
+ orientation,
1341
+ baseline,
1342
+ animated,
1343
+ fill,
1344
+ opacity,
1345
+ radius,
1346
+ }: {
1347
+ bar: BarRect;
1348
+ orientation: BarChartOrientation;
1349
+ baseline: number;
1350
+ animated: boolean;
1351
+ fill: string;
1352
+ opacity: number;
1353
+ radius: number;
1354
+ }) {
1355
+ const { rounded, roundAll, value } = bar;
1356
+ const morph = useChartMorph(
1357
+ [bar.x, bar.y, bar.width, bar.height],
1358
+ collapsedBar(bar, orientation, baseline),
1359
+ animated,
1360
+ );
1361
+ const { from, to, progress } = morph;
1362
+ const animatedProps = useAnimatedProps(() => {
1363
+ const [x = 0, y = 0, width = 0, height = 0] = lerpVector(from.get(), to.get(), progress.get());
1364
+ return {
1365
+ d: barPath({ x, y, width, height, rounded, roundAll, value }, orientation, radius),
1366
+ };
1367
+ }, [from, to, progress, rounded, roundAll, value, orientation, radius]);
1368
+
1369
+ return <AnimatedPath animatedProps={animatedProps} fill={fill} opacity={opacity} />;
1370
+ }
1371
+
1372
+ function ValueLabels({
1373
+ animated,
1374
+ motionKey,
1375
+ children,
1376
+ }: {
1377
+ animated: boolean;
1378
+ motionKey: string;
1379
+ children: (opacity: number) => ReactNode;
1380
+ }) {
1381
+ const [shownKey, setShownKey] = useState(animated ? null : motionKey);
1382
+
1383
+ useEffect(() => {
1384
+ if (!animated) {
1385
+ setShownKey(motionKey);
1386
+ return;
1387
+ }
1388
+ const id = setTimeout(() => setShownKey(motionKey), CHART_MOTION_DURATION);
1389
+ return () => {
1390
+ clearTimeout(id);
1391
+ };
1392
+ }, [animated, motionKey]);
1393
+
1394
+ if (!animated) {
1395
+ return <>{children(1)}</>;
1396
+ }
1397
+ if (shownKey !== motionKey) {
1398
+ return null;
1399
+ }
1400
+ return <FadeInLabels key={motionKey}>{children}</FadeInLabels>;
1401
+ }
1402
+
1403
+ /** Group opacity on native SVG flashes a full-opacity frame; fade each text's fill instead. */
1404
+ function FadeInLabels({ children }: { children: (opacity: number) => ReactNode }) {
1405
+ const [opacity, setOpacity] = useState(0);
1406
+
1407
+ useEffect(() => {
1408
+ let frame = 0;
1409
+ const started = Date.now();
1410
+ const tick = () => {
1411
+ const t = Math.min(1, (Date.now() - started) / LABEL_FADE_DURATION);
1412
+ setOpacity(1 - (1 - t) ** 3);
1413
+ if (t < 1) {
1414
+ frame = requestAnimationFrame(tick);
1415
+ }
1416
+ };
1417
+ frame = requestAnimationFrame(tick);
1418
+ return () => {
1419
+ cancelAnimationFrame(frame);
1420
+ };
1421
+ }, []);
1422
+
1423
+ return <>{children(opacity)}</>;
1424
+ }