@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,207 @@
1
+ import { useMemo } 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 { createPlots } from '../components/BoxPlot/helpers'
8
+ import { getBoxPlotConfig } from '../helpers/getBoxPlotConfig'
9
+ import type { ChartConfig } from '../types/ChartConfig'
10
+
11
+ const expectedOutlier = '1000'
12
+ const expectedMetadataValues = '10,20,30,40,1000'
13
+
14
+ const boxPlotRows = [
15
+ { Group: 'Group A', Score: '10' },
16
+ { Group: 'Group A', Score: '20' },
17
+ { Group: 'Group A', Score: '30' },
18
+ { Group: 'Group A', Score: '40' },
19
+ { Group: 'Group A', Score: '1,000' }
20
+ ]
21
+
22
+ const boxPlotLabels = {
23
+ maximum: 'Maximum',
24
+ q3: 'Upper Quartile',
25
+ median: 'Median',
26
+ q1: 'Lower Quartile',
27
+ minimum: 'Minimum',
28
+ count: 'Count',
29
+ mean: 'Mean',
30
+ sd: 'Standard Deviation',
31
+ iqr: 'Interquartile Range',
32
+ outliers: 'Outliers',
33
+ values: 'Values',
34
+ lowerBounds: 'Lower Bounds',
35
+ upperBounds: 'Upper Bounds'
36
+ }
37
+
38
+ const getConfig = (): ChartConfig =>
39
+ ({
40
+ version: '4.26.8',
41
+ type: 'chart',
42
+ visualizationType: 'Box Plot',
43
+ title: 'Box Plot Stats Parity Regression',
44
+ orientation: 'vertical',
45
+ animate: false,
46
+ data: boxPlotRows,
47
+ table: {
48
+ show: true,
49
+ expanded: true,
50
+ download: false,
51
+ label: 'Data Table',
52
+ indexLabel: ''
53
+ },
54
+ xAxis: {
55
+ dataKey: 'Group',
56
+ type: 'categorical',
57
+ label: 'Group'
58
+ },
59
+ yAxis: {
60
+ dataKey: 'Score',
61
+ label: 'Score',
62
+ hideAxis: false,
63
+ hideTicks: false,
64
+ gridLines: true,
65
+ numTicks: 5
66
+ },
67
+ series: [
68
+ {
69
+ dataKey: 'Score',
70
+ name: 'Score'
71
+ }
72
+ ],
73
+ boxplot: {
74
+ plots: [],
75
+ categories: [],
76
+ borders: 'true',
77
+ plotOutlierValues: true,
78
+ plotNonOutlierValues: true,
79
+ labels: boxPlotLabels
80
+ },
81
+ legend: {
82
+ hide: true
83
+ },
84
+ dataFormat: {
85
+ roundTo: 0,
86
+ commas: false
87
+ }
88
+ } as ChartConfig)
89
+
90
+ function BoxPlotStatsParityRegression() {
91
+ const config = useMemo(() => getConfig(), [])
92
+ const [metadataPlots] = getBoxPlotConfig(config, config.data)
93
+ const rendererPlots = createPlots(config.data, config)
94
+ const metadataPlot = metadataPlots[0]
95
+ const rendererPlot = rendererPlots[0]
96
+
97
+ const metadataValues = metadataPlot?.values?.join(',') || ''
98
+ const metadataOutliers = metadataPlot?.columnOutliers?.join(',') || '(none)'
99
+ const rendererAcceptedValues =
100
+ [...(rendererPlot?.columnNonOutliers?.Score || []), ...(rendererPlot?.columnOutliers?.Score || [])].join(',') ||
101
+ '(none)'
102
+ const rendererOutliers = rendererPlot?.columnOutliers?.Score?.join(',') || '(none)'
103
+ const hasStatsParity = metadataValues === rendererAcceptedValues && metadataOutliers === rendererOutliers
104
+
105
+ return (
106
+ <div
107
+ style={{
108
+ background: '#eef2f7',
109
+ display: 'grid',
110
+ gap: 16,
111
+ padding: 16
112
+ }}
113
+ >
114
+ <section
115
+ style={{
116
+ background: '#fff',
117
+ border: '1px solid #d9dfe7',
118
+ borderRadius: 6,
119
+ padding: 16
120
+ }}
121
+ >
122
+ <Chart config={config} isEditor={false} interactionLabel='boxplot-stats-parity-regression' />
123
+ </section>
124
+
125
+ <section
126
+ aria-label='Box plot stats parity diagnostic'
127
+ style={{
128
+ background: '#f7f8fa',
129
+ border: '1px solid #d9dfe7',
130
+ borderRadius: 6,
131
+ display: 'grid',
132
+ gap: 8,
133
+ padding: 12
134
+ }}
135
+ >
136
+ <div>
137
+ <strong>Input score values:</strong> <span>10,20,30,40,1,000</span>
138
+ </div>
139
+ <div>
140
+ <strong>Metadata values:</strong> <span data-testid='boxplot-metadata-values'>{metadataValues}</span>
141
+ </div>
142
+ <div>
143
+ <strong>Metadata outliers:</strong> <span data-testid='boxplot-metadata-outliers'>{metadataOutliers}</span>
144
+ </div>
145
+ <div>
146
+ <strong>Renderer accepted values:</strong>{' '}
147
+ <span data-testid='boxplot-renderer-values'>{rendererAcceptedValues}</span>
148
+ </div>
149
+ <div>
150
+ <strong>Renderer outliers:</strong> <span data-testid='boxplot-renderer-outliers'>{rendererOutliers}</span>
151
+ </div>
152
+ <div data-testid='boxplot-stats-parity-status'>
153
+ <strong>Status:</strong> {hasStatsParity ? 'Stats parity preserved' : 'Issue reproduced'}
154
+ </div>
155
+ </section>
156
+ </div>
157
+ )
158
+ }
159
+
160
+ const getBoxPlotTableValue = (canvasElement: HTMLElement, rowLabel: string) => {
161
+ const row = Array.from(canvasElement.querySelectorAll('tr')).find(tableRow => {
162
+ const firstCell = tableRow.querySelector('th, td')
163
+ return firstCell?.textContent?.trim() === rowLabel
164
+ })
165
+
166
+ return Array.from(row?.querySelectorAll('th, td') || [])
167
+ .slice(1)
168
+ .map(cell => cell.textContent?.trim())
169
+ .join('')
170
+ }
171
+
172
+ const meta: Meta<typeof BoxPlotStatsParityRegression> = {
173
+ title: 'Components/Templates/Chart/Box Plot Stats Parity Regression',
174
+ component: BoxPlotStatsParityRegression,
175
+ parameters: {
176
+ layout: 'fullscreen'
177
+ }
178
+ }
179
+
180
+ export default meta
181
+ type Story = StoryObj<typeof BoxPlotStatsParityRegression>
182
+
183
+ export const Formatted_Numeric_Values_Keep_Stats_Parity: Story = {
184
+ play: async ({ canvasElement }) => {
185
+ await assertVisualizationRendered(canvasElement)
186
+
187
+ await waitFor(() => {
188
+ expect(canvasElement.querySelector('[data-testid="boxplot-metadata-values"]')?.textContent).toBe(
189
+ expectedMetadataValues
190
+ )
191
+ expect(canvasElement.querySelector('[data-testid="boxplot-metadata-outliers"]')?.textContent).toBe(
192
+ expectedOutlier
193
+ )
194
+ expect(canvasElement.querySelector('[data-testid="boxplot-renderer-values"]')?.textContent).toBe(
195
+ expectedMetadataValues
196
+ )
197
+ expect(canvasElement.querySelector('[data-testid="boxplot-renderer-outliers"]')?.textContent).toBe(
198
+ expectedOutlier
199
+ )
200
+ expect(canvasElement.querySelector('[data-testid="boxplot-stats-parity-status"]')?.textContent).toContain(
201
+ 'Stats parity preserved'
202
+ )
203
+ expect(getBoxPlotTableValue(canvasElement, 'Values')).toBe(expectedMetadataValues)
204
+ expect(getBoxPlotTableValue(canvasElement, 'Outliers')).toBe(expectedOutlier)
205
+ })
206
+ }
207
+ }
@@ -0,0 +1,299 @@
1
+ import React, { useMemo, useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
4
+ import {
5
+ chartV21ColorDistribution,
6
+ colorblindColorDistribution,
7
+ divergentColorDistribution,
8
+ qualitativeStandardColorDistribution,
9
+ v2ColorDistribution
10
+ } from '@cdc/core/helpers/palettes/colorDistributions'
11
+ import Chart from '../CdcChart'
12
+ import type { ChartConfig } from '../types/ChartConfig'
13
+ import BarChartConfig from './_mock/editor-tests/bar-chart-test.json'
14
+ import PieChartConfig from './_mock/pie_custom_colors.json'
15
+
16
+ const meta: Meta<typeof Chart> = {
17
+ title: 'Components/Templates/Chart/Color Distribution',
18
+ component: Chart,
19
+ parameters: {
20
+ layout: 'fullscreen'
21
+ }
22
+ }
23
+
24
+ export default meta
25
+
26
+ type Story = StoryObj<typeof Chart>
27
+
28
+ const ITEM_OPTIONS = Array.from({ length: 9 }, (_, index) => index + 1)
29
+ const PALETTE_GROUPS = [
30
+ {
31
+ label: 'Sequential',
32
+ options: [
33
+ { label: 'Blue', value: 'sequential_blue' },
34
+ { label: 'Teal', value: 'sequential_teal' },
35
+ { label: 'Purple', value: 'sequential_purple' },
36
+ { label: 'Orange', value: 'sequential_orange' },
37
+ { label: 'Green', value: 'sequential_green' }
38
+ ]
39
+ },
40
+ {
41
+ label: 'Divergent',
42
+ options: [
43
+ { label: 'Blue–Cyan', value: 'divergent_blue_cyan' },
44
+ { label: 'Blue–Purple', value: 'divergent_blue_purple' },
45
+ { label: 'Green–Orange', value: 'divergent_green_orange' },
46
+ { label: 'Blue–Orange', value: 'divergent_blue_orange' }
47
+ ]
48
+ },
49
+ {
50
+ label: 'Colorblind Safe',
51
+ options: [{ label: 'Qualitative Standard', value: 'qualitative_standard' }]
52
+ }
53
+ ] as const
54
+
55
+ type Palette = (typeof PALETTE_GROUPS)[number]['options'][number]['value']
56
+ type PaletteVersion = '2.0' | '2.1'
57
+ type ChartType = 'Bar' | 'Pie' | 'Line'
58
+
59
+ const getDistributionIndices = (itemCount: number, palette: Palette, paletteVersion: PaletteVersion): number[] => {
60
+ if (palette === 'qualitative_standard') {
61
+ const distribution = paletteVersion === '2.1' ? qualitativeStandardColorDistribution : colorblindColorDistribution
62
+ return distribution[itemCount]
63
+ }
64
+
65
+ if (palette.includes('divergent')) return divergentColorDistribution[itemCount]
66
+
67
+ const distribution = paletteVersion === '2.1' ? chartV21ColorDistribution : v2ColorDistribution
68
+ return distribution[itemCount]
69
+ }
70
+
71
+ const getDisplayedPaletteIndices = (
72
+ distributionIndices: number[],
73
+ paletteVersion: PaletteVersion,
74
+ isReversed: boolean
75
+ ): number[] => {
76
+ if (!isReversed) return distributionIndices
77
+
78
+ return paletteVersion === '2.1'
79
+ ? [...distributionIndices].reverse()
80
+ : distributionIndices.map(paletteIndex => 8 - paletteIndex)
81
+ }
82
+
83
+ const createCartesianConfig = (
84
+ chartType: Exclude<ChartType, 'Pie'>,
85
+ itemCount: number,
86
+ palette: Palette,
87
+ paletteVersion: PaletteVersion,
88
+ isReversed: boolean
89
+ ): ChartConfig => {
90
+ const config = cloneConfig(BarChartConfig) as ChartConfig
91
+ const distributionIndices = getDistributionIndices(itemCount, palette, paletteVersion)
92
+ const displayedPaletteIndices = getDisplayedPaletteIndices(distributionIndices, paletteVersion, isReversed)
93
+ const series = displayedPaletteIndices.map(paletteIndex => ({
94
+ axis: 'Left',
95
+ dataKey: `color_${paletteIndex + 1}`,
96
+ name: String(paletteIndex + 1),
97
+ tooltip: true,
98
+ type: chartType
99
+ }))
100
+
101
+ config.version = '4.26.8'
102
+ config.title = `${chartType} chart color distribution`
103
+ config.visualizationType = chartType
104
+ config.animate = false
105
+ config.barThickness = 0.8
106
+ config.general.palette = {
107
+ isReversed,
108
+ name: isReversed ? `${palette}reverse` : palette,
109
+ version: paletteVersion
110
+ }
111
+ config.series = series
112
+ config.data = Array.from({ length: chartType === 'Line' ? 4 : 1 }, (_, pointIndex) =>
113
+ series.reduce(
114
+ (row, item, seriesIndex) => ({
115
+ ...row,
116
+ [item.dataKey]: chartType === 'Line' ? 25 + seriesIndex * 6 + pointIndex * (seriesIndex % 2 ? -3 : 3) : 75
117
+ }),
118
+ { Category: chartType === 'Line' ? `Point ${pointIndex + 1}` : 'Items' }
119
+ )
120
+ )
121
+ config.xAxis.axisPadding = 20
122
+ config.xAxis.label = ''
123
+ config.yAxis.hideAxis = true
124
+ config.yAxis.hideLabel = true
125
+ config.yAxis.hideTicks = true
126
+ config.yAxis.label = ''
127
+ config.legend.position = 'top'
128
+ config.legend.style = chartType === 'Line' ? 'circles' : 'boxes'
129
+ config.table.show = false
130
+ config.visual.accent = false
131
+ config.visual.border = false
132
+
133
+ return config
134
+ }
135
+
136
+ const createPieConfig = (
137
+ sliceCount: number,
138
+ palette: Palette,
139
+ paletteVersion: PaletteVersion,
140
+ isReversed: boolean
141
+ ): ChartConfig => {
142
+ const config = cloneConfig(PieChartConfig) as ChartConfig
143
+ const data = Array.from({ length: sliceCount }, (_, index) => ({
144
+ [config.xAxis.dataKey]: `Slice ${index + 1}`,
145
+ [config.yAxis.dataKey]: 10 - index
146
+ }))
147
+
148
+ config.version = '4.26.8'
149
+ config.title = ''
150
+ config.showTitle = false
151
+ config.introText = ''
152
+ config.animate = false
153
+ config.general.palette = {
154
+ name: isReversed ? `${palette}reverse` : palette,
155
+ version: paletteVersion,
156
+ isReversed
157
+ }
158
+ config.data = data
159
+ config.formattedData = data
160
+ config.legend.hide = false
161
+ config.legend.label = ''
162
+ config.legend.description = ''
163
+ config.legend.position = 'top'
164
+ config.table.show = false
165
+ config.visual.accent = false
166
+ config.visual.border = false
167
+
168
+ return config
169
+ }
170
+
171
+ const ColorDistributionHarness = () => {
172
+ const [chartType, setChartType] = useState<ChartType>('Bar')
173
+ const [palette, setPalette] = useState<Palette>('qualitative_standard')
174
+ const [itemCount, setItemCount] = useState(3)
175
+ const [paletteVersion, setPaletteVersion] = useState<PaletteVersion>('2.0')
176
+ const [isReversed, setIsReversed] = useState(false)
177
+ const config = useMemo(
178
+ () =>
179
+ chartType === 'Pie'
180
+ ? createPieConfig(itemCount, palette, paletteVersion, isReversed)
181
+ : createCartesianConfig(chartType, itemCount, palette, paletteVersion, isReversed),
182
+ [chartType, itemCount, palette, paletteVersion, isReversed]
183
+ )
184
+
185
+ return (
186
+ <div style={{ padding: '1.5rem', maxWidth: 1100, margin: '0 auto' }}>
187
+ <div style={{ display: 'flex', flexWrap: 'nowrap', gap: '1rem', marginBottom: '1rem', overflowX: 'auto' }}>
188
+ <div style={{ flex: '0 0 auto' }}>
189
+ <label htmlFor='chart-color-distribution-type' style={{ display: 'block', fontWeight: 700, marginBottom: 4 }}>
190
+ Chart type
191
+ </label>
192
+ <select
193
+ id='chart-color-distribution-type'
194
+ value={chartType}
195
+ onChange={event => setChartType(event.target.value as ChartType)}
196
+ style={{ minWidth: 120 }}
197
+ >
198
+ <option value='Bar'>Bar chart</option>
199
+ <option value='Pie'>Pie chart</option>
200
+ <option value='Line'>Line chart</option>
201
+ </select>
202
+ </div>
203
+
204
+ <div style={{ flex: '0 0 auto' }}>
205
+ <label
206
+ htmlFor='chart-color-distribution-palette'
207
+ style={{ display: 'block', fontWeight: 700, marginBottom: 4 }}
208
+ >
209
+ Palette
210
+ </label>
211
+ <select
212
+ id='chart-color-distribution-palette'
213
+ value={palette}
214
+ onChange={event => setPalette(event.target.value as Palette)}
215
+ style={{ minWidth: 120 }}
216
+ >
217
+ {PALETTE_GROUPS.map(group => (
218
+ <optgroup key={group.label} label={group.label}>
219
+ {group.options.map(option => (
220
+ <option key={option.value} value={option.value}>
221
+ {option.label}
222
+ </option>
223
+ ))}
224
+ </optgroup>
225
+ ))}
226
+ </select>
227
+ </div>
228
+
229
+ <div style={{ flex: '0 0 auto' }}>
230
+ <label
231
+ htmlFor='chart-color-distribution-item-count'
232
+ style={{ display: 'block', fontWeight: 700, marginBottom: 4 }}
233
+ >
234
+ Number of items
235
+ </label>
236
+ <select
237
+ id='chart-color-distribution-item-count'
238
+ value={itemCount}
239
+ onChange={event => setItemCount(Number(event.target.value))}
240
+ style={{ minWidth: 120 }}
241
+ >
242
+ {ITEM_OPTIONS.map(value => (
243
+ <option key={value} value={value}>
244
+ {value}
245
+ </option>
246
+ ))}
247
+ </select>
248
+ </div>
249
+
250
+ <div style={{ flex: '0 0 auto' }}>
251
+ <label
252
+ htmlFor='chart-color-distribution-version'
253
+ style={{ display: 'block', fontWeight: 700, marginBottom: 4 }}
254
+ >
255
+ Palette version
256
+ </label>
257
+ <select
258
+ id='chart-color-distribution-version'
259
+ value={paletteVersion}
260
+ onChange={event => setPaletteVersion(event.target.value as PaletteVersion)}
261
+ style={{ minWidth: 120 }}
262
+ >
263
+ <option value='2.0'>2.0</option>
264
+ <option value='2.1'>2.1</option>
265
+ </select>
266
+ </div>
267
+
268
+ <div style={{ flex: '0 0 auto' }}>
269
+ <label
270
+ htmlFor='chart-color-distribution-reverse'
271
+ style={{ display: 'block', fontWeight: 700, marginBottom: 4 }}
272
+ >
273
+ Reversed
274
+ </label>
275
+ <select
276
+ id='chart-color-distribution-reverse'
277
+ value={isReversed ? 'yes' : 'no'}
278
+ onChange={event => setIsReversed(event.target.value === 'yes')}
279
+ style={{ minWidth: 120 }}
280
+ >
281
+ <option value='no'>No</option>
282
+ <option value='yes'>Yes</option>
283
+ </select>
284
+ </div>
285
+ </div>
286
+
287
+ <Chart
288
+ key={`${chartType}-${palette}-${itemCount}-${paletteVersion}-${isReversed}`}
289
+ config={config}
290
+ isEditor={false}
291
+ />
292
+ </div>
293
+ )
294
+ }
295
+
296
+ export const Color_Distribution: Story = {
297
+ name: 'Color Distribution',
298
+ render: () => <ColorDistributionHarness />
299
+ }
@@ -0,0 +1,147 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { expect, waitFor } from 'storybook/test'
4
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
5
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
6
+ import Chart from '../CdcChart'
7
+ import barChartStacked from './_mock/barchart_labels.mock.json'
8
+
9
+ const meta: Meta<typeof Chart> = {
10
+ title: 'Components/Templates/Chart/Data Cutoff',
11
+ component: Chart
12
+ }
13
+
14
+ type Story = StoryObj<typeof Chart>
15
+
16
+ const wastewaterVariantData = [
17
+ {
18
+ 'Week ending': '2024-08-03',
19
+ JN1: 84.16,
20
+ KP2: 9.84,
21
+ KP3: 5.52,
22
+ LB1: 0.99,
23
+ XEC: 0.12
24
+ },
25
+ {
26
+ 'Week ending': '2024-08-10',
27
+ JN1: 71.08,
28
+ KP2: 17.82,
29
+ KP3: 10.48,
30
+ LB1: 1,
31
+ XEC: 0.18
32
+ },
33
+ {
34
+ 'Week ending': '2024-08-17',
35
+ JN1: 61.22,
36
+ KP2: 22.95,
37
+ KP3: 14.86,
38
+ LB1: 1.04,
39
+ XEC: 0.26
40
+ }
41
+ ]
42
+
43
+ const variantSeries = [
44
+ { dataKey: 'JN1', name: 'JN.1', type: 'Bar', axis: 'Left', tooltip: true },
45
+ { dataKey: 'KP2', name: 'KP.2', type: 'Bar', axis: 'Left', tooltip: true },
46
+ { dataKey: 'KP3', name: 'KP.3', type: 'Bar', axis: 'Left', tooltip: true },
47
+ { dataKey: 'LB1', name: 'LB.1', type: 'Bar', axis: 'Left', tooltip: true },
48
+ { dataKey: 'XEC', name: 'XEC', type: 'Bar', axis: 'Left', tooltip: true }
49
+ ]
50
+
51
+ const wastewaterVariantDataCutoffConfig = editConfigKeys(barChartStacked, [
52
+ {
53
+ path: ['title'],
54
+ value: 'SARS-CoV-2 Variant Abundances in Triturator Wastewater Samples Over Time'
55
+ },
56
+ { path: ['showTitle'], value: true },
57
+ { path: ['visualizationType'], value: 'Bar' },
58
+ { path: ['visualizationSubType'], value: 'stacked' },
59
+ { path: ['orientation'], value: 'vertical' },
60
+ { path: ['dataCutoff'], value: '1' },
61
+ { path: ['dataFormat', 'preserveOriginalDecimals'], value: true },
62
+ { path: ['dataFormat', 'suffix'], value: '%' },
63
+ { path: ['xAxis', 'dataKey'], value: 'Week ending' },
64
+ { path: ['xAxis', 'label'], value: 'Week ending' },
65
+ { path: ['xAxis', 'type'], value: 'categorical' },
66
+ { path: ['xAxis', 'tickRotation'], value: 45 },
67
+ { path: ['yAxis', 'label'], value: 'Variant abundance (%)' },
68
+ { path: ['yAxis', 'min'], value: '0' },
69
+ { path: ['yAxis', 'max'], value: '100' },
70
+ { path: ['yAxis', 'numTicks'], value: '5' },
71
+ { path: ['legend', 'singleRow'], value: false },
72
+ { path: ['table', 'show'], value: true },
73
+ { path: ['table', 'expanded'], value: true },
74
+ { path: ['table', 'showVertical'], value: true },
75
+ { path: ['table', 'indexLabel'], value: 'Week ending' },
76
+ { path: ['series'], value: variantSeries },
77
+ {
78
+ path: ['columns'],
79
+ value: Object.fromEntries(
80
+ variantSeries.map((series, index) => [
81
+ series.dataKey,
82
+ {
83
+ name: series.dataKey,
84
+ label: series.name,
85
+ dataTable: true,
86
+ tooltips: true,
87
+ prefix: '',
88
+ suffix: '',
89
+ order: index + 2
90
+ }
91
+ ])
92
+ )
93
+ },
94
+ { path: ['data'], value: wastewaterVariantData },
95
+ { path: ['formattedData'], value: wastewaterVariantData }
96
+ ])
97
+
98
+ export const Wastewater_Variant_Abundance_Under_One_Percent: Story = {
99
+ args: {
100
+ config: wastewaterVariantDataCutoffConfig,
101
+ isEditor: false
102
+ },
103
+ parameters: {
104
+ docs: {
105
+ description: {
106
+ story:
107
+ 'Reproduces the data cutoff report for stacked SARS-CoV-2 wastewater variant abundances: sub-1% values such as 0.12, 0.26, and 0.99 render as <1% in the tooltip and data table when Data Cutoff is set to 1.'
108
+ }
109
+ }
110
+ },
111
+ play: async ({ canvasElement }) => {
112
+ await assertVisualizationRendered(canvasElement)
113
+
114
+ await waitFor(() => {
115
+ const tooltips = Array.from(canvasElement.querySelectorAll('[data-tooltip-html]')).map(
116
+ element => element.getAttribute('data-tooltip-html') || ''
117
+ )
118
+ const underOneTooltip = tooltips.find(
119
+ tooltip => tooltip.includes('Week ending: 2024-08-03') && tooltip.includes('LB.1: <1%')
120
+ )
121
+ const xecUnderOneTooltip = tooltips.find(
122
+ tooltip => tooltip.includes('Week ending: 2024-08-17') && tooltip.includes('XEC: <1%')
123
+ )
124
+ const exactCutoffTooltip = tooltips.find(
125
+ tooltip => tooltip.includes('Week ending: 2024-08-10') && tooltip.includes('LB.1: 1%')
126
+ )
127
+ const aboveCutoffTooltip = tooltips.find(
128
+ tooltip => tooltip.includes('Week ending: 2024-08-17') && tooltip.includes('LB.1: 1.04%')
129
+ )
130
+ const dataTable = canvasElement.querySelector('.data-table')
131
+ const tableRows = Array.from(dataTable?.querySelectorAll('tbody tr') || []).map(row =>
132
+ Array.from(row.querySelectorAll('th, td')).map(cell => cell.textContent?.trim())
133
+ )
134
+
135
+ expect(underOneTooltip).toBeDefined()
136
+ expect(xecUnderOneTooltip).toBeDefined()
137
+ expect(exactCutoffTooltip).toBeDefined()
138
+ expect(aboveCutoffTooltip).toBeDefined()
139
+ expect(dataTable).toBeInTheDocument()
140
+ expect(tableRows[0]).toEqual(['2024-08-03', '84.16%', '9.84%', '5.52%', '<1%', '<1%'])
141
+ expect(tableRows[1]).toEqual(['2024-08-10', '71.08%', '17.82%', '10.48%', '1%', '<1%'])
142
+ expect(tableRows[2]).toEqual(['2024-08-17', '61.22%', '22.95%', '14.86%', '1.04%', '<1%'])
143
+ })
144
+ }
145
+ }
146
+
147
+ export default meta