@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,417 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, userEvent, within } from 'storybook/test'
3
+ import Chart from '../CdcChartComponent'
4
+ import sankeyConfig from './_mock/sankey_mock.json'
5
+ import {
6
+ assertVisualizationRendered,
7
+ openAccordion,
8
+ performAndAssert,
9
+ waitForEditor,
10
+ waitForOptionsToPopulate,
11
+ waitForPresence
12
+ } from '@cdc/core/helpers/testing'
13
+
14
+ const tabularSankeyRows = sankeyConfig.data
15
+ const expectedPaletteColors = {
16
+ node: '#032659',
17
+ link: '#dbe8f7'
18
+ }
19
+
20
+ const normalizeColor = (color: string | null) => color?.toLowerCase() || ''
21
+
22
+ const longLabelRows = [
23
+ {
24
+ source: 'Public health program referral source with a long label',
25
+ target: 'Eligibility review and documentation step',
26
+ value: '1,200'
27
+ },
28
+ {
29
+ source: 'Public health program referral source with a long label',
30
+ target: 'Not eligible after review',
31
+ value: 240
32
+ },
33
+ {
34
+ source: 'Eligibility review and documentation step',
35
+ target: 'Enrolled in follow up services',
36
+ value: 820
37
+ },
38
+ {
39
+ source: 'Eligibility review and documentation step',
40
+ target: 'Declined services after review',
41
+ value: 140
42
+ },
43
+ {
44
+ source: 'Ignored zero value',
45
+ target: 'Should not render',
46
+ value: 0
47
+ },
48
+ {
49
+ source: 'Ignored invalid value',
50
+ target: 'Should not render',
51
+ value: 'not available'
52
+ }
53
+ ]
54
+
55
+ const mappedColumnRows = [
56
+ { Step: 'Intake', Next: 'Eligible', People: 12 },
57
+ { Step: 'Eligible', Next: 'Enrolled', People: 9 },
58
+ { Step: 'Eligible', Next: 'Declined', People: 3 }
59
+ ]
60
+
61
+ const assertSankeyRendered = async (
62
+ canvasElement: HTMLElement,
63
+ expectedRejectedRows = 0,
64
+ expectedScrollable = false,
65
+ expectedLabelSides: Record<string, 'left' | 'right'> = {},
66
+ expectedColors?: { node: string; link: string }
67
+ ) => {
68
+ await assertVisualizationRendered(canvasElement)
69
+
70
+ const svg = (await waitForPresence('.sankey-chart__diagram', canvasElement)) as SVGElement
71
+ const scrollArea = (await waitForPresence('.sankey-chart__scroll-area', canvasElement)) as HTMLElement
72
+ const nodes = Array.from(svg.querySelectorAll('.sankey-chart__node'))
73
+ const links = Array.from(svg.querySelectorAll('.sankey-chart__link'))
74
+ const labels = Array.from(svg.querySelectorAll('.sankey-chart__label'))
75
+ const svgRect = svg.getBoundingClientRect()
76
+ const scrollAreaRect = scrollArea.getBoundingClientRect()
77
+
78
+ expect(nodes.length).toBeGreaterThan(0)
79
+ expect(links.length).toBeGreaterThan(0)
80
+ expect(labels.length).toBe(nodes.length)
81
+ expect(svgRect.width).toBeGreaterThan(250)
82
+ expect(svgRect.height).toBeGreaterThan(300)
83
+ expect(Number(svg.getAttribute('data-rejected-row-count'))).toBe(expectedRejectedRows)
84
+
85
+ if (expectedScrollable) {
86
+ expect(scrollArea.scrollWidth).toBeGreaterThan(scrollArea.clientWidth)
87
+ expect(svgRect.width).toBeGreaterThan(scrollAreaRect.width)
88
+ } else {
89
+ expect(scrollArea.scrollWidth).toBeLessThanOrEqual(scrollArea.clientWidth + 1)
90
+ }
91
+
92
+ labels.forEach(label => {
93
+ const labelRect = label.getBoundingClientRect()
94
+ expect(labelRect.width).toBeGreaterThan(0)
95
+ expect(labelRect.left).toBeGreaterThanOrEqual(svgRect.left - 1)
96
+ expect(labelRect.right).toBeLessThanOrEqual(svgRect.right + 1)
97
+ })
98
+
99
+ Object.entries(expectedLabelSides).forEach(([nodeId, side]) => {
100
+ const label = labels.find(label => label.getAttribute('data-node-id') === nodeId)
101
+ expect(label?.getAttribute('data-label-side')).toBe(side)
102
+ })
103
+
104
+ if (expectedColors) {
105
+ nodes.forEach(node => {
106
+ expect(normalizeColor(node.getAttribute('fill'))).toBe(expectedColors.node)
107
+ })
108
+ links.forEach(link => {
109
+ expect(normalizeColor(link.getAttribute('stroke'))).toBe(expectedColors.link)
110
+ })
111
+ }
112
+ }
113
+
114
+ const expectedTabularLabelSides = {
115
+ Screened: 'left',
116
+ Eligible: 'right',
117
+ Enrolled: 'right',
118
+ 'Completed Treatment': 'right',
119
+ Ineligible: 'right',
120
+ Declined: 'right',
121
+ 'Dropped Out': 'right',
122
+ Recovered: 'right',
123
+ 'Ongoing Care': 'right'
124
+ } as const
125
+
126
+ const getSankeyNodeIds = (canvasElement: HTMLElement) =>
127
+ Array.from(canvasElement.querySelectorAll('.sankey-chart__node'))
128
+ .map(node => node.getAttribute('data-node-id') || '')
129
+ .filter(Boolean)
130
+ .sort()
131
+
132
+ const getSankeyTooltipAttributeCount = (canvasElement: HTMLElement) =>
133
+ Array.from(canvasElement.querySelectorAll('.sankey-chart__node, .sankey-chart__link')).filter(element =>
134
+ element.getAttribute('data-tooltip-html')
135
+ ).length
136
+
137
+ const getSankeyHighlightState = (canvasElement: HTMLElement) => {
138
+ const nodeOpacity = Object.fromEntries(
139
+ Array.from(canvasElement.querySelectorAll('.sankey-chart__node')).map(node => [
140
+ node.getAttribute('data-node-id') || '',
141
+ node.getAttribute('fill-opacity') || ''
142
+ ])
143
+ )
144
+ const linkOpacity = Object.fromEntries(
145
+ Array.from(canvasElement.querySelectorAll('.sankey-chart__link')).map(link => [
146
+ `${link.getAttribute('data-link-source-id')}->${link.getAttribute('data-link-target-id')}`,
147
+ link.getAttribute('stroke-opacity') || ''
148
+ ])
149
+ )
150
+
151
+ return { linkOpacity, nodeOpacity }
152
+ }
153
+
154
+ const meta: Meta<typeof Chart> = {
155
+ title: 'Components/Templates/Chart/Sankey',
156
+ component: Chart
157
+ }
158
+
159
+ export default meta
160
+ type Story = StoryObj<typeof Chart>
161
+
162
+ export const Sankey_TabularImport: Story = {
163
+ args: {
164
+ config: {
165
+ ...sankeyConfig,
166
+ title: 'Patient Care Pathway',
167
+ data: tabularSankeyRows
168
+ },
169
+ isEditor: false
170
+ },
171
+ play: async ({ canvasElement }) => {
172
+ await assertSankeyRendered(canvasElement, 0, false, expectedTabularLabelSides, expectedPaletteColors)
173
+
174
+ const screenedEligibleLink = canvasElement.querySelector(
175
+ '[data-link-source-id="Screened"][data-link-target-id="Eligible"]'
176
+ )
177
+ expect(screenedEligibleLink?.getAttribute('data-tooltip-html')).toBe(`<div class="sankey-chart__tooltip">
178
+ <span class="sankey-chart__tooltip--tooltip-header">Screened → Eligible</span>
179
+ <span>850</span>
180
+ </div>`)
181
+
182
+ await performAndAssert(
183
+ 'Sankey downstream highlight',
184
+ () => getSankeyHighlightState(canvasElement),
185
+ async () => {
186
+ const enrolledNode = canvasElement.querySelector('[data-node-id="Enrolled"]') as SVGRectElement
187
+ await userEvent.click(enrolledNode)
188
+ },
189
+ (_before, after) =>
190
+ ['Enrolled', 'Completed Treatment', 'Dropped Out', 'Recovered', 'Ongoing Care'].every(
191
+ nodeId => after.nodeOpacity[nodeId] === '1'
192
+ ) &&
193
+ ['Screened', 'Eligible', 'Ineligible', 'Declined'].every(nodeId => after.nodeOpacity[nodeId] === '0.1') &&
194
+ [
195
+ 'Enrolled->Completed Treatment',
196
+ 'Enrolled->Dropped Out',
197
+ 'Completed Treatment->Recovered',
198
+ 'Completed Treatment->Ongoing Care'
199
+ ].every(linkId => after.linkOpacity[linkId] === '0.6') &&
200
+ ['Screened->Eligible', 'Screened->Ineligible', 'Eligible->Enrolled', 'Eligible->Declined'].every(
201
+ linkId => after.linkOpacity[linkId] === '0.1'
202
+ )
203
+ )
204
+ }
205
+ }
206
+
207
+ export const Sankey_NarrowViewport: Story = {
208
+ decorators: [
209
+ Story => (
210
+ <div style={{ width: '360px' }}>
211
+ <Story />
212
+ </div>
213
+ )
214
+ ],
215
+ args: {
216
+ config: {
217
+ ...sankeyConfig,
218
+ title: 'Patient Care Pathway - Narrow',
219
+ data: tabularSankeyRows,
220
+ sankey: {
221
+ ...sankeyConfig.sankey,
222
+ horizontalScrollWidth: 900
223
+ }
224
+ },
225
+ isEditor: false
226
+ },
227
+ play: async ({ canvasElement }) => {
228
+ await assertSankeyRendered(canvasElement, 0, true, expectedTabularLabelSides, expectedPaletteColors)
229
+ }
230
+ }
231
+
232
+ export const Sankey_MediumViewportFullWidthWhenUnset: Story = {
233
+ decorators: [
234
+ Story => (
235
+ <div style={{ width: '760px' }}>
236
+ <Story />
237
+ </div>
238
+ )
239
+ ],
240
+ args: {
241
+ config: {
242
+ ...sankeyConfig,
243
+ title: 'Patient Care Pathway - Medium Full Width',
244
+ data: tabularSankeyRows,
245
+ sankey: {
246
+ ...sankeyConfig.sankey,
247
+ horizontalScrollWidth: ''
248
+ }
249
+ },
250
+ isEditor: false
251
+ },
252
+ play: async ({ canvasElement }) => {
253
+ await assertSankeyRendered(canvasElement, 0, false, expectedTabularLabelSides, expectedPaletteColors)
254
+ }
255
+ }
256
+
257
+ export const Sankey_MediumViewport: Story = {
258
+ decorators: [
259
+ Story => (
260
+ <div style={{ width: '760px' }}>
261
+ <Story />
262
+ </div>
263
+ )
264
+ ],
265
+ args: {
266
+ config: {
267
+ ...sankeyConfig,
268
+ title: 'Patient Care Pathway - Medium',
269
+ data: tabularSankeyRows,
270
+ sankey: {
271
+ ...sankeyConfig.sankey,
272
+ horizontalScrollWidth: 900
273
+ }
274
+ },
275
+ isEditor: false
276
+ },
277
+ play: async ({ canvasElement }) => {
278
+ await assertSankeyRendered(canvasElement, 0, true, expectedTabularLabelSides, expectedPaletteColors)
279
+ }
280
+ }
281
+
282
+ export const Sankey_CustomHorizontalScrollWidth: Story = {
283
+ decorators: [
284
+ Story => (
285
+ <div style={{ width: '980px' }}>
286
+ <Story />
287
+ </div>
288
+ )
289
+ ],
290
+ args: {
291
+ config: {
292
+ ...sankeyConfig,
293
+ title: 'Patient Care Pathway - Custom Scroll Width',
294
+ data: tabularSankeyRows,
295
+ sankey: {
296
+ ...sankeyConfig.sankey,
297
+ horizontalScrollWidth: 1100
298
+ }
299
+ },
300
+ isEditor: false
301
+ },
302
+ play: async ({ canvasElement }) => {
303
+ await assertSankeyRendered(canvasElement, 0, true, expectedTabularLabelSides, expectedPaletteColors)
304
+ }
305
+ }
306
+
307
+ export const Sankey_LongLabelsAndInvalidRows: Story = {
308
+ args: {
309
+ config: {
310
+ ...sankeyConfig,
311
+ title: 'Sankey with Long Labels and Invalid Rows',
312
+ data: longLabelRows
313
+ },
314
+ isEditor: false
315
+ },
316
+ play: async ({ canvasElement }) => {
317
+ await assertSankeyRendered(canvasElement, 2)
318
+ }
319
+ }
320
+
321
+ export const Sankey_TooltipsDisabled: Story = {
322
+ args: {
323
+ config: {
324
+ ...sankeyConfig,
325
+ title: 'Sankey - Tooltips Disabled',
326
+ data: tabularSankeyRows,
327
+ enableTooltips: false
328
+ },
329
+ isEditor: false
330
+ },
331
+ play: async ({ canvasElement }) => {
332
+ await assertSankeyRendered(canvasElement)
333
+ expect(getSankeyTooltipAttributeCount(canvasElement)).toBe(0)
334
+ }
335
+ }
336
+
337
+ export const Sankey_DataColumnControls: Story = {
338
+ args: {
339
+ config: {
340
+ ...sankeyConfig,
341
+ title: 'Sankey - Data Column Controls',
342
+ data: mappedColumnRows,
343
+ sankey: {
344
+ ...sankeyConfig.sankey,
345
+ horizontalScrollWidth: '',
346
+ columns: {
347
+ source: '',
348
+ target: '',
349
+ value: ''
350
+ }
351
+ }
352
+ },
353
+ isEditor: true
354
+ },
355
+ play: async ({ canvasElement }) => {
356
+ const canvas = within(canvasElement)
357
+ await waitForEditor(canvas)
358
+ await openAccordion(canvas, 'Sankey')
359
+
360
+ const sourceSelect = canvas.getByLabelText('Source Column') as HTMLSelectElement
361
+ await waitForOptionsToPopulate(sourceSelect, 4)
362
+ const scrollWidthInput = canvas.getByLabelText('Horizontal Scroll Width') as HTMLInputElement
363
+ expect(scrollWidthInput.value).toBe('')
364
+
365
+ await userEvent.type(scrollWidthInput, '640')
366
+ expect(scrollWidthInput.value).toBe('640')
367
+
368
+ await performAndAssert(
369
+ 'Sankey data column mapping',
370
+ () => getSankeyNodeIds(canvasElement),
371
+ async () => {
372
+ await userEvent.selectOptions(canvas.getByLabelText('Source Column'), 'Step')
373
+ await userEvent.selectOptions(canvas.getByLabelText('Target Column'), 'Next')
374
+ await userEvent.selectOptions(canvas.getByLabelText('Value Column'), 'People')
375
+ },
376
+ (before, after) =>
377
+ before.length === 0 && ['Declined', 'Eligible', 'Enrolled', 'Intake'].every(nodeId => after.includes(nodeId))
378
+ )
379
+ }
380
+ }
381
+
382
+ export const Sankey_TooltipControl: Story = {
383
+ args: {
384
+ config: {
385
+ ...sankeyConfig,
386
+ title: 'Sankey - Tooltip Control',
387
+ data: tabularSankeyRows,
388
+ enableTooltips: false
389
+ },
390
+ isEditor: true
391
+ },
392
+ play: async ({ canvasElement }) => {
393
+ const canvas = within(canvasElement)
394
+ await waitForEditor(canvas)
395
+ await openAccordion(canvas, 'Sankey')
396
+ const scrollWidthInput = canvas.getByLabelText('Horizontal Scroll Width') as HTMLInputElement
397
+
398
+ await performAndAssert(
399
+ 'Sankey horizontal scroll width input',
400
+ () => scrollWidthInput.value,
401
+ async () => {
402
+ await userEvent.clear(scrollWidthInput)
403
+ await userEvent.type(scrollWidthInput, '640')
404
+ },
405
+ (before, after) => before === '900' && after === '640'
406
+ )
407
+
408
+ await performAndAssert(
409
+ 'Sankey tooltip toggle',
410
+ () => getSankeyTooltipAttributeCount(canvasElement),
411
+ async () => {
412
+ await userEvent.click(canvas.getByLabelText('Show Tooltips'))
413
+ },
414
+ (before, after) => before === 0 && after > 0
415
+ )
416
+ }
417
+ }
@@ -16,6 +16,11 @@ const assignedColors = {
16
16
  charlie: '#5A8E3F'
17
17
  }
