@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,60 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
3
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
4
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
5
+ import Chart from '../CdcChart'
6
+ import simplifiedLine from './_mock/simplified_line.json'
7
+
8
+ const meta: Meta<typeof Chart> = {
9
+ title: 'Components/Templates/Data Table Download Filename',
10
+ component: Chart
11
+ }
12
+
13
+ type Story = StoryObj<typeof Chart>
14
+
15
+ const captureImageDownloadName = async (downloadButton: HTMLElement) => {
16
+ const downloads: string[] = []
17
+ const originalClick = HTMLAnchorElement.prototype.click
18
+
19
+ HTMLAnchorElement.prototype.click = function () {
20
+ if (this.download) downloads.push(this.download)
21
+ }
22
+
23
+ try {
24
+ await userEvent.click(downloadButton)
25
+ await waitFor(() => expect(downloads.length).toBeGreaterThan(0), { timeout: 10000 })
26
+ return downloads[0]
27
+ } finally {
28
+ HTMLAnchorElement.prototype.click = originalClick
29
+ document.body.querySelectorAll('a[download]').forEach(anchor => anchor.remove())
30
+ }
31
+ }
32
+
33
+ export const ChartImageDownloadUsesTableFilenameFallback: Story = {
34
+ args: {
35
+ config: editConfigKeys(simplifiedLine, [
36
+ { path: ['title'], value: '' },
37
+ { path: ['dataFileSourceType'], value: 'file' },
38
+ { path: ['dataUrl'], value: '' },
39
+ { path: ['dataFileName'], value: '' },
40
+ { path: ['table', 'show'], value: true },
41
+ { path: ['table', 'expanded'], value: true },
42
+ { path: ['table', 'download'], value: true },
43
+ { path: ['table', 'showDownloadImgButton'], value: true },
44
+ { path: ['table', 'downloadFileName'], value: 'Chart Table Report.csv' },
45
+ { path: ['table', 'showDownloadLinkBelow'], value: false }
46
+ ]),
47
+ isEditor: false
48
+ },
49
+ play: async ({ canvasElement }) => {
50
+ await assertVisualizationRendered(canvasElement)
51
+
52
+ const canvas = within(canvasElement)
53
+ const downloadButton = await canvas.findByRole('button', { name: 'Download Chart as Image' })
54
+ const downloadName = await captureImageDownloadName(downloadButton)
55
+
56
+ expect(downloadName).toMatch(/^chart-table-report-\d{4}-\d{2}-\d{2}\.png$/)
57
+ }
58
+ }
59
+
60
+ export default meta
@@ -0,0 +1,373 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, fireEvent, userEvent, within } from 'storybook/test'
3
+ import {
4
+ assertVisualizationRendered,
5
+ openAccordion,
6
+ performAndAssert,
7
+ waitForEditor,
8
+ waitForPresence
9
+ } from '@cdc/core/helpers/testing'
10
+ import Chart from '../CdcChartComponent'
11
+ import { createNewChartConfig } from '../helpers/createNewChartConfig'
12
+
13
+ const data = [
14
+ { node: 'Public Health', parent: '', style: 'solid', nodeColor: '#005eaa' },
15
+ { node: 'Programs', parent: 'Public Health', style: 'solid', nodeColor: '#4b830d' },
16
+ { node: 'Partners', parent: 'Public Health', style: 'dashed', nodeColor: '#712177' },
17
+ { node: 'Clinics', parent: 'Partners', style: 'solid', nodeColor: '#c05600' }
18
+ ]
19
+
20
+ const dendrogramConfig = createNewChartConfig({
21
+ type: 'chart',
22
+ visualizationType: 'Dendrogram',
23
+ title: 'Public Health Hierarchy',
24
+ enableTooltips: true,
25
+ data,
26
+ dendrogram: {
27
+ columns: { node: 'node', parent: 'parent', style: 'style', nodeColor: 'nodeColor' },
28
+ orientation: 'horizontal',
29
+ height: 420,
30
+ nodeRadius: 6,
31
+ nodeColor: '#005eaa',
32
+ linkColor: '#333333',
33
+ leafSpacing: 40,
34
+ depthSpacing: 140
35
+ }
36
+ })
37
+
38
+ const labelCollisionData = [
39
+ { node: 'WWWW Public Health Operations', parent: '', style: 'solid', nodeColor: '#005eaa' },
40
+ {
41
+ node: 'Community Prevention Programs',
42
+ parent: 'WWWW Public Health Operations',
43
+ style: 'solid',
44
+ nodeColor: '#4b830d'
45
+ },
46
+ {
47
+ node: '区域 Clinical and Laboratory Partners 🙂',
48
+ parent: 'WWWW Public Health Operations',
49
+ style: 'dashed',
50
+ nodeColor: '#712177'
51
+ },
52
+ {
53
+ node: 'Regional Public Health Laboratories',
54
+ parent: '区域 Clinical and Laboratory Partners 🙂',
55
+ style: 'dashed',
56
+ nodeColor: '#c05600'
57
+ }
58
+ ]
59
+
60
+ const labelOverlapsLink = (label: SVGTextElement, link: SVGPathElement) => {
61
+ const labelBounds = label.getBBox()
62
+ const labelTransform = label.getCTM()
63
+ const linkTransform = link.getCTM()
64
+ if (!labelTransform || !linkTransform) {
65
+ throw new Error('Dendrogram collision characterization requires SVG label and link CTMs.')
66
+ }
67
+
68
+ const overlapsAtDistance = (distance: number) => {
69
+ const point = link
70
+ .getPointAtLength(distance)
71
+ .matrixTransform(linkTransform)
72
+ .matrixTransform(labelTransform.inverse())
73
+ if (
74
+ point.x >= labelBounds.x &&
75
+ point.x <= labelBounds.x + labelBounds.width &&
76
+ point.y >= labelBounds.y &&
77
+ point.y <= labelBounds.y + labelBounds.height
78
+ ) {
79
+ return true
80
+ }
81
+ return false
82
+ }
83
+
84
+ const linkLength = link.getTotalLength()
85
+ for (let distance = 0; distance < linkLength; distance += 1) {
86
+ if (overlapsAtDistance(distance)) return true
87
+ }
88
+
89
+ return overlapsAtDistance(linkLength)
90
+ }
91
+
92
+ const meta: Meta<typeof Chart> = {
93
+ title: 'Components/Templates/Chart/Dendrogram',
94
+ component: Chart
95
+ }
96
+
97
+ export default meta
98
+ type Story = StoryObj<typeof Chart>
99
+
100
+ export const Dendrogram_Runtime: Story = {
101
+ args: { config: dendrogramConfig, isEditor: false },
102
+ play: async ({ canvasElement }) => {
103
+ await assertVisualizationRendered(canvasElement)
104
+ const svg = await waitForPresence('.dendrogram-chart', canvasElement)
105
+ expect(svg.querySelectorAll('.dendrogram-chart__node')).toHaveLength(4)
106
+ expect(svg.querySelectorAll('.dendrogram-chart__link')).toHaveLength(3)
107
+ expect(svg.querySelectorAll('.dendrogram-chart__link--dashed')).toHaveLength(1)
108
+ expect(window.getComputedStyle(svg.querySelector('.dendrogram-chart__label')!).fontSize).toBe('16px')
109
+ const linkStyles = within(canvasElement).getByRole('group', { name: 'Link styles' })
110
+ expect(within(linkStyles).getByText('Solid')).toBeVisible()
111
+ expect(within(linkStyles).getByText('Dashed')).toBeVisible()
112
+ await userEvent.click(within(canvasElement).getByRole('button', { name: 'Zoom in' }))
113
+ await userEvent.click(within(canvasElement).getByRole('button', { name: 'Reset view' }))
114
+ }
115
+ }
116
+
117
+ export const Dendrogram_Narrow: Story = {
118
+ args: { config: dendrogramConfig, isEditor: false },
119
+ decorators: [
120
+ Story => (
121
+ <div style={{ width: 360 }}>
122
+ <Story />
123
+ </div>
124
+ )
125
+ ],
126
+ play: async ({ canvasElement }) => {
127
+ await assertVisualizationRendered(canvasElement)
128
+ const svg = await waitForPresence('.dendrogram-chart', canvasElement)
129
+ expect(svg.querySelectorAll('.dendrogram-chart__node')).toHaveLength(4)
130
+ expect(within(canvasElement).getByRole('button', { name: 'Zoom in' })).toBeVisible()
131
+ }
132
+ }
133
+
134
+ export const Dendrogram_Label_Collision_And_Link_Styles: Story = {
135
+ args: {
136
+ config: createNewChartConfig({
137
+ ...dendrogramConfig,
138
+ title: 'Dendrogram Label and Link Style Characterization',
139
+ data: labelCollisionData,
140
+ dendrogram: {
141
+ ...dendrogramConfig.dendrogram,
142
+ height: 360,
143
+ linkStyleLabels: {
144
+ solid: ' Established relationships across programs ',
145
+ dashed: ' Provisional partnerships under review '
146
+ },
147
+ leafSpacing: 32,
148
+ depthSpacing: 150
149
+ }
150
+ }),
151
+ isEditor: false
152
+ },
153
+ decorators: [
154
+ Story => (
155
+ <div style={{ width: 700 }}>
156
+ <Story />
157
+ </div>
158
+ )
159
+ ],
160
+ play: async ({ canvasElement }) => {
161
+ await assertVisualizationRendered(canvasElement)
162
+
163
+ const labels = [...canvasElement.querySelectorAll<SVGTextElement>('.dendrogram-chart__label')]
164
+ const links = [
165
+ ...canvasElement.querySelectorAll<SVGPathElement>('.dendrogram-chart__viewport .dendrogram-chart__link')
166
+ ]
167
+ expect(labels).toHaveLength(labelCollisionData.length)
168
+ expect(getComputedStyle(labels[0]).fontSize).toBe('16px')
169
+ expect(labels.some(label => links.some(link => labelOverlapsLink(label, link)))).toBe(false)
170
+
171
+ const canvas = within(canvasElement)
172
+ const linkStyles = canvas.getByRole('group', { name: 'Link styles' })
173
+ expect(within(linkStyles).getByText('Established relationships across programs')).toBeVisible()
174
+ expect(within(linkStyles).getByText('Provisional partnerships under review')).toBeVisible()
175
+ expect(getComputedStyle(linkStyles).overflowX).toBe('auto')
176
+ expect(getComputedStyle(linkStyles).whiteSpace).toBe('nowrap')
177
+ expect(getComputedStyle(linkStyles.querySelector('.dendrogram-chart__link-key-item')!).flexShrink).toBe('0')
178
+
179
+ const legendLines = linkStyles.querySelectorAll('line')
180
+ expect(legendLines).toHaveLength(2)
181
+ const renderedSolid = canvasElement.querySelector(
182
+ '.dendrogram-chart__viewport .dendrogram-chart__link:not(.dendrogram-chart__link--dashed)'
183
+ )!
184
+ const renderedDashed = canvasElement.querySelector('.dendrogram-chart__viewport .dendrogram-chart__link--dashed')!
185
+ expect(getComputedStyle(legendLines[0]).strokeDasharray).toBe(getComputedStyle(renderedSolid).strokeDasharray)
186
+ expect(getComputedStyle(legendLines[1]).strokeDasharray).toBe(getComputedStyle(renderedDashed).strokeDasharray)
187
+ }
188
+ }
189
+
190
+ export const Dendrogram_Vertical_Label_Collision: Story = {
191
+ args: {
192
+ config: createNewChartConfig({
193
+ ...dendrogramConfig,
194
+ title: 'Vertical Dendrogram Label Collision Characterization',
195
+ data: labelCollisionData,
196
+ dendrogram: {
197
+ ...dendrogramConfig.dendrogram,
198
+ orientation: 'vertical',
199
+ height: 480,
200
+ leafSpacing: 56,
201
+ depthSpacing: 150
202
+ }
203
+ }),
204
+ isEditor: false
205
+ },
206
+ decorators: [
207
+ Story => (
208
+ <div style={{ width: 760 }}>
209
+ <Story />
210
+ </div>
211
+ )
212
+ ],
213
+ play: async ({ canvasElement }) => {
214
+ await assertVisualizationRendered(canvasElement)
215
+ const labels = [...canvasElement.querySelectorAll<SVGTextElement>('.dendrogram-chart__label')]
216
+ const links = [
217
+ ...canvasElement.querySelectorAll<SVGPathElement>('.dendrogram-chart__viewport .dendrogram-chart__link')
218
+ ]
219
+
220
+ expect(labels).toHaveLength(labelCollisionData.length)
221
+ expect(labels.some(label => links.some(link => labelOverlapsLink(label, link)))).toBe(false)
222
+ }
223
+ }
224
+
225
+ export const Dendrogram_Editor: Story = {
226
+ args: { config: dendrogramConfig, isEditor: true },
227
+ play: async ({ canvasElement }) => {
228
+ const canvas = within(canvasElement)
229
+ await waitForEditor(canvas)
230
+ await openAccordion(canvas, 'Dendrogram')
231
+ expect(canvas.queryByLabelText('Show Tooltips')).not.toBeInTheDocument()
232
+ expect(canvas.queryByLabelText('Horizontal Alignment')).not.toBeInTheDocument()
233
+ expect(canvas.queryByLabelText('Vertical Alignment')).not.toBeInTheDocument()
234
+ expect(canvas.queryByLabelText('Show Node Labels')).not.toBeInTheDocument()
235
+
236
+ const nodeColumn = canvas.getByLabelText('Node ID Column')
237
+ await performAndAssert(
238
+ 'Node mapping validates the hierarchy',
239
+ () => canvasElement.querySelector('[role="alert"]')?.textContent || '',
240
+ () => userEvent.selectOptions(nodeColumn, 'nodeColor'),
241
+ (_before, after) => after.includes('references missing parent')
242
+ )
243
+ await userEvent.selectOptions(nodeColumn, 'node')
244
+
245
+ const parentColumn = canvas.getByLabelText('Parent ID Column')
246
+ await performAndAssert(
247
+ 'Parent mapping validates the hierarchy',
248
+ () => canvasElement.querySelector('[role="alert"]')?.textContent || '',
249
+ () => userEvent.selectOptions(parentColumn, 'style'),
250
+ (_before, after) => after.includes('references missing parent')
251
+ )
252
+ await userEvent.selectOptions(parentColumn, 'parent')
253
+
254
+ const linkStyleColumn = canvas.getByLabelText('Link Style Column (Optional)')
255
+ await performAndAssert(
256
+ 'Link style mapping changes rendered dashes',
257
+ () => canvasElement.querySelectorAll('.dendrogram-chart__viewport .dendrogram-chart__link--dashed').length,
258
+ () => userEvent.selectOptions(linkStyleColumn, ''),
259
+ (before, after) => before === 1 && after === 0
260
+ )
261
+ await userEvent.selectOptions(linkStyleColumn, 'style')
262
+
263
+ const getLegendCaptions = () =>
264
+ [...canvasElement.querySelectorAll('.dendrogram-chart__link-key-item span')].map(item => item.textContent)
265
+ const solidLinkLabel = canvas.getByLabelText('Solid Link Legend Label')
266
+ await performAndAssert(
267
+ 'Solid link legend label updates the rendered key',
268
+ getLegendCaptions,
269
+ async () => {
270
+ await userEvent.clear(solidLinkLabel)
271
+ await userEvent.type(solidLinkLabel, 'Established')
272
+ },
273
+ (before, after) => before.includes('Solid') && after.includes('Established')
274
+ )
275
+
276
+ const dashedLinkLabel = canvas.getByLabelText('Dashed Link Legend Label')
277
+ await performAndAssert(
278
+ 'Dashed link legend label updates the rendered key',
279
+ getLegendCaptions,
280
+ async () => {
281
+ await userEvent.clear(dashedLinkLabel)
282
+ await userEvent.type(dashedLinkLabel, 'Provisional')
283
+ },
284
+ (before, after) => before.includes('Dashed') && after.includes('Provisional')
285
+ )
286
+
287
+ const nodeColorColumn = canvas.getByLabelText('Node Color Column (Optional)')
288
+ await performAndAssert(
289
+ 'Node color mapping changes rendered fills',
290
+ () =>
291
+ [...canvasElement.querySelectorAll('.dendrogram-chart__node circle')].map(node => node.getAttribute('fill')),
292
+ () => userEvent.selectOptions(nodeColorColumn, ''),
293
+ (before, after) => new Set(before).size > 1 && new Set(after).size === 1
294
+ )
295
+ await userEvent.selectOptions(nodeColorColumn, 'nodeColor')
296
+
297
+ const orientation = canvas.getByLabelText('Orientation')
298
+ await performAndAssert(
299
+ 'Vertical orientation changes node placement',
300
+ () => canvasElement.querySelector('.dendrogram-chart__node')?.getAttribute('transform'),
301
+ () => userEvent.selectOptions(orientation, 'vertical'),
302
+ (before, after) => Boolean(before && after && before !== after && after.startsWith('translate('))
303
+ )
304
+
305
+ const connectionType = canvas.getByLabelText('Connection Type')
306
+ const getLinkPath = () =>
307
+ canvasElement.querySelector('.dendrogram-chart__viewport .dendrogram-chart__link')?.getAttribute('d') || ''
308
+ await performAndAssert(
309
+ 'Straight connections use direct line segments',
310
+ getLinkPath,
311
+ () => userEvent.selectOptions(connectionType, 'line'),
312
+ (before, after) => before.includes('C') && after.includes('L') && !after.includes('C')
313
+ )
314
+ await performAndAssert(
315
+ 'Elbow connections use orthogonal line segments',
316
+ getLinkPath,
317
+ () => userEvent.selectOptions(connectionType, 'elbow'),
318
+ (before, after) => before !== after && after.split('L').length === 4
319
+ )
320
+ await performAndAssert(
321
+ 'Curved connections restore the default path',
322
+ getLinkPath,
323
+ () => userEvent.selectOptions(connectionType, 'curve'),
324
+ (before, after) => before.includes('L') && after.includes('C')
325
+ )
326
+
327
+ const height = canvas.getByLabelText('Height')
328
+ await performAndAssert(
329
+ 'Height changes the chart container',
330
+ () => (canvasElement.querySelector('.dendrogram-chart__container') as HTMLElement)?.style.height,
331
+ () => fireEvent.change(height, { target: { value: '480' } }),
332
+ (before, after) => before === '420px' && after === '480px'
333
+ )
334
+
335
+ const nodeRadius = canvas.getByLabelText('Node Radius')
336
+ await performAndAssert(
337
+ 'Node radius changes rendered circles',
338
+ () => canvasElement.querySelector('.dendrogram-chart__node circle')?.getAttribute('r'),
339
+ () => fireEvent.change(nodeRadius, { target: { value: '10' } }),
340
+ (before, after) => before === '6' && after === '10'
341
+ )
342
+
343
+ const leafSpacing = canvas.getByLabelText('Leaf Spacing')
344
+ await performAndAssert(
345
+ 'Leaf spacing changes cross-axis placement',
346
+ () => canvasElement.querySelectorAll('.dendrogram-chart__node')[3]?.getAttribute('transform'),
347
+ () => fireEvent.change(leafSpacing, { target: { value: '45' } }),
348
+ (before, after) => Boolean(before && after && before !== after)
349
+ )
350
+
351
+ const depthSpacing = canvas.getByLabelText('Depth Spacing')
352
+ await performAndAssert(
353
+ 'Depth spacing changes hierarchy-level placement',
354
+ () => canvasElement.querySelectorAll('.dendrogram-chart__node')[3]?.getAttribute('transform'),
355
+ () => fireEvent.change(depthSpacing, { target: { value: '150' } }),
356
+ (before, after) => Boolean(before && after && before !== after)
357
+ )
358
+ }
359
+ }
360
+
361
+ export const Dendrogram_Validation: Story = {
362
+ args: {
363
+ config: createNewChartConfig({
364
+ ...dendrogramConfig,
365
+ data: [{ node: 'Child', parent: 'Missing' }]
366
+ }),
367
+ isEditor: false
368
+ },
369
+ play: async ({ canvasElement }) => {
370
+ await waitForPresence('[role="alert"]', canvasElement)
371
+ expect(within(canvasElement).getByRole('alert')).toHaveTextContent('references missing parent')
372
+ }
373
+ }
@@ -0,0 +1,236 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, fireEvent, fn, userEvent, waitFor, within } from 'storybook/test'
3
+ import cloneConfig from '@cdc/core/helpers/cloneConfig'
4
+ import { assertVisualizationRendered, openAccordion, performAndAssert, waitForEditor } from '@cdc/core/helpers/testing'
5
+
6
+ import Chart from '../CdcChartComponent'
7
+ import forecastComboWithGaps from './_mock/forecast_combo_with_gaps.json'
8
+
9
+ const meta: Meta<typeof Chart> = {
10
+ title: 'Components/Templates/Chart/Forecast/Confidence Interval Legend',
11
+ component: Chart
12
+ }
13
+
14
+ export default meta
15
+ type Story = StoryObj<typeof Chart>
16
+
17
+ const buildConfig = () => {
18
+ const config = cloneConfig(forecastComboWithGaps) as any
19
+ config.title = 'Forecast Confidence Intervals'
20
+ config.visualizationType = 'Forecasting'
21
+ config.legend.hide = false
22
+ config.legend.position = 'bottom'
23
+ config.legend.expandForecastConfidenceIntervals = true
24
+ config.xAxis.label = 'Date'
25
+ config.xAxis.tickRotation = 0
26
+ config.yAxis.label = 'Cases'
27
+ config.yAxis.max = ''
28
+ config.general.palette = { name: 'qualitative_standard', version: '2.0', isReversed: false }
29
+ config.columns = Object.fromEntries(
30
+ ['Lower 50%', 'Upper 50%', 'Lower 90%', 'Upper 90%'].map(name => [
31
+ name,
32
+ {
33
+ name,
34
+ label: name,
35
+ dataTable: true,
36
+ tooltips: name.includes('90%'),
37
+ prefix: '',
38
+ suffix: '',
39
+ roundToPlace: 0,
40
+ commas: false
41
+ }
42
+ ])
43
+ )
44
+ config.series = [
45
+ {
46
+ dataKey: 'Stage',
47
+ type: 'Forecasting',
48
+ axis: 'Left',
49
+ tooltip: false,
50
+ stageColumn: 'Stage',
51
+ stages: [
52
+ { key: 'Estimate', color: 'sequential-blue' },
53
+ { key: 'Forecast', color: 'sequential-orange' }
54
+ ],
55
+ confidenceIntervals: [
56
+ { high: 'Upper 90%', low: 'Lower 90%' },
57
+ { high: 'Upper 50%', low: 'Lower 50%' }
58
+ ]
59
+ }
60
+ ]
61
+ config.data = [
62
+ {
63
+ 'Target End Date': '01/01/2026',
64
+ Stage: 'Estimate',
65
+ 'Lower 50%': 90,
66
+ 'Upper 50%': 110,
67
+ 'Lower 90%': 75,
68
+ 'Upper 90%': 125
69
+ },
70
+ {
71
+ 'Target End Date': '01/08/2026',
72
+ Stage: 'Estimate',
73
+ 'Lower 50%': 98,
74
+ 'Upper 50%': 118,
75
+ 'Lower 90%': 82,
76
+ 'Upper 90%': 135
77
+ },
78
+ {
79
+ 'Target End Date': '01/15/2026',
80
+ Stage: 'Estimate',
81
+ 'Lower 50%': 105,
82
+ 'Upper 50%': 126,
83
+ 'Lower 90%': 88,
84
+ 'Upper 90%': 144
85
+ },
86
+ {
87
+ 'Target End Date': '01/22/2026',
88
+ Stage: 'Forecast',
89
+ 'Lower 50%': 101,
90
+ 'Upper 50%': 130,
91
+ 'Lower 90%': 80,
92
+ 'Upper 90%': 154
93
+ },
94
+ {
95
+ 'Target End Date': '01/29/2026',
96
+ Stage: 'Forecast',
97
+ 'Lower 50%': 96,
98
+ 'Upper 50%': 132,
99
+ 'Lower 90%': 72,
100
+ 'Upper 90%': 160
101
+ },
102
+ {
103
+ 'Target End Date': '02/05/2026',
104
+ Stage: 'Forecast',
105
+ 'Lower 50%': 91,
106
+ 'Upper 50%': 136,
107
+ 'Lower 90%': 65,
108
+ 'Upper 90%': 168
109
+ }
110
+ ]
111
+ return config
112
+ }
113
+
114
+ const tooltipConfigUpdates = fn()
115
+
116
+ const showForecastTooltip = async (canvasElement: HTMLElement) => {
117
+ const hitArea = canvasElement.querySelector<SVGRectElement>('.forecasting-items rect')
118
+ expect(hitArea).toBeTruthy()
119
+ const bounds = hitArea!.getBoundingClientRect()
120
+
121
+ fireEvent.mouseOut(hitArea!)
122
+ await waitFor(() => expect(canvasElement.querySelector('.tooltip.cove-visualization')).toBeNull())
123
+ fireEvent.mouseMove(hitArea!, {
124
+ clientX: bounds.left + bounds.width * 0.65,
125
+ clientY: bounds.top + bounds.height * 0.5
126
+ })
127
+
128
+ await waitFor(() => expect(canvasElement.querySelector('.tooltip.cove-visualization')).toBeTruthy())
129
+ }
130
+
131
+ const getTooltipBoundsState = (canvasElement: HTMLElement) => {
132
+ const tooltipText = canvasElement.querySelector('.tooltip.cove-visualization')?.textContent || ''
133
+
134
+ return {
135
+ has50Bounds: tooltipText.includes('Lower 50%') && tooltipText.includes('Upper 50%'),
136
+ has90Bounds: tooltipText.includes('Lower 90%') && tooltipText.includes('Upper 90%')
137
+ }
138
+ }
139
+
140
+ export const LegendRowsAndIndependentInteraction: Story = {
141
+ args: { config: buildConfig(), isEditor: false },
142
+ play: async ({ canvasElement }) => {
143
+ await assertVisualizationRendered(canvasElement)
144
+ const canvas = within(canvasElement)
145
+ const legendItems = canvas.getAllByRole('button').filter(button => button.closest('[aria-label="legend"]'))
146
+
147
+ expect(legendItems.map(item => item.textContent?.trim())).toEqual([
148
+ '50% CI Estimate',
149
+ '50% CI Forecast',
150
+ '90% CI Estimate',
151
+ '90% CI Forecast'
152
+ ])
153
+ expect(canvas.queryByRole('button', { name: 'Show All' })).not.toBeInTheDocument()
154
+
155
+ const innerMarker = legendItems[0].querySelector<HTMLElement>('span.legend-item')
156
+ const outerMarker = legendItems[2].querySelector<HTMLElement>('span.legend-item')
157
+ expect(innerMarker).toBeTruthy()
158
+ expect(outerMarker).toBeTruthy()
159
+ expect(getComputedStyle(innerMarker!).backgroundColor).not.toBe(getComputedStyle(outerMarker!).backgroundColor)
160
+
161
+ await userEvent.click(legendItems[0])
162
+ await waitFor(() => {
163
+ expect(legendItems[0]).toHaveClass('highlighted')
164
+ legendItems.slice(1).forEach(item => expect(item).toHaveClass('inactive'))
165
+ expect(canvas.getByRole('button', { name: 'Show All' })).toBeInTheDocument()
166
+
167
+ const forecastBands = Array.from(canvasElement.querySelectorAll<SVGGElement>('[data-forecast-legend-key]'))
168
+ const activeBands = forecastBands.filter(band => band.dataset.forecastActive === 'true')
169
+ expect(activeBands).toHaveLength(1)
170
+ expect(activeBands[0].dataset.forecastLegendKey).toBe('forecast-ci:Estimate:percentage%3A50')
171
+ expect(activeBands[0].querySelector('path')).not.toHaveAttribute('fill', 'transparent')
172
+ forecastBands
173
+ .filter(band => band !== activeBands[0])
174
+ .forEach(band => expect(band.querySelector('path')).toHaveAttribute('fill', 'transparent'))
175
+ })
176
+
177
+ await showForecastTooltip(canvasElement)
178
+ const tooltipText = canvasElement.querySelector('.tooltip.cove-visualization')?.textContent || ''
179
+ expect(getTooltipBoundsState(canvasElement)).toEqual({ has50Bounds: false, has90Bounds: true })
180
+ expect(tooltipText.match(/Lower 90%/g)).toHaveLength(1)
181
+ expect(tooltipText.match(/Upper 90%/g)).toHaveLength(1)
182
+
183
+ for (const legendItem of legendItems.slice(1)) {
184
+ await userEvent.click(legendItem)
185
+ }
186
+ await waitFor(() => {
187
+ legendItems.forEach(item => expect(item).toHaveClass('highlighted'))
188
+ expect(canvas.queryByRole('button', { name: 'Show All' })).not.toBeInTheDocument()
189
+ })
190
+ }
191
+ }
192
+
193
+ export const TooltipAuthoring: Story = {
194
+ args: { config: buildConfig(), isEditor: true, setConfig: tooltipConfigUpdates },
195
+ play: async ({ canvasElement }) => {
196
+ tooltipConfigUpdates.mockClear()
197
+ const canvas = within(canvasElement)
198
+ await waitForEditor(canvas)
199
+ await openAccordion(canvas, 'Columns')
200
+
201
+ const getColumnTooltipToggle = (columnName: string) => {
202
+ const columnButton = canvas
203
+ .getAllByRole('button', { name: new RegExp(columnName) })
204
+ .find(button => button.classList.contains('accordion__button')) as HTMLElement
205
+ expect(columnButton).toBeTruthy()
206
+ return within(columnButton.closest('.series-item--chart') as HTMLElement).getByRole('checkbox', {
207
+ name: 'Show in tooltip'
208
+ })
209
+ }
210
+
211
+ await showForecastTooltip(canvasElement)
212
+ expect(getTooltipBoundsState(canvasElement)).toEqual({ has50Bounds: false, has90Bounds: true })
213
+
214
+ await performAndAssert(
215
+ 'Include both 50% confidence interval bounds in tooltips',
216
+ () => getTooltipBoundsState(canvasElement),
217
+ async () => {
218
+ await userEvent.click(getColumnTooltipToggle('Lower 50%'))
219
+ await userEvent.click(getColumnTooltipToggle('Upper 50%'))
220
+ await showForecastTooltip(canvasElement)
221
+ },
222
+ (before, after) => !before.has50Bounds && after.has50Bounds && after.has90Bounds
223
+ )
224
+
225
+ await performAndAssert(
226
+ 'Remove both 50% confidence interval bounds from tooltips',
227
+ () => getTooltipBoundsState(canvasElement),
228
+ async () => {
229
+ await userEvent.click(getColumnTooltipToggle('Lower 50%'))
230
+ await userEvent.click(getColumnTooltipToggle('Upper 50%'))
231
+ await showForecastTooltip(canvasElement)
232
+ },
233
+ (before, after) => before.has50Bounds && !after.has50Bounds && after.has90Bounds
234
+ )
235
+ }
236
+ }