@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,432 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, fireEvent, userEvent, within } from 'storybook/test'
3
+ import Chart from '../CdcChartComponent'
4
+ import { createNewChartConfig } from '../helpers/createNewChartConfig'
5
+ import {
6
+ assertVisualizationRendered,
7
+ openAccordion,
8
+ performAndAssert,
9
+ waitForEditor,
10
+ waitForPresence
11
+ } from '@cdc/core/helpers/testing'
12
+
13
+ const rows = [
14
+ {
15
+ source: 'Clinic',
16
+ target: 'Health Department',
17
+ weight: 8,
18
+ style: 'solid',
19
+ nodeColor: '#005eaa',
20
+ alternateSource: 'A',
21
+ alternateTarget: 'B',
22
+ alternateWeight: 1,
23
+ alternateStyle: 'solid',
24
+ alternateNodeColor: '#111111'
25
+ },
26
+ {
27
+ source: 'Health Department',
28
+ target: 'Hospital',
29
+ weight: 5,
30
+ style: 'dashed',
31
+ nodeColor: '#4b830d',
32
+ alternateSource: 'B',
33
+ alternateTarget: 'C',
34
+ alternateWeight: 5,
35
+ alternateStyle: 'dashed',
36
+ alternateNodeColor: '#222222'
37
+ },
38
+ {
39
+ source: 'Clinic',
40
+ target: 'Hospital',
41
+ weight: 3,
42
+ style: 'solid',
43
+ nodeColor: '#005eaa',
44
+ alternateSource: 'A',
45
+ alternateTarget: 'C',
46
+ alternateWeight: 10,
47
+ alternateStyle: 'solid',
48
+ alternateNodeColor: '#111111'
49
+ },
50
+ {
51
+ source: 'Hospital',
52
+ target: 'Clinic',
53
+ weight: 2,
54
+ style: 'dashed',
55
+ nodeColor: '#712177',
56
+ alternateSource: 'C',
57
+ alternateTarget: 'A',
58
+ alternateWeight: 20,
59
+ alternateStyle: 'dashed',
60
+ alternateNodeColor: '#333333'
61
+ }
62
+ ]
63
+
64
+ const networkConfig = createNewChartConfig({
65
+ type: 'chart',
66
+ visualizationType: 'Network',
67
+ title: 'Partner Network',
68
+ showTitle: true,
69
+ enableTooltips: true,
70
+ data: rows,
71
+ network: {
72
+ columns: {
73
+ source: 'source',
74
+ target: 'target',
75
+ weight: 'weight',
76
+ style: 'style',
77
+ nodeColor: 'nodeColor'
78
+ },
79
+ directed: false,
80
+ height: 420,
81
+ nodeRadius: 8,
82
+ nodeColor: '#005eaa',
83
+ linkColor: '#333333',
84
+ linkDistance: 90,
85
+ chargeStrength: -240,
86
+ rotation: 0,
87
+ scale: 1,
88
+ offsetX: 0,
89
+ offsetY: 0
90
+ }
91
+ })
92
+
93
+ const labelOverlapsLink = (label: SVGTextElement, link: SVGLineElement) => {
94
+ const labelBounds = label.getBBox()
95
+ const labelTransform = label.getCTM()
96
+ const linkTransform = link.getCTM()
97
+ if (!labelTransform || !linkTransform) {
98
+ throw new Error('Network collision characterization requires SVG label and link CTMs.')
99
+ }
100
+
101
+ const overlapsAtDistance = (distance: number) => {
102
+ const point = link
103
+ .getPointAtLength(distance)
104
+ .matrixTransform(linkTransform)
105
+ .matrixTransform(labelTransform.inverse())
106
+ return (
107
+ point.x >= labelBounds.x &&
108
+ point.x <= labelBounds.x + labelBounds.width &&
109
+ point.y >= labelBounds.y &&
110
+ point.y <= labelBounds.y + labelBounds.height
111
+ )
112
+ }
113
+
114
+ const linkLength = link.getTotalLength()
115
+ for (let distance = 0; distance < linkLength; distance += 1) {
116
+ if (overlapsAtDistance(distance)) return true
117
+ }
118
+ return overlapsAtDistance(linkLength)
119
+ }
120
+
121
+ const expectLabelsWithinSvg = (svg: SVGSVGElement) => {
122
+ const svgBounds = svg.getBoundingClientRect()
123
+ svg.querySelectorAll<SVGTextElement>('.network-chart__label').forEach(label => {
124
+ const labelBounds = label.getBoundingClientRect()
125
+ expect(labelBounds.left).toBeGreaterThanOrEqual(svgBounds.left - 1)
126
+ expect(labelBounds.right).toBeLessThanOrEqual(svgBounds.right + 1)
127
+ expect(labelBounds.top).toBeGreaterThanOrEqual(svgBounds.top - 1)
128
+ expect(labelBounds.bottom).toBeLessThanOrEqual(svgBounds.bottom + 1)
129
+ })
130
+ }
131
+
132
+ const meta: Meta<typeof Chart> = {
133
+ title: 'Components/Templates/Chart/Network',
134
+ component: Chart
135
+ }
136
+
137
+ export default meta
138
+ type Story = StoryObj<typeof Chart>
139
+
140
+ export const Network_Runtime: Story = {
141
+ args: {
142
+ config: createNewChartConfig({
143
+ ...networkConfig,
144
+ network: { ...networkConfig.network, rotation: 30 }
145
+ }),
146
+ isEditor: false
147
+ },
148
+ play: async ({ canvasElement }) => {
149
+ await assertVisualizationRendered(canvasElement)
150
+ const table = await waitForPresence('.data-table', canvasElement)
151
+ const tableHeaders = Array.from(table.querySelectorAll('thead th > span:first-child')).map(header =>
152
+ header.textContent?.trim()
153
+ )
154
+ expect(tableHeaders).toEqual([
155
+ 'source',
156
+ 'target',
157
+ 'alternateSource',
158
+ 'alternateTarget',
159
+ 'alternateWeight',
160
+ 'alternateStyle',
161
+ 'alternateNodeColor'
162
+ ])
163
+ const svg = await waitForPresence('.network-chart', canvasElement)
164
+ const nodes = svg.querySelectorAll('.network-chart__node')
165
+ const links = svg.querySelectorAll('.network-chart__link')
166
+ const linkGroups = svg.querySelectorAll('.network-chart__link-group')
167
+
168
+ expect(nodes).toHaveLength(3)
169
+ expect(links).toHaveLength(3)
170
+ expect(nodes[0]).toHaveAttribute('tabindex', '0')
171
+ expect(nodes[0]).toHaveAttribute('aria-label')
172
+ expect(linkGroups[0]).toHaveAttribute('data-tooltip-content')
173
+ expect(svg.querySelectorAll('.network-chart__link--dashed')).toHaveLength(2)
174
+ expect(svg.querySelectorAll('.network-chart__link:not(.network-chart__link--dashed)')).toHaveLength(1)
175
+ expect(svg.querySelector('[aria-label^="Clinic,"] circle')).toHaveAttribute('fill', '#005eaa')
176
+ expect(svg.querySelector('[aria-label^="Health Department,"] circle')).toHaveAttribute('fill', '#4b830d')
177
+ expect(svg.querySelector('[aria-label^="Hospital,"] circle')).toHaveAttribute('fill', '#712177')
178
+ expect(window.getComputedStyle(svg.querySelector('.network-chart__label')!).fontSize).toBe('16px')
179
+ const labels = [...svg.querySelectorAll<SVGTextElement>('.network-chart__label')]
180
+ const renderedLinks = [...svg.querySelectorAll<SVGLineElement>('.network-chart__viewport .network-chart__link')]
181
+ expect(labels.some(label => renderedLinks.some(link => labelOverlapsLink(label, link)))).toBe(false)
182
+ const linkStyles = within(canvasElement).getByRole('group', { name: 'Link styles' })
183
+ expect(linkStyles).toHaveTextContent('Solid')
184
+ expect(linkStyles).toHaveTextContent('Dashed')
185
+
186
+ const viewport = svg.querySelector('.network-chart__viewport')
187
+ const initialTransform = viewport?.getAttribute('transform')
188
+ expect(initialTransform).toContain('rotate(30)')
189
+ expectLabelsWithinSvg(svg as SVGSVGElement)
190
+ fireEvent.wheel(svg, { deltaY: -100 })
191
+ expect(viewport?.getAttribute('transform')).toBe(initialTransform)
192
+
193
+ within(canvasElement).getByLabelText('Zoom in').focus()
194
+ await userEvent.keyboard('{Enter}')
195
+ expect(viewport?.getAttribute('transform')).not.toBe(initialTransform)
196
+ expect(within(canvasElement).queryByLabelText('Rotate left')).not.toBeInTheDocument()
197
+ expect(within(canvasElement).queryByLabelText('Rotate right')).not.toBeInTheDocument()
198
+ await userEvent.click(within(canvasElement).getByLabelText('Reset view'))
199
+ expect(viewport?.getAttribute('transform')).toBe(initialTransform)
200
+ expectLabelsWithinSvg(svg as SVGSVGElement)
201
+ await userEvent.hover(svg.querySelector('.network-chart__link-hitbox')!)
202
+ const tooltip = await waitForPresence('.network-chart__tooltip', document.body)
203
+ expect(window.getComputedStyle(tooltip).backgroundColor).toBe('rgb(255, 255, 255)')
204
+ }
205
+ }
206
+
207
+ export const Network_NarrowViewport: Story = {
208
+ args: { config: networkConfig, isEditor: false },
209
+ decorators: [
210
+ Story => (
211
+ <div style={{ width: 360 }}>
212
+ <Story />
213
+ </div>
214
+ )
215
+ ],
216
+ play: async ({ canvasElement }) => {
217
+ const svg = (await waitForPresence('.network-chart', canvasElement)) as SVGSVGElement
218
+ const [, , width, height] = (svg.getAttribute('viewBox') || '').split(' ').map(Number)
219
+ expect(width).toBeGreaterThanOrEqual(160)
220
+ expect(height).toBe(388)
221
+ expect(svg.querySelectorAll('.network-chart__node')).toHaveLength(3)
222
+ }
223
+ }
224
+
225
+ export const Network_MinimumHeightWithLinkKey: Story = {
226
+ args: {
227
+ config: createNewChartConfig({
228
+ ...networkConfig,
229
+ network: { ...networkConfig.network, height: 160 }
230
+ }),
231
+ isEditor: false
232
+ },
233
+ play: async ({ canvasElement }) => {
234
+ const svg = (await waitForPresence('.network-chart', canvasElement)) as SVGSVGElement
235
+ const [, , width, height] = (svg.getAttribute('viewBox') || '').split(' ').map(Number)
236
+ expect(width).toBeGreaterThanOrEqual(160)
237
+ expect(height).toBe(128)
238
+ expect(within(canvasElement).getByRole('group', { name: 'Link styles' })).toBeVisible()
239
+ expectLabelsWithinSvg(svg)
240
+ }
241
+ }
242
+
243
+ export const Network_Editor: Story = {
244
+ args: {
245
+ config: {
246
+ ...networkConfig,
247
+ network: {
248
+ ...networkConfig.network,
249
+ columns: { ...networkConfig.network.columns, style: '', nodeColor: '' }
250
+ }
251
+ },
252
+ isEditor: true
253
+ },
254
+ play: async ({ canvasElement }) => {
255
+ const canvas = within(canvasElement)
256
+ await waitForEditor(canvas)
257
+ await openAccordion(canvas, 'Network')
258
+ expect(canvas.getByLabelText('Source Column')).toBeInTheDocument()
259
+ expect(canvas.getByLabelText('Target Column')).toBeInTheDocument()
260
+ expect(canvas.getByLabelText('Weight Column (Optional)')).toBeInTheDocument()
261
+ expect(canvas.getByLabelText('Link Style Column (Optional)')).toBeInTheDocument()
262
+ expect(canvas.getByLabelText('Solid Link Legend Label')).toHaveValue('Solid')
263
+ expect(canvas.getByLabelText('Dashed Link Legend Label')).toHaveValue('Dashed')
264
+ expect(canvas.getByLabelText('Node Color Column (Optional)')).toBeInTheDocument()
265
+ expect(canvas.getByLabelText('Directed Network')).toBeInTheDocument()
266
+ expect(canvas.queryByLabelText('Show Node Labels')).not.toBeInTheDocument()
267
+ expect(canvas.queryByLabelText('Show Tooltips')).not.toBeInTheDocument()
268
+ expect(canvasElement.querySelector('[data-tooltip-content]')).toBeInTheDocument()
269
+ expect(canvas.getByLabelText('Link Distance')).toHaveAttribute('type', 'range')
270
+ expect(canvas.getByLabelText('Repulsion Strength')).toHaveAttribute('type', 'range')
271
+ expect(canvas.queryByLabelText('Initial Rotation (Degrees)')).not.toBeInTheDocument()
272
+ expect(canvas.queryByLabelText('Initial Zoom')).not.toBeInTheDocument()
273
+ expect(canvas.queryByLabelText('Initial Horizontal Offset')).not.toBeInTheDocument()
274
+ expect(canvas.queryByLabelText('Initial Vertical Offset')).not.toBeInTheDocument()
275
+ expect(canvas.queryByLabelText('Default Node Color')).not.toBeInTheDocument()
276
+ expect(canvas.queryByLabelText('Link Color')).not.toBeInTheDocument()
277
+ expect(canvas.queryByRole('button', { name: 'Reset Initial View' })).not.toBeInTheDocument()
278
+ expect(canvas.queryByText('Pattern Settings')).not.toBeInTheDocument()
279
+ expect(canvas.queryByText('Text Annotations')).not.toBeInTheDocument()
280
+ expect(canvas.getByRole('button', { name: 'Columns' })).toBeInTheDocument()
281
+ expect(canvas.getByRole('button', { name: 'Legend' })).toBeInTheDocument()
282
+
283
+ await openAccordion(canvas, 'Columns')
284
+ expect(canvas.getByRole('button', { name: 'Add Configuration' })).toBeInTheDocument()
285
+ await openAccordion(canvas, 'Network')
286
+
287
+ const nodeLabels = () =>
288
+ Array.from(canvasElement.querySelectorAll('.network-chart__label'))
289
+ .map(label => label.textContent)
290
+ .sort()
291
+ .join('|')
292
+ const linkWidths = () =>
293
+ Array.from(canvasElement.querySelectorAll('.network-chart__viewport .network-chart__link'))
294
+ .map(link => link.getAttribute('stroke-width'))
295
+ .join('|')
296
+ const linkCoordinates = () =>
297
+ Array.from(canvasElement.querySelectorAll('.network-chart__viewport .network-chart__link'))
298
+ .map(link => ['x1', 'y1', 'x2', 'y2'].map(attribute => link.getAttribute(attribute)).join(','))
299
+ .join('|')
300
+ const nodeFills = () =>
301
+ Array.from(canvasElement.querySelectorAll('.network-chart__node circle'))
302
+ .map(node => node.getAttribute('fill'))
303
+ .sort()
304
+ .join('|')
305
+
306
+ await performAndAssert(
307
+ 'Source mapping changes rendered nodes',
308
+ nodeLabels,
309
+ async () => userEvent.selectOptions(canvas.getByLabelText('Source Column'), 'alternateSource'),
310
+ (before, after) => before !== after && after.includes('A')
311
+ )
312
+
313
+ await performAndAssert(
314
+ 'Target mapping changes rendered nodes',
315
+ nodeLabels,
316
+ async () => userEvent.selectOptions(canvas.getByLabelText('Target Column'), 'alternateTarget'),
317
+ (before, after) => before !== after && after === 'A|B|C'
318
+ )
319
+
320
+ await performAndAssert(
321
+ 'Weight mapping changes rendered link widths',
322
+ linkWidths,
323
+ async () => userEvent.selectOptions(canvas.getByLabelText('Weight Column (Optional)'), 'alternateWeight'),
324
+ (before, after) => before !== after
325
+ )
326
+
327
+ await performAndAssert(
328
+ 'Directed links show arrowheads',
329
+ () =>
330
+ Array.from(canvasElement.querySelectorAll('.network-chart__link')).filter(link =>
331
+ link.hasAttribute('marker-end')
332
+ ).length,
333
+ async () => userEvent.click(canvas.getByLabelText('Directed Network')),
334
+ (before, after) => before === 0 && after === 4
335
+ )
336
+
337
+ await performAndAssert(
338
+ 'Node color mapping applies source-node data colors',
339
+ nodeFills,
340
+ async () => userEvent.selectOptions(canvas.getByLabelText('Node Color Column (Optional)'), 'alternateNodeColor'),
341
+ (before, after) => before !== after && after.includes('#111111')
342
+ )
343
+
344
+ await performAndAssert(
345
+ 'Link style mapping applies styles per row',
346
+ () => canvasElement.querySelectorAll('.network-chart__viewport .network-chart__link--dashed').length,
347
+ async () => userEvent.selectOptions(canvas.getByLabelText('Link Style Column (Optional)'), 'alternateStyle'),
348
+ (before, after) => before === 0 && after === 2
349
+ )
350
+
351
+ await performAndAssert(
352
+ 'Solid link legend label updates the rendered key',
353
+ () => canvasElement.querySelector('[aria-label="Link styles"]')?.textContent || '',
354
+ async () =>
355
+ fireEvent.change(canvas.getByLabelText('Solid Link Legend Label'), { target: { value: 'Established' } }),
356
+ (before, after) => before.includes('Solid') && after.includes('Established')
357
+ )
358
+
359
+ await performAndAssert(
360
+ 'Dashed link legend label updates the rendered key',
361
+ () => canvasElement.querySelector('[aria-label="Link styles"]')?.textContent || '',
362
+ async () =>
363
+ fireEvent.change(canvas.getByLabelText('Dashed Link Legend Label'), { target: { value: 'Provisional' } }),
364
+ (before, after) => before.includes('Dashed') && after.includes('Provisional')
365
+ )
366
+
367
+ await openAccordion(canvas, 'Legend')
368
+ const legendPanel = within(
369
+ canvas.getByRole('button', { name: 'Legend' }).closest('.accordion__item') as HTMLElement
370
+ )
371
+ expect(legendPanel.getByRole('checkbox', { name: /Hide Legend/ })).toBeInTheDocument()
372
+ expect(legendPanel.queryByLabelText('Position')).not.toBeInTheDocument()
373
+ expect(legendPanel.queryByLabelText('Legend Style')).not.toBeInTheDocument()
374
+ expect(legendPanel.queryByLabelText('Title')).not.toBeInTheDocument()
375
+ expect(legendPanel.queryByLabelText('Legend Description')).not.toBeInTheDocument()
376
+
377
+ await performAndAssert(
378
+ 'Hide Legend removes the rendered link-style key',
379
+ () => canvasElement.querySelectorAll('[aria-label="Link styles"]').length,
380
+ async () => userEvent.click(legendPanel.getByRole('checkbox', { name: /Hide Legend/ })),
381
+ (before, after) => before === 1 && after === 0
382
+ )
383
+
384
+ await performAndAssert(
385
+ 'Show Legend restores the rendered link-style key',
386
+ () => canvasElement.querySelectorAll('[aria-label="Link styles"]').length,
387
+ async () => userEvent.click(legendPanel.getByRole('checkbox', { name: /Hide Legend/ })),
388
+ (before, after) => before === 0 && after === 1
389
+ )
390
+
391
+ await openAccordion(canvas, 'Network')
392
+
393
+ await performAndAssert(
394
+ 'Height changes the rendered SVG viewport',
395
+ () => Number(canvasElement.querySelector('.network-chart')?.getAttribute('viewBox')?.split(' ')[3]),
396
+ async () => {
397
+ const heightInput = canvas.getByLabelText('Height')
398
+ await userEvent.clear(heightInput)
399
+ await userEvent.type(heightInput, '360')
400
+ },
401
+ (before, after) => before !== after && after === 328
402
+ )
403
+
404
+ await performAndAssert(
405
+ 'Blank height restores the rendered SVG viewport default',
406
+ () => Number(canvasElement.querySelector('.network-chart')?.getAttribute('viewBox')?.split(' ')[3]),
407
+ async () => userEvent.clear(canvas.getByLabelText('Height')),
408
+ (before, after) => before === 328 && after === 468
409
+ )
410
+
411
+ await performAndAssert(
412
+ 'Node radius changes rendered circles',
413
+ () => canvasElement.querySelector('.network-chart__node circle')?.getAttribute('r'),
414
+ async () => fireEvent.change(canvas.getByLabelText('Node Radius'), { target: { value: '12' } }),
415
+ (before, after) => before !== after && after === '12'
416
+ )
417
+
418
+ await performAndAssert(
419
+ 'Link distance changes node layout',
420
+ linkCoordinates,
421
+ async () => fireEvent.change(canvas.getByLabelText('Link Distance'), { target: { value: '140' } }),
422
+ (before, after) => before !== after
423
+ )
424
+
425
+ await performAndAssert(
426
+ 'Repulsion strength changes node layout',
427
+ linkCoordinates,
428
+ async () => fireEvent.change(canvas.getByLabelText('Repulsion Strength'), { target: { value: '-80' } }),
429
+ (before, after) => before !== after
430
+ )
431
+ }
432
+ }
@@ -0,0 +1,208 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, fn, userEvent, waitFor, within } from 'storybook/test'
3
+ import cloneConfig from '@cdc/core/helpers/cloneConfig'
4
+ import { openAccordion, waitForEditor } from '@cdc/core/helpers/testing'
5
+
6
+ import Chart from '../CdcChartComponent'
7
+ import mockScatterPlot from './_mock/scatterplot_mock.json'
8
+ import forecastComboWithGaps from './_mock/forecast_combo_with_gaps.json'
9
+
10
+ const meta: Meta<typeof Chart> = {
11
+ title: 'Components/Templates/Chart/Editor Tests/Palette Conversion',
12
+ component: Chart
13
+ }
14
+
15
+ export default meta
16
+ type Story = StoryObj<typeof Chart>
17
+
18
+ const twoColorVersion10Updates = fn()
19
+ const twoColorVersion20Updates = fn()
20
+ const forecastVersion10Updates = fn()
21
+ const forecastVersion20Updates = fn()
22
+
23
+ const buildBarConfig = (version: '1.0' | '2.0' | '2.1', visualizationType: 'Bar' | 'Paired Bar' = 'Bar') => {
24
+ const config = cloneConfig(mockScatterPlot) as any
25
+ config.version = '4.26.8'
26
+ config.visualizationType = visualizationType
27
+ config.xAxis = { ...config.xAxis, dataKey: 'category', type: 'categorical' }
28
+ config.series = config.series.slice(0, 2).map(series => ({ ...series, type: 'Bar' }))
29
+ config.data = [
30
+ { category: 'Q1', y1: 19, y2: 47 },
31
+ { category: 'Q2', y1: 18, y2: 32 }
32
+ ]
33
+ config.general.palette = {
34
+ isReversed: false,
35
+ name: version === '1.0' ? 'qualitative-bold' : 'qualitative_standard',
36
+ version
37
+ }
38
+ return config
39
+ }
40
+
41
+ const selectAlternativePalette = async (canvasElement: HTMLElement) => {
42
+ const button = Array.from(canvasElement.querySelectorAll<HTMLButtonElement>('.color-palette button[title]')).find(
43
+ candidate => !candidate.classList.contains('selected') && !candidate.title.startsWith('theme-')
44
+ ) as HTMLButtonElement
45
+ expect(button).toBeTruthy()
46
+ await userEvent.click(button)
47
+ return button
48
+ }
49
+
50
+ const expectConversionModal = (canvas: ReturnType<typeof within>) =>
51
+ canvas.getByRole('heading', { name: 'Color Palette Conversion' })
52
+
53
+ export const MainPaletteVersion10: Story = {
54
+ args: { isEditor: true, config: buildBarConfig('1.0') },
55
+ play: async ({ canvasElement }) => {
56
+ const canvas = within(canvasElement)
57
+ await waitForEditor(canvas)
58
+ await openAccordion(canvas, 'Visual')
59
+
60
+ const cancelledPalette = await selectAlternativePalette(canvasElement)
61
+ expect(expectConversionModal(canvas)).toBeInTheDocument()
62
+ expect(canvas.getByText(/uses an outdated color palette/)).toBeInTheDocument()
63
+ await userEvent.click(canvas.getByRole('button', { name: 'Cancel' }))
64
+ expect(cancelledPalette).toHaveClass('selected')
65
+
66
+ await selectAlternativePalette(canvasElement)
67
+ await userEvent.click(canvas.getByRole('button', { name: 'Convert to New Palette' }))
68
+ await selectAlternativePalette(canvasElement)
69
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
70
+ }
71
+ }
72
+
73
+ export const MainPaletteVersion20: Story = {
74
+ args: { isEditor: true, config: buildBarConfig('2.0') },
75
+ play: async ({ canvasElement }) => {
76
+ const canvas = within(canvasElement)
77
+ await waitForEditor(canvas)
78
+ await openAccordion(canvas, 'Visual')
79
+
80
+ const cancelledPalette = await selectAlternativePalette(canvasElement)
81
+ expect(expectConversionModal(canvas)).toBeInTheDocument()
82
+ expect(canvas.getByText(/updated color distribution that improves contrast/)).toBeInTheDocument()
83
+ await userEvent.click(canvas.getByRole('button', { name: 'Cancel' }))
84
+ expect(cancelledPalette).toHaveClass('selected')
85
+
86
+ await selectAlternativePalette(canvasElement)
87
+ await userEvent.click(canvas.getByRole('button', { name: 'Convert to New Palette' }))
88
+ await selectAlternativePalette(canvasElement)
89
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
90
+ }
91
+ }
92
+
93
+ export const MainPaletteVersion21: Story = {
94
+ args: { isEditor: true, config: buildBarConfig('2.1') },
95
+ play: async ({ canvasElement }) => {
96
+ const canvas = within(canvasElement)
97
+ await waitForEditor(canvas)
98
+ await openAccordion(canvas, 'Visual')
99
+ await selectAlternativePalette(canvasElement)
100
+
101
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
102
+ }
103
+ }
104
+
105
+ export const TwoColorVersion10: Story = {
106
+ args: { isEditor: true, config: buildBarConfig('1.0', 'Paired Bar'), setConfig: twoColorVersion10Updates },
107
+ play: async ({ canvasElement }) => {
108
+ twoColorVersion10Updates.mockClear()
109
+ const canvas = within(canvasElement)
110
+ await waitForEditor(canvas)
111
+ await openAccordion(canvas, 'Visual')
112
+ await selectAlternativePalette(canvasElement)
113
+
114
+ expect(expectConversionModal(canvas)).toBeInTheDocument()
115
+ await userEvent.click(canvas.getByRole('button', { name: 'Convert to New Palette' }))
116
+ await waitFor(() => {
117
+ const updatedConfig = twoColorVersion10Updates.mock.calls.at(-1)?.[0]
118
+ expect(updatedConfig.general.palette.version).toBe('2.1')
119
+ expect(updatedConfig.general.palette.backups).toContainEqual(
120
+ expect.objectContaining({ version: '1.0', type: 'twoColor' })
121
+ )
122
+ })
123
+ await selectAlternativePalette(canvasElement)
124
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
125
+ }
126
+ }
127
+
128
+ export const TwoColorVersion20: Story = {
129
+ args: { isEditor: true, config: buildBarConfig('2.0', 'Paired Bar'), setConfig: twoColorVersion20Updates },
130
+ play: async ({ canvasElement }) => {
131
+ twoColorVersion20Updates.mockClear()
132
+ const canvas = within(canvasElement)
133
+ await waitForEditor(canvas)
134
+ await openAccordion(canvas, 'Visual')
135
+ await selectAlternativePalette(canvasElement)
136
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
137
+ await waitFor(() => expect(twoColorVersion20Updates.mock.calls.at(-1)?.[0].general.palette.version).toBe('2.0'))
138
+ }
139
+ }
140
+
141
+ const buildForecastConfig = (version: '1.0' | '2.0') => {
142
+ const config = cloneConfig(forecastComboWithGaps) as any
143
+ config.version = '4.26.8'
144
+ config.general.palette = {
145
+ isReversed: false,
146
+ name: version === '1.0' ? 'qualitative-bold' : 'qualitative_standard',
147
+ version
148
+ }
149
+ config.series[0].stages[0].color = version === '1.0' ? 'Sequential Blue Two' : 'sequential-blue'
150
+ return config
151
+ }
152
+
153
+ const changeFirstForecastColor = async (canvas: ReturnType<typeof within>) => {
154
+ const forecastSeries = canvas
155
+ .getAllByRole('button', { name: /Month/ })
156
+ .find(button => button.classList.contains('accordion__button')) as HTMLElement
157
+ expect(forecastSeries).toBeTruthy()
158
+ await userEvent.click(forecastSeries)
159
+ const colorLabel = canvas.getByText('11 Series Color').closest('label')
160
+ const colorSelect = colorLabel?.querySelector('select') as HTMLSelectElement
161
+ expect(colorSelect).toBeTruthy()
162
+ const alternative = Array.from(colorSelect.options).find(option => option.value && option.value !== colorSelect.value)
163
+ expect(alternative).toBeTruthy()
164
+ await userEvent.selectOptions(colorSelect, alternative!.value)
165
+ }
166
+
167
+ export const ForecastVersion10: Story = {
168
+ args: { isEditor: true, config: buildForecastConfig('1.0'), setConfig: forecastVersion10Updates },
169
+ play: async ({ canvasElement }) => {
170
+ forecastVersion10Updates.mockClear()
171
+ const canvas = within(canvasElement)
172
+ await waitForEditor(canvas)
173
+ await openAccordion(canvas, 'Data Series')
174
+ await changeFirstForecastColor(canvas)
175
+
176
+ expect(expectConversionModal(canvas)).toBeInTheDocument()
177
+ await userEvent.click(canvas.getByRole('button', { name: 'Convert to New Palette' }))
178
+ await waitFor(() => {
179
+ const updatedConfig = forecastVersion10Updates.mock.calls.at(-1)?.[0]
180
+ expect(updatedConfig.general.palette.version).toBe('2.1')
181
+ updatedConfig.series
182
+ .filter(series => series.type === 'Forecasting')
183
+ .flatMap(series => series.stages)
184
+ .forEach(stage => expect(stage.color).toMatch(/^sequential-[a-z]+(?:reverse)?$/))
185
+ })
186
+
187
+ await openAccordion(canvas, 'Visual')
188
+ await selectAlternativePalette(canvasElement)
189
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
190
+ }
191
+ }
192
+
193
+ export const ForecastVersion20: Story = {
194
+ args: { isEditor: true, config: buildForecastConfig('2.0'), setConfig: forecastVersion20Updates },
195
+ play: async ({ canvasElement }) => {
196
+ forecastVersion20Updates.mockClear()
197
+ const canvas = within(canvasElement)
198
+ await waitForEditor(canvas)
199
+ await openAccordion(canvas, 'Data Series')
200
+ await changeFirstForecastColor(canvas)
201
+ expect(canvas.queryByRole('heading', { name: 'Color Palette Conversion' })).not.toBeInTheDocument()
202
+ await waitFor(() => expect(forecastVersion20Updates.mock.calls.at(-1)?.[0].general.palette.version).toBe('2.0'))
203
+
204
+ await openAccordion(canvas, 'Visual')
205
+ await selectAlternativePalette(canvasElement)
206
+ expect(expectConversionModal(canvas)).toBeInTheDocument()
207
+ }
208
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react'
2
+ import { expect } from 'storybook/test'
2
3
  import StackedPattern from './_mock/stacked-pattern-test.json'
3
4
 
4
5
  import Chart from '../CdcChartComponent'
@@ -21,4 +22,34 @@ export const Stacked_Bar_Pattern: Story = {
21
22
  }
22
23
  }
23
24
 
25
+ export const Large_Wave_Pattern: Story = {
26
+ args: {
27
+ config: {
28
+ ...StackedPattern,
29
+ legend: {
30
+ ...StackedPattern.legend,
31
+ patterns: {
32
+ Provisional: {
33
+ ...StackedPattern.legend.patterns.Provisional,
34
+ shape: 'waves',
35
+ patternSize: 16
36
+ }
37
+ }
38
+ }
39
+ } as any
40
+ },
41
+ play: async ({ canvasElement }) => {
42
+ await assertVisualizationRendered(canvasElement)
43
+
44
+ const wavePaths = Array.from(canvasElement.querySelectorAll('path.visx-pattern-wave'))
45
+ expect(wavePaths.length).toBeGreaterThan(0)
46
+
47
+ wavePaths.forEach(path => {
48
+ expect(path).toHaveAttribute('fill', 'transparent')
49
+ expect(path).toHaveAttribute('stroke', '#000000')
50
+ expect(path).toHaveAttribute('stroke-width', '0.75')
51
+ })
52
+ }
53
+ }
54
+
24
55
  export default meta