18
18
 
19
+ const comboColors = {
20
+ bars: '#0057B7',
21
+ line: '#E69F00'
22
+ }
23
+
19
24
  const normalizeColor = (color: string) => {
20
25
  const swatch = document.createElement('div')
21
26
  swatch.style.color = color
@@ -180,4 +185,46 @@ export const FilteredSeriesKeepAssignedColors: Story = {
180
185
  }
181
186
  }
182
187
 
188
+ const comboWithIsolatedLinePointConfig = {
189
+ ...config,
190
+ title: 'Combo Chart Isolated Point Color',
191
+ visualizationType: 'Combo',
192
+ lineDatapointStyle: 'hidden',
193
+ general: {
194
+ ...config.general,
195
+ palette: {
196
+ ...config.general.palette,
197
+ colorAssignments: [
198
+ { key: 'bars', color: comboColors.bars },
199
+ { key: 'line', color: comboColors.line }
200
+ ]
201
+ }
202
+ },
203
+ legend: {
204
+ ...config.legend,
205
+ hide: true
206
+ },
207
+ series: [
208
+ { dataKey: 'bars', name: 'Bars', type: 'Bar', axis: 'Left', tooltip: true },
209
+ { dataKey: 'line', name: 'Line', type: 'Line', axis: 'Left', tooltip: true }
210
+ ],
211
+ filters: [],
212
+ data: [
213
+ { week: '2024-01-01', bars: 10, line: 20 },
214
+ { week: '2024-01-08', bars: 12, line: null },
215
+ { week: '2024-01-15', bars: 14, line: null }
216
+ ]
217
+ }
218
+
219
+ export const ComboIsolatedPointUsesLineColor: Story = {
220
+ args: {
221
+ config: comboWithIsolatedLinePointConfig,
222
+ isEditor: false
223
+ },
224
+ play: async ({ canvasElement }) => {
225
+ await assertVisualizationRendered(canvasElement)
226
+ await expectVisibleSeriesColors(canvasElement, [['0', comboColors.line]])
227
+ }
228
+ }
229
+
183
230
  export default meta
