@cdc/chart 4.26.6 → 4.26.10

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 (276) hide show
  1. package/CONFIG.md +504 -368
  2. package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
  3. package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
  4. package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
  5. package/dist/cdcchart.js +4 -96498
  6. package/examples/__data__/vega-remote-data.json +6 -0
  7. package/examples/dendrogram.json +18 -0
  8. package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
  9. package/examples/feature/heatmap/average-age-categorical.json +2 -2
  10. package/examples/feature/heatmap/calendar-heatmap.json +2 -2
  11. package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
  12. package/examples/feature/heatmap/sparse-aggregation.json +2 -2
  13. package/examples/feature/line/line-chart-race.json +50 -0
  14. package/examples/feature/sankey/sankey-example-data.json +40 -1441
  15. package/examples/feature/sankey/sankey_chart_data.csv +9 -20
  16. package/examples/network.json +31 -0
  17. package/examples/private/box-plot.json +573 -0
  18. package/examples/private/custom-color-issue.json +625 -0
  19. package/examples/private/custom-colors.json +522 -0
  20. package/examples/private/cutoff.json +10750 -0
  21. package/examples/private/flueview.json +22051 -0
  22. package/examples/private/overlay-legend.json +485 -0
  23. package/examples/private/pie-test.json +469 -0
  24. package/examples/vega-remote.json +22 -0
  25. package/package.json +11 -13
  26. package/src/CdcChart.tsx +2 -2
  27. package/src/CdcChartComponent.tsx +444 -217
  28. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  29. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  30. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  31. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  32. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  33. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  34. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  35. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  36. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  37. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  38. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  39. package/src/_stories/Chart.Network.stories.tsx +432 -0
  40. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  41. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  42. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  43. package/src/_stories/Chart.PieLabels.stories.tsx +49 -0
  44. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  45. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  46. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  47. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  48. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  49. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  50. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  51. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  52. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  53. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  54. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  55. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  56. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  57. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  58. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  59. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  60. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  61. package/src/_stories/_mock/editor-test-configs.json +20 -10
  62. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  63. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  64. package/src/_stories/_mock/heatmap.json +2 -2
  65. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  66. package/src/_stories/_mock/sankey_mock.json +23 -27
  67. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  68. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  69. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  70. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  71. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  72. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  73. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  74. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  75. package/src/components/Axis/BottomAxis.tsx +1 -1
  76. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  77. package/src/components/Axis/RightAxis.tsx +21 -13
  78. package/src/components/BarChart/components/BarChart.Horizontal.tsx +39 -8
  79. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +13 -4
  80. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  81. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +30 -26
  82. package/src/components/BarChart/components/BarChart.Vertical.tsx +38 -28
  83. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  84. package/src/components/BarChart/components/context.tsx +6 -0
  85. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  86. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  87. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  88. package/src/components/BarChart/helpers/index.ts +8 -0
  89. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  90. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  91. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  92. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  93. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +202 -0
  94. package/src/components/BarChart/helpers/useBarChart.ts +60 -60
  95. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  96. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  97. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  98. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  99. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  100. package/src/components/BarChartRace/helpers.test.ts +114 -0
  101. package/src/components/BarChartRace/helpers.ts +185 -0
  102. package/src/components/BarChartRace/index.ts +3 -0
  103. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  104. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  105. package/src/components/BoxPlot/helpers/index.ts +13 -83
  106. package/src/components/Brush/BrushSelector.tsx +10 -24
  107. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  108. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  109. package/src/components/Dendrogram/dendrogram.css +113 -0
  110. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  111. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  112. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  113. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  114. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  115. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  116. package/src/components/Dendrogram/index.tsx +1 -0
  117. package/src/components/Dendrogram/types/index.ts +66 -0
  118. package/src/components/EditorPanel/EditorPanel.tsx +1111 -805
  119. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  120. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  121. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  122. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  123. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +104 -61
  124. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  125. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  126. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +236 -36
  127. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  128. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  129. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +21 -6
  130. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  131. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  132. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  133. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  134. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  135. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  136. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  137. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  138. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +5 -4
  139. package/src/components/EditorPanel/useEditorPermissions.test.tsx +94 -0
  140. package/src/components/EditorPanel/useEditorPermissions.ts +75 -25
  141. package/src/components/Forecasting/Forecasting.tsx +16 -7
  142. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  143. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  144. package/src/components/HeatMap/heatmap.css +101 -2
  145. package/src/components/HeatMap/helpers/layout.ts +80 -0
  146. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  147. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  148. package/src/components/Legend/Legend.Component.tsx +163 -148
  149. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  150. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  151. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -35
  152. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  153. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  154. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  155. package/src/components/LineChart/helpers.test.ts +126 -38
  156. package/src/components/LineChart/helpers.ts +9 -6
  157. package/src/components/LineChart/index.tsx +71 -27
  158. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  159. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  160. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  161. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  162. package/src/components/LineChartRace/helpers.test.ts +88 -0
  163. package/src/components/LineChartRace/helpers.ts +157 -0
  164. package/src/components/LineChartRace/index.ts +3 -0
  165. package/src/components/LineChartRace/line-chart-race.css +16 -0
  166. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  167. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  168. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  169. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  170. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  171. package/src/components/LinearChart.tsx +85 -54
  172. package/src/components/Network/components/Network.tsx +372 -0
  173. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  174. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  175. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  176. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  177. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  178. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  179. package/src/components/Network/index.tsx +3 -0
  180. package/src/components/Network/network.css +107 -0
  181. package/src/components/Network/types/index.ts +56 -0
  182. package/src/components/PairedBarChart.jsx +5 -10
  183. package/src/components/PairedBarChart.test.tsx +71 -0
  184. package/src/components/PieChart/PieChart.tsx +121 -13
  185. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  186. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  187. package/src/components/RacingChartRenderer.tsx +44 -0
  188. package/src/components/Regions/components/Regions.test.tsx +179 -0
  189. package/src/components/Regions/components/Regions.tsx +104 -69
  190. package/src/components/Regions/index.tsx +2 -3
  191. package/src/components/Sankey/components/Sankey.tsx +264 -437
  192. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  193. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  194. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  195. package/src/components/Sankey/sankey.scss +57 -116
  196. package/src/components/Sankey/types/index.ts +41 -4
  197. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -12
  198. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  199. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  200. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  201. package/src/components/raceTiming.ts +18 -0
  202. package/src/data/initial-state.js +79 -31
  203. package/src/helpers/applyChartTypeContract.ts +71 -0
  204. package/src/helpers/axisLabelFontSize.ts +2 -2
  205. package/src/helpers/boxPlotStats.ts +60 -0
  206. package/src/helpers/bumpChartTooltip.ts +2 -1
  207. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  208. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  209. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  210. package/src/helpers/categoryOrder.ts +60 -0
  211. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  212. package/src/helpers/createNewChartConfig.ts +56 -0
  213. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  214. package/src/helpers/forecastLegend.ts +88 -0
  215. package/src/helpers/getBoxPlotConfig.ts +26 -38
  216. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  217. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  218. package/src/helpers/getColorScale.ts +31 -22
  219. package/src/helpers/getComboChartConfig.ts +0 -1
  220. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  221. package/src/helpers/getExcludedData.ts +0 -1
  222. package/src/helpers/getHasBoundarySuppression.ts +4 -3
  223. package/src/helpers/getMinMax.ts +11 -8
  224. package/src/helpers/getNewRuntime.ts +4 -3
  225. package/src/helpers/getPiePercent.ts +3 -3
  226. package/src/helpers/getStackedSeriesMax.ts +51 -0
  227. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  228. package/src/helpers/handleRankByValue.ts +15 -15
  229. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  230. package/src/helpers/seriesColumnSettings.ts +31 -3
  231. package/src/helpers/sizeHelpers.ts +4 -5
  232. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  233. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  234. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  235. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  236. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  237. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  238. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  239. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  240. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  241. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  242. package/src/helpers/tests/getColorScale.test.ts +226 -1
  243. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  244. package/src/helpers/tests/getMinMax.test.ts +41 -0
  245. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  246. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  247. package/src/helpers/tests/seriesColumnSettings.test.ts +83 -0
  248. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  249. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  250. package/src/helpers/tooltipHelpers.ts +16 -0
  251. package/src/hooks/tests/useRightAxis.test.tsx +143 -2
  252. package/src/hooks/tests/useScales.test.tsx +3 -6
  253. package/src/hooks/tests/useTooltip.test.tsx +484 -0
  254. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  255. package/src/hooks/useReduceData.ts +4 -36
  256. package/src/hooks/useRightAxis.ts +19 -26
  257. package/src/hooks/useScales.ts +15 -0
  258. package/src/hooks/useTooltip.tsx +106 -71
  259. package/src/scss/main.scss +74 -2
  260. package/src/store/chart.reducer.ts +4 -4
  261. package/src/test/CdcChart.dataTable.test.tsx +582 -14
  262. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  263. package/src/test/CdcChart.finalization.test.tsx +143 -0
  264. package/src/test/CdcChart.network.test.tsx +271 -0
  265. package/src/types/ChartConfig.ts +91 -54
  266. package/src/types/ChartContext.ts +5 -0
  267. package/src/types/Horizon.ts +1 -1
  268. package/LICENSE +0 -201
  269. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  270. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  271. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  272. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  273. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  274. package/src/data/legacy-defaults.ts +0 -18
  275. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  276. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -0,0 +1,202 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { createMockChartContext, createMockConfig } from '../../../LinearChart/tests/mockConfigContext'
