@cdc/chart 4.26.7 → 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 (255) hide show
  1. package/CONFIG.md +502 -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 -97406
  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/overlay-legend.json +485 -0
  22. package/examples/private/pie-test.json +469 -0
  23. package/examples/vega-remote.json +22 -0
  24. package/package.json +11 -5
  25. package/src/CdcChartComponent.tsx +405 -208
  26. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  27. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  28. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  29. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  30. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  31. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  32. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  33. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  34. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  35. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  36. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  37. package/src/_stories/Chart.Network.stories.tsx +432 -0
  38. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  39. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  40. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  41. package/src/_stories/Chart.PieLabels.stories.tsx +4 -2
  42. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  43. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  44. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  45. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  46. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  47. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  48. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  49. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  50. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  51. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  52. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  53. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  54. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  55. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  56. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  57. package/src/_stories/_mock/editor-test-configs.json +20 -10
  58. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  59. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  60. package/src/_stories/_mock/heatmap.json +2 -2
  61. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  62. package/src/_stories/_mock/sankey_mock.json +23 -27
  63. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  64. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  65. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  66. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  67. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  68. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  69. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  70. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  71. package/src/components/Axis/BottomAxis.tsx +1 -1
  72. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  73. package/src/components/Axis/RightAxis.tsx +21 -13
  74. package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -6
  75. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -3
  76. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  77. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +29 -25
  78. package/src/components/BarChart/components/BarChart.Vertical.tsx +37 -26
  79. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  80. package/src/components/BarChart/components/context.tsx +6 -0
  81. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  82. package/src/components/BarChart/helpers/index.ts +8 -0
  83. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  84. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  85. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  86. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  87. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +104 -0
  88. package/src/components/BarChart/helpers/useBarChart.ts +60 -59
  89. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  90. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  91. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  92. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  93. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  94. package/src/components/BarChartRace/helpers.test.ts +114 -0
  95. package/src/components/BarChartRace/helpers.ts +185 -0
  96. package/src/components/BarChartRace/index.ts +3 -0
  97. package/src/components/BoxPlot/helpers/index.ts +11 -82
  98. package/src/components/Brush/BrushSelector.tsx +9 -23
  99. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  100. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  101. package/src/components/Dendrogram/dendrogram.css +113 -0
  102. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  103. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  104. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  105. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  106. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  107. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  108. package/src/components/Dendrogram/index.tsx +1 -0
  109. package/src/components/Dendrogram/types/index.ts +66 -0
  110. package/src/components/EditorPanel/EditorPanel.tsx +1005 -760
  111. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  112. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  113. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  114. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  115. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +88 -54
  116. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  117. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  118. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +233 -33
  119. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  120. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  121. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  122. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  123. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  124. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  125. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  126. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  127. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  128. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  129. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  130. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +2 -1
  131. package/src/components/EditorPanel/useEditorPermissions.test.tsx +62 -0
  132. package/src/components/EditorPanel/useEditorPermissions.ts +70 -24
  133. package/src/components/Forecasting/Forecasting.tsx +15 -6
  134. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  135. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  136. package/src/components/HeatMap/heatmap.css +101 -2
  137. package/src/components/HeatMap/helpers/layout.ts +80 -0
  138. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  139. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  140. package/src/components/Legend/Legend.Component.tsx +110 -90
  141. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  142. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -34
  143. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  144. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  145. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  146. package/src/components/LineChart/helpers.test.ts +126 -38
  147. package/src/components/LineChart/helpers.ts +9 -4
  148. package/src/components/LineChart/index.tsx +69 -24
  149. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  150. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  151. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  152. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  153. package/src/components/LineChartRace/helpers.test.ts +88 -0
  154. package/src/components/LineChartRace/helpers.ts +157 -0
  155. package/src/components/LineChartRace/index.ts +3 -0
  156. package/src/components/LineChartRace/line-chart-race.css +16 -0
  157. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  158. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  159. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  160. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  161. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  162. package/src/components/LinearChart.tsx +81 -51
  163. package/src/components/Network/components/Network.tsx +372 -0
  164. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  165. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  166. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  167. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  168. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  169. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  170. package/src/components/Network/index.tsx +3 -0
  171. package/src/components/Network/network.css +107 -0
  172. package/src/components/Network/types/index.ts +56 -0
  173. package/src/components/PairedBarChart.jsx +5 -10
  174. package/src/components/PairedBarChart.test.tsx +71 -0
  175. package/src/components/PieChart/PieChart.tsx +9 -8
  176. package/src/components/RacingChartRenderer.tsx +44 -0
  177. package/src/components/Regions/components/Regions.test.tsx +179 -0
  178. package/src/components/Regions/components/Regions.tsx +104 -69
  179. package/src/components/Regions/index.tsx +2 -3
  180. package/src/components/Sankey/components/Sankey.tsx +264 -437
  181. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  182. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  183. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  184. package/src/components/Sankey/sankey.scss +57 -116
  185. package/src/components/Sankey/types/index.ts +41 -4
  186. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -4
  187. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  188. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  189. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  190. package/src/components/raceTiming.ts +18 -0
  191. package/src/data/initial-state.js +77 -31
  192. package/src/helpers/applyChartTypeContract.ts +71 -0
  193. package/src/helpers/axisLabelFontSize.ts +2 -2
  194. package/src/helpers/boxPlotStats.ts +60 -0
  195. package/src/helpers/bumpChartTooltip.ts +2 -1
  196. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  197. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  198. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  199. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  200. package/src/helpers/createNewChartConfig.ts +56 -0
  201. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  202. package/src/helpers/forecastLegend.ts +88 -0
  203. package/src/helpers/getBoxPlotConfig.ts +18 -37
  204. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  205. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  206. package/src/helpers/getColorScale.ts +31 -22
  207. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  208. package/src/helpers/getMinMax.ts +9 -7
  209. package/src/helpers/getNewRuntime.ts +5 -2
  210. package/src/helpers/getStackedSeriesMax.ts +51 -0
  211. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  212. package/src/helpers/handleRankByValue.ts +15 -15
  213. package/src/helpers/seriesColumnSettings.ts +10 -0
  214. package/src/helpers/sizeHelpers.ts +4 -3
  215. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  216. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  217. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  218. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  219. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  220. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  221. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  222. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  223. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  224. package/src/helpers/tests/getColorScale.test.ts +226 -1
  225. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  226. package/src/helpers/tests/getMinMax.test.ts +41 -0
  227. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  228. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  229. package/src/helpers/tests/seriesColumnSettings.test.ts +23 -0
  230. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  231. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  232. package/src/helpers/tooltipHelpers.ts +16 -0
  233. package/src/hooks/tests/useTooltip.test.tsx +353 -1
  234. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  235. package/src/hooks/useReduceData.ts +4 -36
  236. package/src/hooks/useScales.ts +15 -0
  237. package/src/hooks/useTooltip.tsx +95 -64
  238. package/src/scss/main.scss +74 -2
  239. package/src/store/chart.reducer.ts +4 -3
  240. package/src/test/CdcChart.dataTable.test.tsx +490 -16
  241. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  242. package/src/test/CdcChart.finalization.test.tsx +143 -0
  243. package/src/test/CdcChart.network.test.tsx +271 -0
  244. package/src/types/ChartConfig.ts +91 -54
  245. package/src/types/ChartContext.ts +5 -0
  246. package/src/types/Horizon.ts +1 -1
  247. package/LICENSE +0 -201
  248. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  249. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  250. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  251. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  252. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  253. package/src/data/legacy-defaults.ts +0 -18
  254. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  255. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -0,0 +1,280 @@
