@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,311 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
3
+ import DynamicSeriesBarConfig from './_mock/dynamic_series_bar_config.json'
4
+ import SuppressedBarConfig from './_mock/bar-chart-suppressed.json'
5
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
6
+
7
+ import Chart from '../CdcChart'
8
+ import MinimalExampleConfig from '../../examples/minimal-example.json'
9
+
10
+ const meta: Meta<typeof Chart> = {
11
+ title: 'Components/Templates/Chart/Percentage Values',
12
+ component: Chart
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof Chart>
17
+
18
+ const percentageValueConfig = {
19
+ ...MinimalExampleConfig,
20
+ title: 'Mixed Percentage and Numeric Values',
21
+ dataFormat: {
22
+ ...MinimalExampleConfig.dataFormat,
23
+ roundTo: 1
24
+ },
25
+ data: [
26
+ { category: 'Percentage', value: '12.5%' },
27
+ { category: 'Count', value: 20 }
28
+ ]
29
+ }
30
+
31
+ const horizontalPercentageValueConfig = {
32
+ ...percentageValueConfig,
33
+ title: 'Horizontal Percentage Value',
34
+ orientation: 'horizontal'
35
+ }
36
+
37
+ const findTooltipPath = (canvasElement: HTMLElement, ...requiredText: string[]) =>
38
+ Array.from(canvasElement.querySelectorAll<SVGPathElement>('path[data-tooltip-html]')).find(path => {
39
+ const tooltip = path.getAttribute('data-tooltip-html') || ''
40
+
41
+ return requiredText.every(text => tooltip.includes(text))
42
+ })
43
+
44
+ const getPercentageBar = (canvasElement: HTMLElement) => findTooltipPath(canvasElement, 'Percentage')
45
+
46
+ const stackedPercentageValueConfig = {
47
+ ...MinimalExampleConfig,
48
+ title: 'Stacked Percentage Value',
49
+ visualizationSubType: 'stacked',
50
+ orientation: 'vertical',
51
+ dataFormat: {
52
+ ...MinimalExampleConfig.dataFormat,
53
+ roundTo: 1
54
+ },
55
+ data: [
56
+ { category: 'Stack A', rate: '12.5%', count: 20 },
57
+ { category: 'Stack B', rate: 5, count: 10 }
58
+ ],
59
+ series: [
60
+ { dataKey: 'rate', name: 'Rate', type: 'Bar', axis: 'Left', tooltip: true },
61
+ { dataKey: 'count', name: 'Count', type: 'Bar', axis: 'Left', tooltip: true }
62
+ ]
63
+ }
64
+
65
+ const stackedHorizontalPercentageValueConfig = {
66
+ ...stackedPercentageValueConfig,
67
+ title: 'Stacked Horizontal Percentage Value',
68
+ orientation: 'horizontal'
69
+ }
70
+
71
+ const dynamicPercentageData = DynamicSeriesBarConfig.data.map(row =>
72
+ row.YearStart === 2015 && row.Stratification1 === '65 years or older' ? { ...row, Data_Value: '56.4%' } : row
73
+ )
74
+
75
+ const dynamicPercentageValueConfig = {
76
+ ...DynamicSeriesBarConfig,
77
+ title: 'Dynamic Series Percentage Value',
78
+ dataFormat: {
79
+ ...DynamicSeriesBarConfig.dataFormat,
80
+ roundTo: 1
81
+ },
82
+ data: dynamicPercentageData,
83
+ formattedData: dynamicPercentageData
84
+ }
85
+
86
+ const suppressionPercentageValueConfig = {
87
+ ...SuppressedBarConfig,
88
+ data: SuppressedBarConfig.data.map((row, index) => (index === 1 ? { ...row, 'Data 2': '110%' } : row))
89
+ }
90
+
91
+ const suppressionConfidenceData = [
92
+ { Race_Ethnicity: 'American Indian or Alaska Native', Data_Value: '73.1', CI_Lower: '70.4', CI_Upper: '75.8' },
93
+ { Race_Ethnicity: 'Asian', Data_Value: '77.2', CI_Lower: '75.3', CI_Upper: '79.1' },
94
+ { Race_Ethnicity: 'Black or African American', Data_Value: '68.4', CI_Lower: '66.7', CI_Upper: '70.1' },
95
+ { Race_Ethnicity: 'Hispanic or Latino', Data_Value: '71.6', CI_Lower: '70.2', CI_Upper: '73.0' },
96
+ { Race_Ethnicity: 'More than one race', Data_Value: '72.8', CI_Lower: '70.5', CI_Upper: '75.1' },
97
+ { Race_Ethnicity: 'White', Data_Value: '76.4', CI_Lower: '75.8', CI_Upper: '77.0' },
98
+ {
99
+ Race_Ethnicity: 'NHOPI',
100
+ Data_Value: 'Suppressed',
101
+ CI_Lower: 'Suppressed',
102
+ CI_Upper: 'Suppressed'
103
+ }
104
+ ]
105
+
106
+ const wildcardSuppressionConfidenceConfig = {
107
+ ...SuppressedBarConfig,
108
+ title: 'Wildcard Suppression with Confidence Intervals',
109
+ general: {
110
+ ...SuppressedBarConfig.general,
111
+ showMissingDataLabel: false,
112
+ showSuppressedSymbol: true,
113
+ showZeroValueData: false
114
+ },
115
+ xAxis: {
116
+ ...SuppressedBarConfig.xAxis,
117
+ dataKey: 'Race_Ethnicity',
118
+ type: 'categorical'
119
+ },
120
+ confidenceKeys: {
121
+ lower: 'CI_Lower',
122
+ upper: 'CI_Upper'
123
+ },
124
+ preliminaryData: [
125
+ {
126
+ ...SuppressedBarConfig.preliminaryData[0],
127
+ column: '',
128
+ value: 'Suppressed',
129
+ hideBarSymbol: true
130
+ }
131
+ ],
132
+ series: [{ ...SuppressedBarConfig.series[0], dataKey: 'Data_Value' }],
133
+ data: suppressionConfidenceData
134
+ }
135
+
136
+ const dynamicSuppressionCategory = '65 years or older'
137
+ const dynamicSuppressionData = DynamicSeriesBarConfig.data.map(row =>
138
+ row.YearStart === 2015 && row.Stratification1 === dynamicSuppressionCategory ? { ...row, Data_Value: 'ABC' } : row
139
+ )
140
+ const dynamicSuppressionConfig = {
141
+ ...DynamicSeriesBarConfig,
142
+ data: dynamicSuppressionData,
143
+ formattedData: dynamicSuppressionData,
144
+ preliminaryData: [
145
+ {
146
+ ...SuppressedBarConfig.preliminaryData[0],
147
+ column: dynamicSuppressionCategory,
148
+ value: 'ABC'
149
+ }
150
+ ]
151
+ }
152
+
153
+ export const Percent_Decorated_Value_Renders_As_Bar_And_Tooltip: Story = {
154
+ args: {
155
+ config: percentageValueConfig
156
+ },
157
+ play: async ({ canvasElement }) => {
158
+ await assertVisualizationRendered(canvasElement)
159
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
160
+
161
+ const percentageBar = getPercentageBar(canvasElement)
162
+
163
+ expect(percentageBar).toBeDefined()
164
+ expect(percentageBar?.closest('g[display]')).toHaveAttribute('display', 'block')
165
+ expect(percentageBar?.getBBox().height).toBeGreaterThan(3)
166
+ expect(percentageBar?.getAttribute('data-tooltip-html')).toContain('12.5%')
167
+ }
168
+ }
169
+
170
+ export const Dynamic_Percent_Decoration_Is_Preserved_In_Tooltip: Story = {
171
+ args: {
172
+ config: dynamicPercentageValueConfig
173
+ },
174
+ play: async ({ canvasElement }) => {
175
+ await assertVisualizationRendered(canvasElement)
176
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
177
+
178
+ const dynamicPercentageBar = findTooltipPath(canvasElement, '2015', '65 years or older')
179
+
180
+ expect(dynamicPercentageBar).toBeDefined()
181
+ expect(dynamicPercentageBar?.closest('g[display]')).toHaveAttribute('display', 'block')
182
+ expect(dynamicPercentageBar?.getAttribute('data-tooltip-html')).toContain('56.4%')
183
+ }
184
+ }
185
+
186
+ export const Suppression_Config_Does_Not_Hide_Percentage_Bars: Story = {
187
+ args: {
188
+ config: suppressionPercentageValueConfig
189
+ },
190
+ play: async ({ canvasElement }) => {
191
+ await assertVisualizationRendered(canvasElement)
192
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
193
+
194
+ const percentageBar = findTooltipPath(canvasElement, '110%')
195
+ const suppressedBarGroup = Array.from(canvasElement.querySelectorAll<SVGGElement>('g[display="block"]')).find(
196
+ group => group.textContent?.includes('†') && group.querySelector('path[data-tooltip-html]')
197
+ )
198
+
199
+ expect(percentageBar).toBeDefined()
200
+ expect(percentageBar?.closest('g[display]')).toHaveAttribute('display', 'block')
201
+ expect(percentageBar?.getBBox().height).toBeGreaterThan(3)
202
+ expect(suppressedBarGroup).toBeDefined()
203
+ }
204
+ }
205
+
206
+ export const Wildcard_Suppression_Does_Not_Corrupt_Confidence_Domain: Story = {
207
+ args: {
208
+ config: wildcardSuppressionConfidenceConfig
209
+ },
210
+ play: async ({ canvasElement }) => {
211
+ await assertVisualizationRendered(canvasElement)
212
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
213
+ await waitForPresence('path[stroke-width="px"]', canvasElement)
214
+
215
+ const visibleNumericBars = Array.from(
216
+ canvasElement.querySelectorAll<SVGPathElement>('path[data-tooltip-html]')
217
+ ).filter(path => path.closest('g[display]')?.getAttribute('display') === 'block')
218
+ const visibleConfidenceIntervals = Array.from(
219
+ canvasElement.querySelectorAll<SVGPathElement>('path[stroke-width="px"]')
220
+ ).filter(path => path.closest('g[display]')?.getAttribute('display') === 'block')
221
+ const suppressedBar = findTooltipPath(canvasElement, 'NHOPI')
222
+ const suppressedBarGroup = suppressedBar?.closest('g[display]')
223
+
224
+ expect(visibleNumericBars).toHaveLength(6)
225
+ visibleNumericBars.forEach(bar => {
226
+ expect(bar.getAttribute('d')).not.toContain('NaN')
227
+ expect(bar.getBBox().height).toBeGreaterThan(3)
228
+ })
229
+
230
+ expect(visibleConfidenceIntervals).toHaveLength(6)
231
+ visibleConfidenceIntervals.forEach(path => {
232
+ const pathData = path.getAttribute('d') || ''
233
+ const coordinates = pathData.match(/-?\d+(?:\.\d+)?/g)?.map(Number)
234
+
235
+ expect(pathData).not.toMatch(/NaN|Infinity/)
236
+ expect(coordinates?.length).toBeGreaterThan(0)
237
+ expect(coordinates?.every(Number.isFinite)).toBe(true)
238
+ })
239
+
240
+ expect(suppressedBar).toBeDefined()
241
+ expect(suppressedBarGroup).toHaveAttribute('display', 'none')
242
+ expect(suppressedBarGroup?.querySelector('rect[data-tooltip-html]')).toBeTruthy()
243
+ expect(suppressedBarGroup).not.toHaveTextContent('†')
244
+ }
245
+ }
246
+
247
+ export const Dynamic_Suppression_Target_Is_Preserved: Story = {
248
+ args: {
249
+ config: dynamicSuppressionConfig
250
+ },
251
+ play: async ({ canvasElement }) => {
252
+ await assertVisualizationRendered(canvasElement)
253
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
254
+
255
+ const suppressedBar = findTooltipPath(canvasElement, '2015', dynamicSuppressionCategory)
256
+ const suppressedBarGroup = suppressedBar?.closest('g[display]')
257
+
258
+ expect(suppressedBar).toBeDefined()
259
+ expect(suppressedBarGroup).toHaveAttribute('display', 'block')
260
+ expect(suppressedBarGroup).toHaveTextContent('†')
261
+ }
262
+ }
263
+
264
+ export const Horizontal_Percent_Decoration_Is_Preserved_In_Tooltip: Story = {
265
+ args: {
266
+ config: horizontalPercentageValueConfig
267
+ },
268
+ play: async ({ canvasElement }) => {
269
+ await assertVisualizationRendered(canvasElement)
270
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
271
+
272
+ const percentageBar = getPercentageBar(canvasElement)
273
+
274
+ expect(percentageBar).toBeDefined()
275
+ expect(percentageBar?.closest('g[display]')).toHaveAttribute('display', 'block')
276
+ expect(percentageBar?.getBBox().width).toBeGreaterThanOrEqual(3)
277
+ expect(percentageBar?.getAttribute('data-tooltip-html')).toContain('12.5%')
278
+ }
279
+ }
280
+
281
+ export const Stacked_Vertical_Percent_Decoration_Is_Preserved_In_Tooltip: Story = {
282
+ args: {
283
+ config: stackedPercentageValueConfig
284
+ },
285
+ play: async ({ canvasElement }) => {
286
+ await assertVisualizationRendered(canvasElement)
287
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
288
+
289
+ const percentageSegment = findTooltipPath(canvasElement, 'Stack A', 'Rate:')
290
+
291
+ expect(percentageSegment).toBeDefined()
292
+ expect(percentageSegment).toHaveStyle({ display: 'block' })
293
+ expect(percentageSegment?.getAttribute('data-tooltip-html')).toContain('12.5%')
294
+ }
295
+ }
296
+
297
+ export const Stacked_Horizontal_Percent_Decoration_Is_Preserved_In_Tooltip: Story = {
298
+ args: {
299
+ config: stackedHorizontalPercentageValueConfig
300
+ },
301
+ play: async ({ canvasElement }) => {
302
+ await assertVisualizationRendered(canvasElement)
303
+ await waitForPresence('path[data-tooltip-html]', canvasElement)
304
+
305
+ const percentageSegment = findTooltipPath(canvasElement, 'Stack A', 'Rate:')
306
+
307
+ expect(percentageSegment).toBeDefined()
308
+ expect(percentageSegment).toHaveStyle({ display: 'block' })
309
+ expect(percentageSegment?.getAttribute('data-tooltip-html')).toContain('12.5%')
310
+ }
311
+ }
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
- import { expect } from 'storybook/test'
2
+ import { expect, within } from 'storybook/test'
3
3
  import Chart from '../CdcChart'
4
4
  import pieConfig from './_mock/pie_with_data.json'
5
5
  import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
6
- import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
6
+ import { assertVisualizationRendered, waitForEditor, waitForPresence } from '@cdc/core/helpers/testing'
7
7
 
8
8
  const meta: Meta<typeof Chart> = {
9
9
  title: 'Components/Templates/Chart/Pie Labels',
@@ -21,6 +21,7 @@ export const CategoryPercentageLabels: Story = {
21
21
  isEditor: true
22
22
  },
23
23
  play: async ({ canvasElement }) => {
24
+ await waitForEditor(within(canvasElement))
24
25
  await assertVisualizationRendered(canvasElement)
25
26
  await waitForPresence('.pie-label', canvasElement)
26
27
  expect(canvasElement.querySelector('line')).toBeNull()
@@ -37,6 +38,7 @@ export const DonutCategoryPercentageLabels: Story = {
37
38
  isEditor: true
38
39
  },
39
40
  play: async ({ canvasElement }) => {
41
+ await waitForEditor(within(canvasElement))
40
42
  await assertVisualizationRendered(canvasElement)
41
43
  await waitForPresence('.pie-label--outside', canvasElement)
42
44
  expect(canvasElement.querySelector('.pie-label--inside')).toBeNull()
@@ -0,0 +1,206 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { expect, waitFor } from 'storybook/test'
4
+
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+ import Chart from '../CdcChartComponent'
7
+ import { ChartConfig } from '../types/ChartConfig'
8
+
9
+ const meta: Meta<typeof RankByValueMutationHarness> = {
10
+ title: 'Components/Templates/Chart/Rank By Value Mutation Regression',
11
+ component: RankByValueMutationHarness,
12
+ parameters: {
13
+ layout: 'fullscreen'
14
+ }
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof RankByValueMutationHarness>
19
+
20
+ type RankRow = {
21
+ category: string
22
+ value: number
23
+ }
24
+
25
+ const sourceOrder = 'Alpha, Charlie, Bravo'
26
+ const rankedOrder = 'Charlie, Bravo, Alpha'
27
+ const rankedExcludingAlphaOrder = 'Charlie, Bravo'
28
+
29
+ const getOrder = (rows: RankRow[]) => rows.map(row => row.category).join(', ')
30
+
31
+ const getRenderedCategoryOrder = (panel: Element) => {
32
+ const categoryNames = sourceOrder.split(', ')
33
+ return Array.from(panel.querySelectorAll('svg text'))
34
+ .filter(textElement => categoryNames.includes(textElement.textContent?.trim() || ''))
35
+ .sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left)
36
+ .map(textElement => textElement.textContent?.trim())
37
+ .join(', ')
38
+ }
39
+
40
+ const getConfig = (
41
+ title: string,
42
+ data: RankRow[],
43
+ rankByValue?: 'asc' | 'desc',
44
+ excludedCategories: string[] = []
45
+ ): ChartConfig =>
46
+ ({
47
+ version: '4.26.8',
48
+ type: 'chart',
49
+ visualizationType: 'Bar',
50
+ title,
51
+ rankByValue,
52
+ data,
53
+ animate: false,
54
+ legend: {
55
+ hide: true
56
+ },
57
+ table: {
58
+ show: false
59
+ },
60
+ xAxis: {
61
+ dataKey: 'category',
62
+ type: 'categorical',
63
+ label: 'Category'
64
+ },
65
+ yAxis: {
66
+ label: 'Value',
67
+ hideAxis: false,
68
+ hideTicks: false,
69
+ gridLines: true,
70
+ numTicks: 4
71
+ },
72
+ series: [
73
+ {
74
+ dataKey: 'value',
75
+ name: 'Value',
76
+ type: 'Bar'
77
+ }
78
+ ],
79
+ dataFormat: {
80
+ roundTo: 0,
81
+ commas: false
82
+ },
83
+ exclusions: {
84
+ active: excludedCategories.length > 0,
85
+ keys: excludedCategories
86
+ }
87
+ } as ChartConfig)
88
+
89
+ const panelStyle = {
90
+ background: '#fff',
91
+ border: '1px solid #d9dfe7',
92
+ borderRadius: 6,
93
+ minWidth: 0,
94
+ padding: 16
95
+ }
96
+
97
+ const labelStyle = {
98
+ color: '#4b5563',
99
+ fontSize: 12,
100
+ fontWeight: 700,
101
+ margin: '0 0 8px',
102
+ textTransform: 'uppercase' as const
103
+ }
104
+
105
+ const diagnosticStyle = {
106
+ background: '#f7f8fa',
107
+ border: '1px solid #d9dfe7',
108
+ borderRadius: 6,
109
+ display: 'grid',
110
+ gap: 8,
111
+ gridColumn: '1 / -1',
112
+ padding: 12
113
+ }
114
+
115
+ function RankByValueMutationHarness() {
116
+ const sharedRows = useMemo<RankRow[]>(
117
+ () => [
118
+ { category: 'Alpha', value: 30 },
119
+ { category: 'Charlie', value: 10 },
120
+ { category: 'Bravo', value: 20 }
121
+ ],
122
+ []
123
+ )
124
+
125
+ const [observedOrder, setObservedOrder] = useState<string>('Checking shared data order...')
126
+
127
+ useEffect(() => {
128
+ const timeoutId = window.setTimeout(() => {
129
+ setObservedOrder(getOrder(sharedRows))
130
+ }, 100)
131
+
132
+ return () => window.clearTimeout(timeoutId)
133
+ }, [sharedRows])
134
+
135
+ const rankedConfig = useMemo(() => getConfig('Ranked by value ascending', sharedRows, 'asc'), [sharedRows])
136
+ const rankedExclusionConfig = useMemo(
137
+ () => getConfig('Ranked by value ascending with Alpha excluded', sharedRows, 'asc', ['Alpha']),
138
+ [sharedRows]
139
+ )
140
+ const sourceOrderConfig = useMemo(() => getConfig('Should preserve source order', sharedRows), [sharedRows])
141
+ const isChecking = observedOrder === 'Checking shared data order...'
142
+ const hasMutated = !isChecking && observedOrder !== sourceOrder
143
+
144
+ return (
145
+ <div
146
+ style={{
147
+ background: '#eef2f7',
148
+ display: 'grid',
149
+ gap: 16,
150
+ gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))',
151
+ padding: 16
152
+ }}
153
+ >
154
+ <section style={panelStyle} data-rank-by-value-panel='ranked'>
155
+ <p style={labelStyle}>Ranked chart</p>
156
+ <Chart config={rankedConfig} isEditor={false} interactionLabel='' />
157
+ </section>
158
+
159
+ <section style={panelStyle} data-rank-by-value-panel='source-order'>
160
+ <p style={labelStyle}>Source-order chart sharing the same data</p>
161
+ <Chart config={sourceOrderConfig} isEditor={false} interactionLabel='' />
162
+ </section>
163
+
164
+ <section style={panelStyle} data-rank-by-value-panel='ranked-exclusion'>
165
+ <p style={labelStyle}>Ranked chart with Alpha excluded</p>
166
+ <Chart config={rankedExclusionConfig} isEditor={false} interactionLabel='' />
167
+ </section>
168
+
169
+ <section style={diagnosticStyle} aria-label='Shared data order diagnostic'>
170
+ <div>
171
+ <strong>Expected shared data order:</strong> <span>{sourceOrder}</span>
172
+ </div>
173
+ <div>
174
+ <strong>Observed shared data order:</strong>{' '}
175
+ <span data-testid='rank-by-value-observed-order'>{observedOrder}</span>
176
+ </div>
177
+ <div data-testid='rank-by-value-mutation-status'>
178
+ <strong>Status:</strong> {hasMutated ? 'Shared data was mutated' : 'Shared data order is preserved'}
179
+ </div>
180
+ </section>
181
+ </div>
182
+ )
183
+ }
184
+
185
+ export const Shared_Data_Order_Is_Preserved: Story = {
186
+ play: async ({ canvasElement }) => {
187
+ await assertVisualizationRendered(canvasElement)
188
+
189
+ await waitFor(() => {
190
+ const rankedPanel = canvasElement.querySelector('[data-rank-by-value-panel="ranked"]')
191
+ const sourceOrderPanel = canvasElement.querySelector('[data-rank-by-value-panel="source-order"]')
192
+ const rankedExclusionPanel = canvasElement.querySelector('[data-rank-by-value-panel="ranked-exclusion"]')
193
+
194
+ expect(rankedPanel?.querySelector('svg')).toBeTruthy()
195
+ expect(sourceOrderPanel?.querySelector('svg')).toBeTruthy()
196
+ expect(rankedExclusionPanel?.querySelector('svg')).toBeTruthy()
197
+ expect(rankedPanel ? getRenderedCategoryOrder(rankedPanel) : '').toBe(rankedOrder)
198
+ expect(sourceOrderPanel ? getRenderedCategoryOrder(sourceOrderPanel) : '').toBe(sourceOrder)
199
+ expect(rankedExclusionPanel ? getRenderedCategoryOrder(rankedExclusionPanel) : '').toBe(rankedExcludingAlphaOrder)
200
+ expect(canvasElement.querySelector('[data-testid="rank-by-value-observed-order"]')?.textContent).toBe(sourceOrder)
201
+ expect(canvasElement.querySelector('[data-testid="rank-by-value-mutation-status"]')?.textContent).toContain(
202
+ 'Shared data order is preserved'
203
+ )
204
+ })
205
+ }
206
+ }