4
+ import { useBarChart } from '../useBarChart'
5
+
6
+ const baseConfig = createMockConfig()
7
+
8
+ const makeConfigContext = (countColumnOverrides: Record<string, unknown> = {}) => {
9
+ const config = createMockConfig({
10
+ visualizationType: 'Bar',
11
+ orientation: 'horizontal',
12
+ barHeight: 25,
13
+ xAxis: {
14
+ ...baseConfig.xAxis,
15
+ type: 'categorical',
16
+ dataKey: 'Cause of death'
17
+ },
18
+ yAxis: {
19
+ ...baseConfig.yAxis,
20
+ labelPlacement: 'Below Bar'
21
+ },
22
+ series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }] as any,
23
+ columns: {
24
+ Count: {
25
+ name: 'Count',
26
+ label: 'Count',
27
+ prefix: '',
28
+ suffix: '',
29
+ roundToPlace: 0,
30
+ commas: true,
31
+ dataTable: true,
32
+ tooltips: true,
33
+ ...countColumnOverrides
34
+ }
35
+ } as any,
36
+ dataFormat: {
37
+ abbreviated: false,
38
+ bottomAbbreviated: false,
39
+ bottomPrefix: '',
40
+ bottomRoundTo: 0,
41
+ bottomSuffix: '',
42
+ bottomCommas: false,
43
+ commas: false,
44
+ prefix: '',
45
+ preserveOriginalDecimals: false,
46
+ rightPrefix: '',
47
+ rightRoundTo: 0,
48
+ rightSuffix: '',
49
+ roundTo: 1,
50
+ suffix: '%'
51
+ } as any,
52
+ runtime: {
53
+ ...baseConfig.runtime,
54
+ xAxis: {
55
+ ...baseConfig.runtime.xAxis,
56
+ type: 'categorical',
57
+ dataKey: 'Cause of death'
58
+ },
59
+ yAxis: {
60
+ ...baseConfig.runtime.yAxis,
61
+ dataKey: 'Percentage'
62
+ },
63
+ originalXAxis: {
64
+ ...baseConfig.runtime.originalXAxis,
65
+ dataKey: 'Cause of death'
66
+ },
67
+ series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }],
68
+ seriesKeys: ['Percentage'],
69
+ seriesLabels: { Percentage: 'Percentage' },
70
+ seriesLabelsAll: ['Percentage']
71
+ } as any
72
+ })
73
+
74
+ return createMockChartContext(config, {
75
+ tableData: [
76
+ {
77
+ 'Cause of death': 'Cardiovascular conditions',
78
+ Percentage: 22,
79
+ Count: 146
80
+ }
81
+ ],
82
+ updateConfig: vi.fn()
83
+ })
84
+ }
85
+
86
+ describe('useBarChart', () => {
87
+ it('adds raw percent decoration without replacing the formatted tooltip value', () => {
88
+ const configContext = makeConfigContext()
89
+ configContext.tableData[0].Percentage = '22.500%'
90
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
91
+
92
+ expect(result.current.formatTooltipValue('Percentage', 'Cardiovascular conditions', '22.5')).toBe('22.5%')
93
+ })
94
+
95
+ it('preserves a raw trailing percent sign for the matching dynamic category', () => {
96
+ const configContext = makeConfigContext()
97
+ configContext.config.series = [
98
+ {
99
+ dataKey: 'Data_Value',
100
+ dynamicCategory: 'Age group',
101
+ type: 'Bar',
102
+ axis: 'Left',
103
+ tooltip: true
104
+ }
105
+ ] as any
106
+ configContext.tableData = [
107
+ {
108
+ 'Cause of death': 'Cardiovascular conditions',
109
+ 'Age group': '65 years or older',
110
+ Data_Value: '56.4%'
111
+ }
112
+ ]
113
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
114
+
115
+ expect(result.current.formatTooltipValue('65 years or older', 'Cardiovascular conditions', '56.4')).toBe('56.4%')
116
+ })
117
+
118
+ it('matches numeric dynamic-category values to their rendered string series keys', () => {
119
+ const configContext = makeConfigContext()
120
+ configContext.config.series = [
121
+ {
122
+ dataKey: 'Data_Value',
123
+ dynamicCategory: 'Age group',
124
+ type: 'Bar',
125
+ axis: 'Left',
126
+ tooltip: true
127
+ }
128
+ ] as any
129
+ configContext.tableData = [
130
+ {
131
+ 'Cause of death': 'Cardiovascular conditions',
132
+ 'Age group': 65,
133
+ Data_Value: '56.4%'
134
+ }
135
+ ]
136
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
137
+
138
+ expect(result.current.formatTooltipValue('65', 'Cardiovascular conditions', '56.4')).toBe('56.4%')
139
+ })
140
+
141
+ it('uses the indexed raw row when static categories are duplicated', () => {
142
+ const configContext = makeConfigContext()
143
+ configContext.tableData = [
144
+ { 'Cause of death': 'Repeated category', Percentage: '22.5%', Count: 146 },
145
+ { 'Cause of death': 'Repeated category', Percentage: '30%', Count: 150 }
146
+ ]
147
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
148
+
149
+ expect(result.current.formatTooltipValue('Percentage', 'Repeated category', '30.0', 1)).toBe('30.0%')
150
+ })
151
+
152
+ it('uses the brush-aligned raw row when a duplicate category subset shifts the rendered index', () => {
153
+ const configContext = makeConfigContext()
154
+ configContext.tableData = [
155
+ { 'Cause of death': 'Repeated category', Percentage: 22.5, Count: 146 },
156
+ { 'Cause of death': 'Repeated category', Percentage: '30%', Count: 150 }
157
+ ]
158
+ configContext.brushData = [configContext.tableData[1]]
159
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
160
+
161
+ expect(result.current.formatTooltipValue('Percentage', 'Repeated category', '30.0', 0)).toBe('30.0%')
162
+ })
163
+
164
+ it('uses the formatted tooltip value when the raw value is numeric', () => {
165
+ const configContext = makeConfigContext()
166
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
167
+
168
+ expect(result.current.formatTooltipValue('Percentage', 'Cardiovascular conditions', '22.0')).toBe('22.0')
169
+ })
170
+
171
+ it('uses the formatted tooltip value when no raw row matches', () => {
172
+ const configContext = makeConfigContext()
173
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
174
+
175
+ expect(result.current.formatTooltipValue('Percentage', 'Missing category', 'N/A')).toBe('N/A')
176
+ })
177
+
178
+ it('does not mutate config when horizontal bar label placement is missing', () => {
179
+ const configContext = makeConfigContext()
180
+ delete configContext.config.yAxis.labelPlacement
181
+
182
+ renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
183
+
184
+ expect(configContext.updateConfig).not.toHaveBeenCalled()
185
+ })
186
+
187
+ it('does not apply the left-axis suffix to an additional single-series tooltip column', () => {
188
+ const configContext = makeConfigContext()
189
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
190
+
191
+ expect(result.current.getAdditionalColumn('Percentage', 'Cardiovascular conditions')).toBe('Count : 146 <br/>')
192
+ })
193
+
194
+ it('applies an additional column suffix when one is configured', () => {
195
+ const configContext = makeConfigContext({ suffix: ' deaths' })
196
+ const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
197
+
198
+ expect(result.current.getAdditionalColumn('Percentage', 'Cardiovascular conditions')).toBe(
199
+ 'Count : 146 deaths <br/>'
200
+ )
201
+ })
202
+ })
@@ -1,16 +1,17 @@
1
- import React, { useContext, useEffect, useState } from 'react'
1
+ import React, { useContext, useEffect, useMemo, useState } from 'react'
2
2
  import { ChartDispatchContext } from '../../../ConfigContext'