1
+ import { getChartPatternId } from '../../../../helpers/getChartPatternId'
2
+ import {
3
+ createPatternColumnConfig,
4
+ ensurePatternColumnConfig,
5
+ findColumnConfigKey,
6
+ getPortionPatternBoundaryGeometry,
7
+ getPortionPatternGeometry,
8
+ getPortionPatternOverlay,
9
+ getPortionPatternRenderData,
10
+ isPortionPatternSupported,
11
+ removeUnusedPatternColumnConfig
12
+ } from '../portionPattern'
13
+
14
+ describe('portion pattern column configurations', () => {
15
+ it('creates only the standard tooltip and data-table settings needed by a pattern column', () => {
16
+ expect(createPatternColumnConfig('new_cases')).toEqual({
17
+ name: 'new_cases',
18
+ label: 'new_cases',
19
+ prefix: '',
20
+ suffix: '',
21
+ roundToPlace: 0,
22
+ commas: true,
23
+ dataTable: true,
24
+ tooltips: true
25
+ })
26
+ })
27
+
28
+ it('creates a configuration only when the selected column is not already configured', () => {
29
+ const columns = {
30
+ existing: {
31
+ name: 'new_cases',
32
+ label: 'New cases',
33
+ commas: false,
34
+ dataTable: false,
35
+ tooltips: false
36
+ }
37
+ } as any
38
+
39
+ expect(ensurePatternColumnConfig(columns, 'new_cases')).toBe(columns)
40
+ expect(ensurePatternColumnConfig(columns, 'previous_cases')).toEqual({
41
+ ...columns,
42
+ previous_cases: createPatternColumnConfig('previous_cases')
43
+ })
44
+ })
45
+
46
+ it('finds configurations stored under either the column name or an alias key', () => {
47
+ expect(findColumnConfigKey({ new_cases: { label: 'New cases' } } as any, 'new_cases')).toBe('new_cases')
48
+ expect(
49
+ findColumnConfigKey({ additionalColumn1: { name: 'new_cases', label: 'New cases' } } as any, 'new_cases')
50
+ ).toBe('additionalColumn1')
51
+ })
52
+
53
+ it('removes an obsolete pattern column configuration', () => {
54
+ const columns = {
55
+ additionalColumn1: { name: 'new_cases', label: 'New cases' },
56
+ source: { name: 'source', label: 'Source' }
57
+ } as any
58
+
59
+ expect(
60
+ removeUnusedPatternColumnConfig({
61
+ columns,
62
+ columnName: 'new_cases',
63
+ patterns: {}
64
+ })
65
+ ).toEqual({ source: columns.source })
66
+ })
67
+
68
+ it('preserves a column still used by another portion pattern', () => {
69
+ const columns = { new_cases: { name: 'new_cases', label: 'New cases' } } as any
70
+
71
+ expect(
72
+ removeUnusedPatternColumnConfig({
73
+ columns,
74
+ columnName: 'new_cases',
75
+ patterns: {
76
+ Pattern2: { application: 'portion', patternValueKey: 'new_cases' }
77
+ }
78
+ })
79
+ ).toBe(columns)
80
+ })
81
+
82
+ it('preserves columns owned by an axis or chart series', () => {
83
+ const columns = { cases: { name: 'cases', label: 'Cases' } } as any
84
+
85
+ expect(
86
+ removeUnusedPatternColumnConfig({
87
+ columns,
88
+ columnName: 'cases',
89
+ patterns: {},
90
+ protectedColumnNames: ['date', 'cases']
91
+ })
92
+ ).toBe(columns)
93
+ })
94
+ })
95
+
96
+ describe('portion pattern support', () => {
97
+ const supportedConfig = {
98
+ visualizationType: 'Bar',
99
+ visualizationSubType: 'regular',
100
+ isLollipopChart: false,
101
+ yAxis: { type: 'linear' }
102
+ } as any
103
+
104
+ it('supports regular Bar and Combo charts with linear axes', () => {
105
+ expect(isPortionPatternSupported(supportedConfig)).toBe(true)
106
+ expect(isPortionPatternSupported({ ...supportedConfig, visualizationType: 'Combo' })).toBe(true)
107
+ })
108
+
109
+ it.each([
110
+ ['stacked bars', { visualizationSubType: 'stacked' }],
111
+ ['lollipops', { isLollipopChart: true }],
112
+ ['logarithmic axes', { yAxis: { type: 'logarithmic' } }],
113
+ ['other chart types', { visualizationType: 'Line' }]
114
+ ])('does not support %s', (_description, overrides) => {
115
+ expect(isPortionPatternSupported({ ...supportedConfig, ...overrides } as any)).toBe(false)
116
+ })
117
+ })
118
+
119
+ describe('getPortionPatternOverlay', () => {
120
+ const getOverlay = (totalValue: unknown, portion: unknown, placement?: 'start' | 'end') =>
121
+ getPortionPatternOverlay({
122
+ patterns: {
123
+ Pattern1: {
124
+ application: 'portion',
125
+ dataKey: 'total',
126
+ patternValueKey: 'portion',
127
+ placement,
128
+ color: '#123456'
129
+ }
130
+ },
131
+ datum: { total: totalValue, portion },
132
+ seriesKey: 'total',
133
+ totalValue
134
+ })
135
+
136
+ it('calculates ratios for portions less than and equal to the total', () => {
137
+ expect(getOverlay(100, 25)).toEqual({
138
+ patternUrl: `url(#${getChartPatternId('Pattern1')})`,
139
+ ratio: 0.25,
140
+ placement: 'end',
141
+ patternColor: '#123456'
142
+ })
143
+ expect(getOverlay('100', '100')?.ratio).toBe(1)
144
+ })
145
+
146
+ it('preserves beginning placement', () => {
147
+ expect(getOverlay(100, 40, 'start')?.placement).toBe('start')
148
+ })
149
+
150
+ it.each([
151
+ ['missing total', undefined, 10],
152
+ ['nonnumeric total', 'not-a-number', 10],
153
+ ['zero total', 0, 0],
154
+ ['negative total', -100, 10],
155
+ ['missing portion', 100, undefined],
156
+ ['nonnumeric portion', 100, 'not-a-number'],
157
+ ['zero portion', 100, 0],
158
+ ['negative portion', 100, -10],
159
+ ['portion greater than total', 100, 101]
160
+ ])('returns no overlay for %s', (_description, totalValue, portion) => {
161
+ expect(getOverlay(totalValue, portion)).toBeNull()
162
+ })
163
+
164
+ it('uses only the first configured portion pattern for a target series', () => {
165
+ const overlay = getPortionPatternOverlay({
166
+ patterns: {
167
+ First: { application: 'portion', dataKey: 'total', patternValueKey: 'invalidPortion' },
168
+ Second: { application: 'portion', dataKey: 'total', patternValueKey: 'validPortion' }
169
+ },
170
+ datum: { total: 100, invalidPortion: 200, validPortion: 25 },
171
+ seriesKey: 'total',
172
+ totalValue: 100
173
+ })
174
+
175
+ expect(overlay).toBeNull()
176
+ })
177
+ })
178
+
179
+ describe('getPortionPatternGeometry', () => {
180
+ it.each([
181
+ ['vertical end', 'vertical', 'end', { x: 10, y: 20, width: 100, height: 40 }],
182
+ ['vertical start', 'vertical', 'start', { x: 10, y: 60, width: 100, height: 40 }],
183
+ ['horizontal end', 'horizontal', 'end', { x: 60, y: 20, width: 50, height: 80 }],
184
+ ['horizontal start', 'horizontal', 'start', { x: 10, y: 20, width: 50, height: 80 }]
185
+ ] as const)('calculates %s placement', (_description, orientation, placement, expected) => {
186
+ expect(
187
+ getPortionPatternGeometry({
188
+ orientation,
189
+ bounds: { x: 10, y: 20, width: 100, height: 80 },
190
+ overlay: { ratio: 0.5, placement }
191
+ })
192
+ ).toEqual(expected)
193
+ })
194
+ })
195
+
196
+ describe('getPortionPatternBoundaryGeometry', () => {
197
+ it.each([
198
+ ['vertical end', 'vertical', 'end', { x1: 10, y1: 60, x2: 110, y2: 60 }],
199
+ ['vertical start', 'vertical', 'start', { x1: 10, y1: 20, x2: 110, y2: 20 }],
200
+ ['horizontal end', 'horizontal', 'end', { x1: 10, y1: 20, x2: 10, y2: 100 }],
201
+ ['horizontal start', 'horizontal', 'start', { x1: 60, y1: 20, x2: 60, y2: 100 }]
202
+ ] as const)('calculates the %s internal divider', (_description, orientation, placement, expected) => {
203
+ expect(
204
+ getPortionPatternBoundaryGeometry({
205
+ orientation,
206
+ geometry: {
207
+ x: 10,
208
+ y: 20,
209
+ width: orientation === 'vertical' ? 100 : 50,
210
+ height: orientation === 'vertical' ? 40 : 80
211
+ },
212
+ overlay: { ratio: 0.5, placement }
213
+ })
214
+ ).toEqual(expected)
215
+ })
216
+
217
+ it('omits the divider when the portion covers the full bar', () => {
218
+ expect(
219
+ getPortionPatternBoundaryGeometry({
220
+ orientation: 'vertical',
221
+ geometry: { x: 10, y: 20, width: 100, height: 80 },
222
+ overlay: { ratio: 1, placement: 'end' }
223
+ })
224
+ ).toBeNull()
225
+ })
226
+ })
227
+
228
+ describe('getPortionPatternRenderData', () => {
229
+ it('returns a complete render model for supported valid portions', () => {
230
+ expect(
231
+ getPortionPatternRenderData({
232
+ config: {
233
+ visualizationType: 'Bar',
234
+ visualizationSubType: 'regular',
235
+ isLollipopChart: false,
236
+ yAxis: { type: 'linear' }
237
+ } as any,
238
+ orientation: 'vertical',
239
+ bounds: { x: 10, y: 20, width: 100, height: 80 },
240
+ patterns: {
241
+ Pattern1: {
242
+ application: 'portion',
243
+ dataKey: 'total',
244
+ patternValueKey: 'portion',
245
+ color: '#123456'
246
+ }
247
+ },
248
+ datum: { total: 100, portion: 25 },
249
+ seriesKey: 'total',
250
+ totalValue: 100
251
+ })
252
+ ).toEqual({
253
+ patternUrl: `url(#${getChartPatternId('Pattern1')})`,
254
+ ratio: 0.25,
255
+ placement: 'end',
256
+ patternColor: '#123456',
257
+ geometry: { x: 10, y: 20, width: 100, height: 20 },
258
+ boundaryGeometry: { x1: 10, y1: 40, x2: 110, y2: 40 }
259
+ })
260
+ })
261
+
262
+ it('returns no render model for unsupported configurations', () => {
263
+ expect(
264
+ getPortionPatternRenderData({
265
+ config: {
266
+ visualizationType: 'Bar',
267
+ visualizationSubType: 'stacked',
268
+ isLollipopChart: false,
269
+ yAxis: { type: 'linear' }
270
+ } as any,
271
+ orientation: 'vertical',
272
+ bounds: { x: 10, y: 20, width: 100, height: 80 },
273
+ patterns: {},
274
+ datum: {},
275
+ seriesKey: 'total',
276
+ totalValue: 100
277
+ })
278
+ ).toBeNull()
279
+ })
280
+ })
@@ -60,6 +60,10 @@ const makeConfigContext = (countColumnOverrides: Record<string, unknown> = {}) =
60
60
  ...baseConfig.runtime.yAxis,