@@ -0,0 +1,137 @@
1
+ import { useMemo } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { expect, waitFor } from 'storybook/test'
4
+
5
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
6
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
7
+ import Chart from '../CdcChartComponent'
8
+ import smallMultiplesBars from './_mock/small_multiples/small_multiples_bars.json'
9
+ import type { ChartConfig } from '../types/ChartConfig'
10
+
11
+ const sourceTileValues = ['10', '2', '1', '20', '3']
12
+ const expectedNumericAwareOrder = ['Week 1', 'Week 2', 'Week 3', 'Week 10', 'Week 20']
13
+
14
+ const numericTileRows = sourceTileValues.map((week, index) => ({
15
+ Week: week,
16
+ Date: 'Current period',
17
+ Cases: String((index + 1) * 10)
18
+ }))
19
+
20
+ const tileTitles = sourceTileValues.reduce<Record<string, string>>((titles, week) => {
21
+ titles[week] = `Week ${week}`
22
+ return titles
23
+ }, {})
24
+
25
+ const getConfig = (): ChartConfig =>
26
+ editConfigKeys(smallMultiplesBars, [
27
+ { path: ['title'], value: 'Small Multiples Numeric Tile Order' },
28
+ { path: ['showTitle'], value: true },
29
+ { path: ['visualizationSubType'], value: 'regular' },
30
+ { path: ['data'], value: numericTileRows },
31
+ { path: ['formattedData'], value: numericTileRows },
32
+ { path: ['dataUrl'], value: '' },
33
+ { path: ['runtimeDataUrl'], value: '' },
34
+ { path: ['filters'], value: [] },
35
+ { path: ['table', 'show'], value: false },
36
+ { path: ['xAxis', 'dataKey'], value: 'Date' },
37
+ { path: ['xAxis', 'type'], value: 'categorical' },
38
+ { path: ['xAxis', 'label'], value: 'Date' },
39
+ { path: ['yAxis', 'label'], value: 'Cases' },
40
+ { path: ['yAxis', 'hideAxis'], value: false },
41
+ { path: ['yAxis', 'hideTicks'], value: false },
42
+ { path: ['yAxis', 'numTicks'], value: '3' },
43
+ {
44
+ path: ['series'],
45
+ value: [{ dataKey: 'Cases', type: 'Bar', axis: 'Left', tooltip: true }]
46
+ },
47
+ { path: ['smallMultiples', 'mode'], value: 'by-column' },
48
+ { path: ['smallMultiples', 'tileColumn'], value: 'Week' },
49
+ { path: ['smallMultiples', 'tileOrderType'], value: 'asc' },
50
+ { path: ['smallMultiples', 'tileOrder'], value: [] },
51
+ { path: ['smallMultiples', 'tileTitles'], value: tileTitles },
52
+ { path: ['smallMultiples', 'tilesPerRowDesktop'], value: 5 },
53
+ { path: ['smallMultiples', 'tilesPerRowMobile'], value: 1 }
54
+ ]) as ChartConfig
55
+
56
+ const getRenderedTileTitles = (canvasElement: HTMLElement) =>
57
+ Array.from(canvasElement.querySelectorAll('.small-multiple-tile .tile-title')).map(
58
+ title => title.textContent?.trim() || ''
59
+ )
60
+
61
+ function SmallMultiplesNumericTileOrderRegression() {
62
+ const config = useMemo(() => getConfig(), [])
63
+
64
+ return (
65
+ <div
66
+ style={{
67
+ background: '#f7f8fa',
68
+ display: 'grid',
69
+ gap: 16,
70
+ padding: 16
71
+ }}
72
+ >
73
+ <section
74
+ style={{
75
+ background: '#fff',
76
+ border: '1px solid #d9dfe7',
77
+ borderRadius: 6,
78
+ padding: 16
79
+ }}
80
+ >
81
+ <Chart config={config} isEditor={true} interactionLabel='small-multiples-numeric-tile-order-regression' />
82
+ </section>
83
+
84
+ <section
85
+ aria-label='Small multiples numeric tile order diagnostic'
86
+ style={{
87
+ background: '#fff',
88
+ border: '1px solid #d9dfe7',
89
+ borderRadius: 6,
90
+ display: 'grid',
91
+ gap: 8,
92
+ padding: 12
93
+ }}
94
+ >
95
+ <div>
96
+ <strong>Input tile values:</strong> <span>{sourceTileValues.join(', ')}</span>
97
+ </div>
98
+ <div>
99
+ <strong>Expected numeric-aware order:</strong> <span>{expectedNumericAwareOrder.join(', ')}</span>
100
+ </div>
101
+ <div>
102
+ <strong>Expected rendered order:</strong>{' '}
103
+ <span data-testid='small-multiples-expected-order'>{expectedNumericAwareOrder.join(', ')}</span>
104
+ </div>
105
+ <div data-testid='small-multiples-order-status'>
106
+ <strong>Status:</strong> Numeric order preserved
107
+ </div>
108
+ </section>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ const meta: Meta<typeof SmallMultiplesNumericTileOrderRegression> = {
114
+ title: 'Components/Templates/Chart/Small Multiples',
115
+ component: SmallMultiplesNumericTileOrderRegression,
116
+ parameters: {
117
+ layout: 'fullscreen'
118
+ }
119
+ }
120
+
121
+ export default meta
122
+ type Story = StoryObj<typeof SmallMultiplesNumericTileOrderRegression>
123
+
124
+ export const Numeric_Tile_Values_Render_In_Numeric_Aware_Order: Story = {
125
+ play: async ({ canvasElement }) => {
126
+ await assertVisualizationRendered(canvasElement)
127
+
128
+ await waitFor(() => {
129
+ const renderedTileTitles = getRenderedTileTitles(canvasElement)
130
+
131
+ expect(renderedTileTitles).toEqual(expectedNumericAwareOrder)
132
+ expect(canvasElement.querySelector('[data-testid="small-multiples-order-status"]')?.textContent).toContain(
133
+ 'Numeric order preserved'
134
+ )
135
+ })
136
+ }
137
+ }
@@ -0,0 +1,43 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
3
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
4
+ import Chart from '../CdcChartComponent'
5
+ import pairedBar from './_mock/paired-bar.json'
6
+
7
+ const meta: Meta<typeof Chart> = {
8
+ title: 'Components/Templates/Chart/Tooltip/Series Labels',
9
+ component: Chart
10
+ }
11
+
12
+ type Story = StoryObj<typeof Chart>
13
+
14
+ export const Paired_Bar_Custom_Series_Labels: Story = {
15
+ args: {
16
+ config: {
17
+ ...pairedBar,
18
+ title: 'Paired Bar Custom Series Labels',
19
+ series: pairedBar.series.map(series => ({
20
+ ...series,
21
+ name: series.dataKey === 'Male' ? 'Reported Males' : 'Reported Females'
22
+ })),
23
+ columns: {
24
+ Male: { name: 'Male', label: 'Male Rate' },
25
+ Female: { name: 'Female', label: 'Female Rate' }
26
+ }
27
+ }
28
+ },
29
+ play: async ({ canvasElement }) => {
30
+ await assertVisualizationRendered(canvasElement)
31
+
32
+ const firstBarTooltip = canvasElement.querySelector('.bar.group-1')?.getAttribute('data-tooltip-html') || ''
33
+ const secondBarTooltip = canvasElement.querySelector('.bar.group-2')?.getAttribute('data-tooltip-html') || ''
34
+
35
+ expect(firstBarTooltip).toContain('Male Rate:')
36
+ expect(firstBarTooltip).not.toContain('Reported Males:')
37
+ expect(secondBarTooltip).toContain('Female Rate:')
38
+ expect(secondBarTooltip).not.toContain('Reported Females:')
39
+ expect(`${firstBarTooltip}${secondBarTooltip}`).not.toContain('undefined:')
40
+ }
41
+ }
42
+
43
+ export default meta
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, within } from 'storybook/test'
3
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
4
+ import Chart from '../CdcChart'
5
+ import config from '../../examples/vega-remote.json'
6
+ import remoteDataUrl from '../../examples/__data__/vega-remote-data.json?url'
7
+
8
+ const meta: Meta<typeof Chart> = {
9
+ title: 'Components/Templates/Chart/Vega Remote Data',
10
+ component: Chart
11
+ }
12
+
13
+ export default meta
14
+
15
+ type Story = StoryObj<typeof Chart>
16
+
17
+ export const ProcessesFetchedData: Story = {
18
+ args: {
19
+ config: { ...config, dataUrl: remoteDataUrl } as any,
20
+ isEditor: false
21
+ },
22
+ play: async ({ canvasElement }) => {
23
+ await assertVisualizationRendered(canvasElement)
24
+
25
+ const table = await within(canvasElement).findByRole('table', { name: /data table showing data for the chart/i })
26
+ expect(within(table).getByRole('columnheader', { name: 'Alpha' })).toBeVisible()
27
+ expect(within(table).getByRole('gridcell', { name: '20' })).toBeVisible()
28
+ expect(within(table).getByRole('columnheader', { name: 'Beta' })).toBeVisible()
29
+ expect(within(table).getByRole('gridcell', { name: '40' })).toBeVisible()
30
+ }
31
+ }