3
3
  import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
4
4
  import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
5
- import { getPaletteColors } from '@cdc/core/helpers/palettes/utils'
6
5
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
7
6
  import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
8
7
  import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
9
- import { getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
8
+ import { getAdditionalColumnFormattingParams, getSeriesOwnedColumnNames } from '../../../helpers/seriesColumnSettings'
9
+ import { getColorCodeCategoryColorMap } from '../../../helpers/getColorCodeCategoryColorMap'
10
10
 
11
11
  export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, configContext) => {
12
12
  const {
13
13
  config,
14
+ brushData,
14
15
  colorPalettes,
15
16
  tableData,
16
17
  updateConfig,
@@ -53,18 +54,37 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
53
54
  ? seriesHighlight
54
55
  : config.runtime.barSeriesKeys || config.runtime.seriesKeys
55
56
  const seriesOwnedColumnNames = getSeriesOwnedColumnNames(config.series)
57
+ const dynamicSeries = config.series.find(series => series.dynamicCategory)
58
+ const originalXAxisDataKey = config.runtime.originalXAxis.dataKey
59
+ const sourceDataRows = Array.isArray(brushData) && brushData.length > 0 ? brushData : tableData
56
60
 
57
- useEffect(() => {
58
- if (orientation === 'horizontal' && !config.yAxis.labelPlacement) {
59
- updateConfig({
60
- ...config,
61
- yAxis: {
62
- ...config.yAxis,
63
- labelPlacement: 'Below Bar'
64
- }
61
+ // Index source rows once so rendered bars can recover display-only formatting without rescanning the data.
62
+ const sourceRowsByXAxisAndSeries = useMemo(() => {
63
+ const rowsByXAxis = new Map<string, Map<string, Record<string, unknown>>>()
64
+
65
+ sourceDataRows?.forEach(row => {
66
+ const xAxisValue = String(row?.[originalXAxisDataKey])
67
+ const rowsBySeries = rowsByXAxis.get(xAxisValue) ?? new Map<string, Record<string, unknown>>()
68
+
69
+ config.series.forEach(series => {
70
+ const seriesKey = series.dynamicCategory ? row?.[series.dynamicCategory] : series.dataKey
71
+ if (seriesKey === undefined || seriesKey === null) return
72
+ rowsBySeries.set(String(seriesKey), row)
65
73
  })
66
- }
67
- }, [config, updateConfig]) // eslint-disable-line
74
+
75
+ rowsByXAxis.set(xAxisValue, rowsBySeries)
76
+ })
77
+
78
+ return rowsByXAxis
79
+ }, [sourceDataRows, config.series, originalXAxisDataKey])
80
+
81
+ const findSourceDataRow = (seriesKey: string, xAxisValue: unknown, rowIndex?: number) => {
82
+ // Static charts can repeat x-axis values, so prefer the rendered row index when it still matches.
83
+ const indexedRow = !dynamicSeries && Number.isInteger(rowIndex) ? sourceDataRows?.[rowIndex] : undefined
84
+ if (indexedRow && String(indexedRow[originalXAxisDataKey]) === String(xAxisValue)) return indexedRow
85
+
86
+ return sourceRowsByXAxisAndSeries.get(String(xAxisValue))?.get(String(seriesKey))
87
+ }
68
88
 
69
89
  useEffect(() => {
70
90
  if (config.isLollipopChart === false && config.barHeight < 25) {
@@ -72,15 +92,6 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
72
92
  }
73
93
  }, [config.isLollipopChart]) // eslint-disable-line
74
94
 
75
- useEffect(() => {
76
- if (config.visualizationSubType === 'horizontal') {
77
- updateConfig({
78
- ...config,
79
- orientation: 'horizontal'
80
- })
81
- }
82
- }, []) // eslint-disable-line
83
-
84
95
  useEffect(() => {
85
96
  if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
86
97
  updateConfig({ ...config, isLollipopChart: true })
@@ -111,34 +122,16 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
111
122
  return style
112
123
  }
113
124
 
114
- const assignColorsToValues = (barsCount, barIndex, currentBarColor) => {
115
- if (!config.legend.colorCode && config.series.length > 1) {
116
- return currentBarColor
117
- }
118
- const palettesArr = getPaletteColors(config, colorPalettes)
119
- const values = tableData.map(d => {
120
- return d[config.legend.colorCode]
121
- })
122
- // Map to hold unique values and their colors
123
- let colorMap = new Map()
124
- // Resultant array to hold colors to the values
125
- let palette = []
125
+ const colorCodeCategoryColors = getColorCodeCategoryColorMap(config, tableData, colorPalettes)
126
126
 
127
- for (let i = 0; i < values.length; i++) {
128
- // If value not in map, add it and assign a color
129
- if (!colorMap.has(values[i])) {
130
- colorMap.set(values[i], palettesArr[colorMap.size % palettesArr.length])
131
- }
132
- // push the color to the result array
133
- palette.push(colorMap.get(values[i]))
134
- }
127
+ const assignColorsToValues = (barIndex, currentBarColor) => {
128
+ const colorCode = config.legend.colorCode
129
+ if (!colorCode) return currentBarColor
135
130
 
136
- // loop throghy existing colors and extend if needed
137
- while (palette.length < barsCount) {
138
- palette = palette.concat(palette)
139
- }
140
- const barColor = palette[barIndex]
141
- return barColor
131
+ const rowIndex = tableData.length ? barIndex % tableData.length : -1
132
+ const category = tableData[rowIndex]?.[colorCode]
133
+
134
+ return colorCodeCategoryColors.get(category) ?? currentBarColor
142
135
  }
143
136
 
144
137
  const getHighlightedBarColorByValue = value => {
@@ -173,22 +166,12 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
173
166
  const columns = config.columns
174
167
  const columnsWithTooltips = []
175
168
  let additionalTooltipItems = ''
176
- const dynamicCategorySeries = config.runtime?.series?.find(series => series?.dynamicCategory)
177
- const closestVal =
178
- tableData.find(d => {
179
- const dynamicCategoryMatch = dynamicCategorySeries ? d[dynamicCategorySeries.dynamicCategory] === series : true
180
- return d[config.xAxis.dataKey] === xAxisDataValue && dynamicCategoryMatch
181
- }) || {}
169
+ const closestVal = findSourceDataRow(series, xAxisDataValue) || {}
182
170
  Object.keys(columns).forEach(colKeys => {
183
171
  const colConfig = config.columns[colKeys]
184
172
  if (seriesOwnedColumnNames.includes(colConfig.name || colKeys)) return
185
173
  if (series && colConfig.series && colConfig.series !== series && !colConfig.tooltips) return
186
- const formattingParams = {
187
- addColPrefix: config.columns[colKeys].prefix,
188
- addColSuffix: config.columns[colKeys].suffix,
189
- addColRoundTo: config.columns[colKeys].roundToPlace ? config.columns[colKeys].roundToPlace : '',
190
- addColCommas: config.columns[colKeys].commas
191
- }
174
+ const formattingParams = getAdditionalColumnFormattingParams(config.columns[colKeys])
192
175
 
193
176
  const formattedValue = formatColNumber(
194
177
  closestVal[config.columns[colKeys].name],
@@ -207,6 +190,22 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
207
190
  return additionalTooltipItems
208
191
  }
209
192
 
193
+ const formatTooltipValue = (
194
+ seriesKey: string,
195
+ xAxisValue: unknown,
196
+ displayValue: string | number,
197
+ rowIndex?: number
198
+ ): string | number => {
199
+ const rawRow = findSourceDataRow(seriesKey, xAxisValue, rowIndex)
200
+ const staticSeries = config.series.find(series => !series.dynamicCategory && series.dataKey === seriesKey)
201
+ const rawValueKey = staticSeries?.dataKey ?? dynamicSeries?.dataKey ?? seriesKey
202
+ const rawValue = rawRow?.[rawValueKey]
203
+ const formattedText = String(displayValue)
204
+ const sourceUsesPercentage = typeof rawValue === 'string' && rawValue.trim().endsWith('%')
205
+
206
+ return sourceUsesPercentage && formattedText && !formattedText.endsWith('%') ? `${formattedText}%` : displayValue
207
+ }
208
+
210
209
  const onMouseOverBar = (categoryValue, barKey, event, data, barValue) => {
211
210
  if (config.legend.highlightOnHover && config.legend.behavior === 'highlight' && barKey) {
212
211
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: [barKey] })
@@ -256,6 +255,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
256
255
  barStackedSeriesKeys,
257
256
  hasMultipleSeries,
258
257
  labelFontSize,
258
+ formatTooltipValue,
259
259
  applyRadius,
260
260
  assignColorsToValues,
261
261
  getHighlightedBarColorByValue,
@@ -0,0 +1,199 @@
1
+ import React from 'react'
2
+ import { act, fireEvent, render, screen } from '@testing-library/react'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import ConfigContext from '../../ConfigContext'
5
+ import { createMockChartContext, createMockConfig } from '../LinearChart/tests/mockConfigContext'
6
+ import BarChartRace from './BarChartRace'
7
+ import { getBarRaceEligibility } from './helpers'
8
+
9
+ const data = [
10
+ { Year: '2020', Place: 'Alpha', Value: 10 },
11
+ { Year: '2020', Place: 'Beta', Value: 20 },
12
+ { Year: '2021', Place: 'Alpha', Value: 30 },
13
+ { Year: '2021', Place: 'Beta', Value: 15 }
14
+ ]
15
+
16
+ type RaceOptions = {
17
+ displayNumbersOnBar?: boolean
18
+ formatNumber?: ReturnType<typeof vi.fn>
19
+ labelPlacement?: string
20
+ }
21
+
22
+ const getRaceContext = (rows = data, options: RaceOptions = {}) => {
23
+ const baseConfig = createMockConfig()
24
+ const yAxis = { ...baseConfig.yAxis }
25
+ delete yAxis.labelPlacement
26
+ if (options.labelPlacement) yAxis.labelPlacement = options.labelPlacement
27
+ if (options.displayNumbersOnBar !== undefined) yAxis.displayNumbersOnBar = options.displayNumbersOnBar
28
+ const config = createMockConfig({
29
+ visualizationType: 'Bar',
30
+ visualizationSubType: 'racing',
31
+ orientation: 'horizontal',
32
+ barStyle: 'flat',
33
+ isLollipopChart: false,
34
+ barRace: { maxBars: 2, secondsPerFrame: 0.5 },
35
+ xAxis: { ...baseConfig.xAxis, type: 'categorical', dataKey: 'Year', label: 'Year' },
36
+ yAxis,
37
+ series: [{ dataKey: 'Value', dynamicCategory: 'Place', axis: 'left', type: 'Bar' }] as any,
38
+ columns: { Value: { name: 'Value', label: 'Value', prefix: '$', roundToPlace: 0 } } as any,
39
+ runtime: { ...baseConfig.runtime, seriesKeys: ['Alpha', 'Beta'], seriesLabelsAll: ['Alpha', 'Beta'] }
40
+ })
41
+ return createMockChartContext(config, {
42
+ transformedData: rows,
43
+ colorScale: category => (category === 'Alpha' ? '#005ea8' : '#712177'),
44
+ formatNumber:
45
+ options.formatNumber ?? ((value, _axis, _abbreviated, prefix = '', suffix = '') => `${prefix}${value}${suffix}`)
46
+ })
47
+ }
48
+
49
+ const renderRace = (rows = data, options: RaceOptions = {}) => {
50
+ const context = getRaceContext(rows, options)
51
+ const race = getBarRaceEligibility(context.config, rows)
52
+
53
+ return render(
54
+ <ConfigContext.Provider value={context}>
55
+ <BarChartRace parentWidth={700} race={race} />
56
+ </ConfigContext.Provider>
57
+ )
58
+ }
59
+
60
+ describe('BarChartRace', () => {
61
+ beforeEach(() => {
62
+ vi.useFakeTimers()
63
+ vi.stubGlobal('matchMedia', () => ({
64
+ matches: true,
65
+ addEventListener: vi.fn(),
66
+ removeEventListener: vi.fn()
67
+ }))
68
+ })
69
+
70
+ afterEach(() => {
71
+ vi.useRealTimers()
72
+ vi.unstubAllGlobals()
73
+ })
74
+
75
+ it('uses the on-axis label layout when label placement is missing', () => {
76
+ const { container } = renderRace()
77
+
78
+ expect(container.querySelector('.bar-chart-race')).toHaveClass('bar-chart-race--labels-on-axis')
79
+ expect(container.querySelector('.bar-chart-race__plot')).toHaveStyle({ height: '96px' })
80
+ })
81
+
82
+ it('uses the on-axis label layout when label placement is explicit', () => {
83
+ const { container } = renderRace(data, { labelPlacement: 'On Date/Category Axis' })
84
+
85
+ expect(container.querySelector('.bar-chart-race')).toHaveClass('bar-chart-race--labels-on-axis')
86
+ expect(container.querySelector('.bar-chart-race')).not.toHaveClass('bar-chart-race--labels-below-bar')
87
+ })
88
+
89
+ it('moves labels below bars and adds row spacing for the below-bar layout', () => {
90
+ const { container } = renderRace(data, { labelPlacement: 'Below Bar' })
91
+
92
+ expect(container.querySelector('.bar-chart-race')).toHaveClass('bar-chart-race--labels-below-bar')
93
+ expect(container.querySelector('.bar-chart-race__plot')).toHaveStyle({ height: '128px' })
94
+ })
95
+
96
+ it('keeps value labels after bars when display numbers on bar is disabled', () => {
97
+ const { container } = renderRace(data, { displayNumbersOnBar: false })
98
+
99
+ expect(container.querySelectorAll('.bar-chart-race__value')).toHaveLength(2)
100
+ expect(container.querySelectorAll('.bar-chart-race__value--after')).toHaveLength(2)
101
+ expect(container.querySelector('.bar-chart-race__value--inside')).not.toBeInTheDocument()
102
+ })
103
+
104
+ it('places displayed values inside fitting bars and after bars that are too short', () => {
105
+ const rows = [
106
+ { Year: '2020', Place: 'Short', Value: 1 },
107
+ { Year: '2020', Place: 'Long', Value: 100 },
108
+ { Year: '2021', Place: 'Short', Value: 2 },
109
+ { Year: '2021', Place: 'Long', Value: 90 }
110
+ ]
111
+ const { container } = renderRace(rows, { displayNumbersOnBar: true })
112
+
113
+ expect(container.querySelector('[data-category="Long"] .bar-chart-race__value')).toHaveClass(
114
+ 'bar-chart-race__value--inside'
115
+ )
116
+ expect(container.querySelector('[data-category="Short"] .bar-chart-race__value')).toHaveClass(
117
+ 'bar-chart-race__value--after'
118
+ )
119
+ })
120
+
121
+ it('starts paused, advances, pauses, and replays from the first frame', () => {
122
+ const { container } = renderRace()
123
+
124
+ const frame = container.querySelector('.bar-chart-race__frame')
125
+ const frameAxis = screen.getByRole('group', { name: 'Year axis' })
126
+ expect(frame).toHaveTextContent('2020')
127
+ expect(frameAxis).toHaveTextContent('2020')
128
+ expect(frameAxis).toHaveTextContent('2021')
129
+ expect(frameAxis.querySelector('[aria-current="step"]')).toHaveTextContent('2020')
130
+ const playButton = screen.getByRole('button', { name: 'Play' })
131
+ expect(container.querySelector('.bar-chart-race__header')?.firstElementChild).toBe(playButton)
132
+ expect(playButton.nextElementSibling).toBe(frame)
133
+ expect(playButton.querySelector('[data-icon="play"]')).toBeInTheDocument()
134
+ fireEvent.click(playButton)
135
+ expect(screen.getByRole('button', { name: 'Pause' }).querySelector('[data-icon="pause"]')).toBeInTheDocument()
136
+ act(() => vi.advanceTimersByTime(500))
137
+ expect(frame).toHaveTextContent('2021')
138
+ expect(frameAxis.querySelector('[aria-current="step"]')).toHaveTextContent('2021')
139
+ expect(screen.getByRole('button', { name: 'Replay' }).querySelector('[data-icon="replay"]')).toBeInTheDocument()
140
+
141
+ fireEvent.click(screen.getByRole('button', { name: 'Replay' }))
142
+ expect(frame).toHaveTextContent('2020')
143
+ expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument()
144
+ fireEvent.click(screen.getByRole('button', { name: 'Pause' }))
145
+ act(() => vi.advanceTimersByTime(500))
146
+ expect(frame).toHaveTextContent('2020')
147
+ })
148
+
149
+ it('uses stable category colors, column formatting, and the standard chart tooltip markup', () => {
150
+ const { container } = renderRace(data, { displayNumbersOnBar: true })
151
+ const alphaRow = container.querySelector('[data-category="Alpha"]') as HTMLElement
152
+
153
+ expect(alphaRow.querySelector('.bar-chart-race__bar')).toHaveStyle({ backgroundColor: '#005ea8' })
154
+ expect(alphaRow).toHaveTextContent('$10')
155
+ expect(alphaRow.dataset.tooltipHtml).toContain('class="tooltip-heading">2020')
156
+ expect(alphaRow.dataset.tooltipHtml).toContain('Alpha: $10')
157
+ expect(alphaRow.dataset.tooltipId).toContain('cdc-open-viz-tooltip-')
158
+ expect(container.querySelector('.bar-chart-race__tooltip')).not.toBeInTheDocument()
159
+ })
160
+
161
+ it('normalizes authored series axis names before applying value formatting', () => {
162
+ const formatNumber = vi.fn((value, _axis, _abbreviated, prefix = '', suffix = '') => `${prefix}${value}${suffix}`)
163
+ const { container } = renderRace(data, { formatNumber })
164
+
165
+ expect(container.querySelector('[data-category="Alpha"] .bar-chart-race__value')).toHaveTextContent('$10')
166
+ expect(formatNumber).toHaveBeenCalledWith(10, 'left', false, '$', undefined, 0)
167
+ })
168
+
169
+ it('clears playback timers when unmounted', () => {
170
+ const clearIntervalSpy = vi.spyOn(window, 'clearInterval')
171
+ const view = renderRace()
172
+ fireEvent.click(screen.getByRole('button', { name: 'Play' }))
173
+ view.unmount()
174
+ expect(clearIntervalSpy).toHaveBeenCalled()
175
+ })
176
+
177
+ it('resets to the first paused frame when visible data changes', () => {
178
+ const context = getRaceContext()
179
+ const race = getBarRaceEligibility(context.config, data)
180
+ const view = render(
181
+ <ConfigContext.Provider value={context}>
182
+ <BarChartRace parentWidth={700} race={race} />
183
+ </ConfigContext.Provider>
184
+ )
185
+ fireEvent.click(screen.getByRole('button', { name: 'Play' }))
186
+ act(() => vi.advanceTimersByTime(500))
187
+ expect(view.container.querySelector('.bar-chart-race__frame')).toHaveTextContent('2021')
188
+
189
+ const nextRows = [...data, { Year: '2022', Place: 'Alpha', Value: 50 }, { Year: '2022', Place: 'Beta', Value: 45 }]
190
+ view.rerender(
191
+ <ConfigContext.Provider value={{ ...context, transformedData: nextRows }}>
192
+ <BarChartRace parentWidth={700} race={getBarRaceEligibility(context.config, nextRows)} />
193
+ </ConfigContext.Provider>
194
+ )
195
+
196
+ expect(view.container.querySelector('.bar-chart-race__frame')).toHaveTextContent('2020')
197
+ expect(screen.getByRole('button', { name: 'Play' })).toBeInTheDocument()
198
+ })
199
+ })