61
61
  dataKey: 'Percentage'
62
62
  },
63
+ originalXAxis: {
64
+ ...baseConfig.runtime.originalXAxis,
65
+ dataKey: 'Cause of death'
66
+ },
63
67
  series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }],
64
68
  seriesKeys: ['Percentage'],
65
69
  seriesLabels: { Percentage: 'Percentage' },
@@ -80,6 +84,106 @@ const makeConfigContext = (countColumnOverrides: Record<string, unknown> = {}) =
80
84
  }
81
85
 
82
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
+
83
187
  it('does not apply the left-axis suffix to an additional single-series tooltip column', () => {
84
188
  const configContext = makeConfigContext()
85
189
  const { result } = renderHook(() => useBarChart(vi.fn(), vi.fn(), configContext))
@@ -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 { getSeriesColumnFormattingParams, 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,21 +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: '',
188
- addColSuffix: '',
189
- ...getSeriesColumnFormattingParams(config.columns[colKeys])
190
- }
174
+ const formattingParams = getAdditionalColumnFormattingParams(config.columns[colKeys])
191
175
 
192
176
  const formattedValue = formatColNumber(
193
177
  closestVal[config.columns[colKeys].name],
@@ -206,6 +190,22 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
206
190
  return additionalTooltipItems
207
191
  }
208
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
+
209
209
  const onMouseOverBar = (categoryValue, barKey, event, data, barValue) => {
210
210
  if (config.legend.highlightOnHover && config.legend.behavior === 'highlight' && barKey) {
211
211
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: [barKey] })
@@ -255,6 +255,7 @@ export const useBarChart = (handleTooltipMouseOver, handleTooltipMouseOff, confi
255
255
  barStackedSeriesKeys,
256
256
  hasMultipleSeries,
257
257
  labelFontSize,
258
+ formatTooltipValue,
258
259
  applyRadius,
259
260
  assignColorsToValues,
260
261
  getHighlightedBarColorByValue,