@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
@@ -98,6 +98,27 @@ export const BrushDefaultSelectionTests: Story = {
98
98
  }
99
99
  )
100
100
 
101
+ const hideHatchingCheckbox = canvas.getByLabelText(/hide diagonal hatching/i) as HTMLInputElement
102
+ const getBrushHatchingState = () => {
103
+ const brushExtent = canvasElement.querySelector('.visx-brush rect[class*="selection"]') as SVGRectElement
104
+
105
+ return {
106
+ checked: hideHatchingCheckbox.checked,
107
+ fill: brushExtent?.getAttribute('fill') || brushExtent?.style.fill || ''
108
+ }
109
+ }
110
+
111
+ expect(hideHatchingCheckbox.checked).toBe(false)
112
+ expect(hideHatchingCheckbox.classList.contains('ms-4')).toBe(true)
113
+ expect(getBrushHatchingState().fill).toContain('brush_pattern')
114
+
115
+ await performAndAssert(
116
+ 'Hide Diagonal Hatching - Brush selection becomes transparent',
117
+ getBrushHatchingState,
118
+ async () => await userEvent.click(hideHatchingCheckbox),
119
+ (before, after) => !before.checked && after.checked && after.fill === 'transparent'
120
+ )
121
+
101
122
  // ============================================================================
102
123
  // TEST: Default Recent Date Count Input
103
124
  // Verifies: When "Show last X dates" is set, the brush selection changes
@@ -128,42 +149,34 @@ export const BrushDefaultSelectionTests: Story = {
128
149
  const totalBrushWidth = brushSvg ? brushSvg.clientWidth || brushWidthAttr : 0
129
150
  const countInput = canvasElement.querySelector('input[id*="brushDefaultRecentDateCount"]') as HTMLInputElement
130
151
 
131
- // Get the visible data points in the main chart (lines or bars)
132
- const chartSvg = canvasElement.querySelector('.linear-chart svg, .cove-chart svg') as SVGSVGElement
133
- const dataPointsInChart = chartSvg?.querySelectorAll('circle, rect:not([fill="transparent"])').length || 0
134
-
135
152
  return {
136
153
  brushWidth: brushExtent ? parseFloat(brushExtent.getAttribute('width') || '0') : 0,
137
154
  brushX: brushExtent ? parseFloat(brushExtent.getAttribute('x') || '0') : 0,
138
155
  totalBrushWidth,
139
- visibleDataPoints: dataPointsInChart,
140
- defaultRecentDateCountValue: countInput?.value || '',
141
- selectionPercentage:
142
- brushExtent && totalBrushWidth > 0
143
- ? (parseFloat(brushExtent.getAttribute('width') || '0') / totalBrushWidth) * 100
144
- : 0
156
+ defaultRecentDateCountValue: countInput?.value || ''
145
157
  }
146
158
  }
147
159
 
148
160
  const initialSelectionState = getBrushSelectionState()
161
+ const selectionMatchesRecentDateCount = (state: ReturnType<typeof getBrushSelectionState>, count: number) => {
162
+ const expectedWidth = (state.totalBrushWidth * count) / brushEnabledConfig.data.length
163
+ const rightEdge = state.brushX + state.brushWidth
164
+
165
+ return Math.abs(state.brushWidth - expectedWidth) <= 2 && Math.abs(rightEdge - state.totalBrushWidth) <= 2
166
+ }
149
167
 
150
168
  // Default is ~35% of the width
151
- // With 329 data points in brush_enabled.json, 35% would be ~115 points
152
- // Setting to 30 should make the selection much narrower AND update immediately
169
+ // With 253 data points in brush_enabled.json, 35% is about 89 points.
170
+ // Setting the count to 30 should make the selection narrower and update immediately.
153
171
  await performAndAssert(
154
172
  'Set Default Recent Date Count to 30 - Brush Updates Dynamically',
155
173
  getBrushSelectionState,
156
174
  async () => {
157
- await userEvent.clear(recentDateCountInput)
158
175
  await userEvent.type(recentDateCountInput, '30')
159
- // Trigger change/blur to apply the value - this should update the brush immediately
160
176
  await userEvent.tab()
161
177
  },
162
178
  (before, after) => {
163
- // The brush selection width should decrease when we set a smaller count
164
- // Since we have ~329 data points and default is 35% (~115 points),
165
- // setting to 30 points should make the selection narrower
166
- return after.brushWidth < before.brushWidth || after.selectionPercentage < before.selectionPercentage
179
+ return after.brushWidth < before.brushWidth && selectionMatchesRecentDateCount(after, 30)
167
180
  }
168
181
  )
169
182
 
@@ -172,66 +185,32 @@ export const BrushDefaultSelectionTests: Story = {
172
185
  // Verifies: Multiple changes continue to update the brush dynamically
173
186
  // ============================================================================
174
187
 
188
+ // Wait for the debounced clear to reach the chart before typing the next value.
189
+ // Otherwise the pending empty value can overwrite the newly typed count under CI load.
175
190
  await performAndAssert(
176
- 'Change to 50 dates - Brush Expands Dynamically',
191
+ 'Clear Default Recent Date Count',
177
192
  getBrushSelectionState,
178
193
  async () => {
179
194
  await userEvent.clear(recentDateCountInput)
180
- await userEvent.type(recentDateCountInput, '50')
181
195
  await userEvent.tab()
182
196
  },
183
197
  (before, after) => {
184
- // Changing from 30 to 50 should expand the brush width
185
- return after.defaultRecentDateCountValue === '50' && after.brushWidth > before.brushWidth
198
+ const widthDeltaFromInitial = Math.abs(after.brushWidth - initialSelectionState.brushWidth)
199
+ return after.brushWidth > before.brushWidth && widthDeltaFromInitial <= 2
186
200
  }
187
201
  )
188
202
 
189
- // ============================================================================
190
- // TEST: Verify Exact Data Point Count in Selection
191
- // Verifies: The number of selected data points matches the input value
192
- // ============================================================================
193
-
194
- // The brush selection should now show exactly 30 data points
195
- // We can verify this by checking the filtered data in the visualization
196
- const getSelectedDataPointCount = () => {
197
- // When brush is active, only the selected data points are rendered in the main chart
198
- // Look for the number of data points (circles, bars, or line path points)
199
- const chartContainer = canvasElement.querySelector('.linear-chart, .cove-chart')
200
- const svg = chartContainer?.querySelector('svg')
201
-
202
- // For line charts, count the line path data points
203
- const linePaths = svg?.querySelectorAll('path[class*="line"], .visx-linepath')
204
- let dataPointCount = 0
205
-
206
- if (linePaths && linePaths.length > 0) {
207
- // Count points in the path by looking at the rendered circles
208
- const circles = svg?.querySelectorAll('circle')
209
- dataPointCount = circles?.length || 0
210
- }
211
-
212
- // For bar charts
213
- const bars = svg?.querySelectorAll('rect[class*="bar"]')
214
- if (bars && bars.length > 0) {
215
- dataPointCount = bars.length
216
- }
217
-
218
- return {
219
- dataPointCount,
220
- hasData: dataPointCount > 0
203
+ await performAndAssert(
204
+ 'Set to 50 dates - Brush Updates Dynamically',
205
+ getBrushSelectionState,
206
+ async () => {
207
+ await userEvent.type(recentDateCountInput, '50')
208
+ await userEvent.tab()
209
+ },
210
+ (before, after) => {
211
+ return after.brushWidth < before.brushWidth && selectionMatchesRecentDateCount(after, 50)
221
212
  }
222
- }
223
-
224
- // After setting to 30, the chart should show ~30 data points
225
- // (exact count may vary based on how brush boundaries align with data points)
226
- const afterSettingCount = getSelectedDataPointCount()
227
-
228
- // The count should be close to 30 (allowing some tolerance for edge cases)
229
- // Note: This assertion helps verify the feature works - if it fails,
230
- // the implementation needs adjustment
231
- if (afterSettingCount.hasData) {
232
- expect(afterSettingCount.dataPointCount).toBeGreaterThan(0)
233
- expect(afterSettingCount.dataPointCount).toBeLessThanOrEqual(35) // 30 + tolerance
234
- }
213
+ )
235
214
  }
236
215
  }
237
216
 
@@ -0,0 +1,135 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, waitFor } from 'storybook/test'
3
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
4
+ import Chart from '../CdcChartComponent'
5
+ import brushEnabledConfig from './_mock/brush_enabled.json'
6
+
7
+ const brushEffectConfig = {
8
+ ...brushEnabledConfig,
9
+ title: 'Brush Slider with Limited Coverage Markers',
10
+ xAxis: {
11
+ ...brushEnabledConfig.xAxis,
12
+ type: 'date',
13
+ brushDefaultRecentDateCount: 4
14
+ },
15
+ data: brushEnabledConfig.data.slice(0, 8).map(row => ({
16
+ ...row,
17
+ Coverage: 'Limited Coverage'
18
+ })),
19
+ preliminaryData: [
20
+ {
21
+ type: 'effect',
22
+ seriesKeys: ['Production'],
23
+ label: 'Limited Coverage',
24
+ column: 'Coverage',
25
+ value: 'Limited Coverage',
26
+ style: 'Open Circles',
27
+ displayTooltip: true,
28
+ displayLegend: true,
29
+ displayTable: true,
30
+ circleSize: 4
31
+ }
32
+ ]
33
+ }
34
+
35
+ const brushSuppressionConfig = {
36
+ ...brushEnabledConfig,
37
+ title: 'Brush Slider with Suppressed Bars',
38
+ visualizationType: 'Bar',
39
+ visualizationSubType: 'regular',
40
+ xAxis: {
41
+ ...brushEnabledConfig.xAxis,
42
+ type: 'date',
43
+ brushDefaultRecentDateCount: 4
44
+ },
45
+ general: {
46
+ showMissingDataLabel: true,
47
+ showSuppressedSymbol: true,
48
+ showZeroValueData: true
49
+ },
50
+ data: brushEnabledConfig.data.slice(0, 8).map((row, index) => ({
51
+ ...row,
52
+ Production: index === 0 ? 'Suppressed' : row.Production
53
+ })),
54
+ series: brushEnabledConfig.series.slice(0, 2).map(series => ({
55
+ ...series,
56
+ type: 'Bar'
57
+ })),
58
+ preliminaryData: [
59
+ {
60
+ type: 'suppression',
61
+ seriesKeys: ['Production'],
62
+ label: 'Suppressed',
63
+ column: 'Production',
64
+ value: 'Suppressed',
65
+ style: '',
66
+ displayTooltip: true,
67
+ displayLegend: true,
68
+ displayTable: true,
69
+ symbol: 'Dagger',
70
+ iconCode: '†',
71
+ hideBarSymbol: false
72
+ }
73
+ ]
74
+ }
75
+
76
+ const meta: Meta<typeof Chart> = {
77
+ title: 'Components/Templates/Chart/BrushSlider/Regression',
78
+ component: Chart
79
+ }
80
+
81
+ export default meta
82
+
83
+ type Story = StoryObj<typeof Chart>
84
+
85
+ export const EffectMarkersStayInsideSelection: Story = {
86
+ args: {
87
+ config: brushEffectConfig,
88
+ isEditor: false
89
+ },
90
+ parameters: {
91
+ docs: {
92
+ description: {
93
+ story: 'Effect markers render only for dates inside the active brush selection.'
94
+ }
95
+ }
96
+ },
97
+ play: async ({ canvasElement }) => {
98
+ await assertVisualizationRendered(canvasElement)
99
+
100
+ await waitFor(() => {
101
+ const effectCircles = Array.from(canvasElement.querySelectorAll('.line-chart-group > g > circle'))
102
+
103
+ expect(effectCircles).toHaveLength(4)
104
+ effectCircles.forEach(circle => {
105
+ expect(Number.isFinite(Number(circle.getAttribute('cx')))).toBe(true)
106
+ })
107
+ })
108
+ }
109
+ }
110
+
111
+ export const SuppressedBarsStayInsideSelection: Story = {
112
+ args: {
113
+ config: brushSuppressionConfig,
114
+ isEditor: false
115
+ },
116
+ parameters: {
117
+ docs: {
118
+ description: {
119
+ story: 'Suppression metadata does not cause bars outside the active brush selection to render at the origin.'
120
+ }
121
+ }
122
+ },
123
+ play: async ({ canvasElement }) => {
124
+ await assertVisualizationRendered(canvasElement)
125
+
126
+ await waitFor(() => {
127
+ const barGroups = Array.from(canvasElement.querySelectorAll('g[class*="bar-group-"]'))
128
+
129
+ expect(barGroups).toHaveLength(4)
130
+ barGroups.forEach(group => {
131
+ expect(group.getAttribute('transform')).not.toContain('NaN')
132
+ })
133
+ })
134
+ }
135
+ }
@@ -46,6 +46,19 @@ export const GeneralSectionTests: Story = {
46
46
  },
47
47
  play: async ({ canvasElement }) => {
48
48
  const canvas = within(canvasElement)
49
+ const documentCanvas = within(canvasElement.ownerDocument.body)
50
+
51
+ const expectTooltipContent = async (field: HTMLElement, expectedText: string) => {
52
+ const tooltipTarget = field.closest('label')?.querySelector('.cove-tooltip--target') as HTMLElement
53
+ expect(tooltipTarget).toBeTruthy()
54
+
55
+ await userEvent.hover(tooltipTarget)
56
+
57
+ const tooltipContent = await documentCanvas.findByText(expectedText)
58
+ expect(tooltipContent).toBeTruthy()
59
+
60
+ await userEvent.unhover(tooltipTarget)
61
+ }
49
62
 
50
63
  // Wait for editor to load
51
64
  await waitForEditor(canvas)
@@ -232,6 +245,11 @@ export const GeneralSectionTests: Story = {
232
245
  })() as HTMLInputElement
233
246
 
234
247
  if (superTitleInput) {
248
+ await expectTooltipContent(
249
+ superTitleInput,
250
+ 'To keep the main title as short as possible, consider adding extra details in a super title, which displays in small font just above the main title in the visualization’s title bar. (If you hide the regular title, the super title does not display.)'
251
+ )
252
+
235
253
  await performAndAssert(
236
254
  'Super Title Text Change',
237
255
  getSuperTitleText,
@@ -295,6 +313,11 @@ export const GeneralSectionTests: Story = {
295
313
  })() as HTMLTextAreaElement
296
314
  expect(messageInput).toBeTruthy()
297
315
 
316
+ await expectTooltipContent(
317
+ messageInput,
318
+ 'Explain how to use the chart, introduce any special concepts, etc. This text displays above the visualization (and below the title bar, if displayed).'
319
+ )
320
+
298
321
  await performAndAssert(
299
322
  'Message Text Change',
300
323
  getMessageText,
@@ -406,6 +429,11 @@ export const GeneralSectionTests: Story = {
406
429
  })() as HTMLTextAreaElement
407
430
 
408
431
  if (footnotesInput) {
432
+ await expectTooltipContent(
433
+ footnotesInput,
434
+ 'Use this text element for any supplemental information that should display below the data table.'
435
+ )
436
+
409
437
  await performAndAssert(
410
438
  'Footnotes Text Change',
411
439
  getFootnotesText,
@@ -670,9 +698,11 @@ export const ComboRightValueAxisSectionTests: Story = {
670
698
  await waitForEditor(canvas)
671
699
  await openAccordion(canvas, 'Right Value Axis')
672
700
 
673
- const rightAxisLabelOffsetInput = canvas.getByLabelText(/^label offset$/i) as HTMLInputElement
674
- expect(rightAxisLabelOffsetInput).toBeTruthy()
675
- expect(rightAxisLabelOffsetInput.name).toContain('rightLabelOffsetSize')
701
+ expect(canvas.queryByLabelText(/^label offset$/i)).toBeFalsy()
702
+
703
+ const labelPlacementSelect = canvasElement.querySelector('select[name="rightTitlePlacement"]') as HTMLSelectElement
704
+ expect(labelPlacementSelect).toBeTruthy()
705
+ expect(labelPlacementSelect.value).toBe('side')
676
706
  }
677
707
  }
678
708
 
@@ -0,0 +1,120 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect, userEvent, within } from 'storybook/test'
3
+ import { assertVisualizationRendered, openAccordion, performAndAssert, waitForEditor } from '@cdc/core/helpers/testing'
4
+ import Chart from '../CdcChartComponent'
5
+ import baseConfig from './_mock/line_chart_dashed_dates.json'
6
+
7
+ const config = {
8
+ ...baseConfig,
9
+ animate: false,
10
+ title: 'Preliminary line effect compatibility',
11
+ data: [
12
+ { date: '2024-10-01', Value: '52', Status: 'final' },
13
+ { date: '2025-01-01', Value: '57', Status: 'preliminary' },
14
+ { date: '2025-04-01', Value: '54', Status: 'preliminary' }
15
+ ]
16
+ }
17
+
18
+ const getIncomingPath = (canvasElement: HTMLElement) =>
19
+ canvasElement.querySelector<SVGPathElement>('.line-chart-group path.visx-linepath')
20
+
21
+ const meta: Meta<typeof Chart> = {
22
+ title: 'Components/Templates/Chart/Special Lines/Compatibility',
23
+ component: Chart
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Chart>
28
+
29
+ export const ExistingPreliminaryRule: Story = {
30
+ args: { config, isEditor: true },
31
+ play: async ({ canvasElement }) => {
32
+ const canvas = within(canvasElement)
33
+ await waitForEditor(canvas)
34
+ await assertVisualizationRendered(canvasElement)
35
+ const incomingPath = getIncomingPath(canvasElement)
36
+ expect(incomingPath).toHaveAttribute('stroke-dasharray', '5 5')
37
+ expect(incomingPath.getTotalLength()).toBeGreaterThan(1)
38
+ const originalPath = incomingPath.getAttribute('d')
39
+
40
+ await openAccordion(canvas, 'Data Series')
41
+ const effect = within(
42
+ canvasElement.querySelector<HTMLElement>('[data-rfd-draggable-id="preliminaryData-0"] .accordion')
43
+ )
44
+ await openAccordion(effect, 'Effect Data 1')
45
+ expect(effect.getByLabelText('Apply style to')).toHaveValue('')
46
+ await performAndAssert(
47
+ 'Editing an existing rule preserves its incoming dashed segment',
48
+ () => canvasElement.querySelector('.legend-preliminary')?.textContent,
49
+ async () => {
50
+ const label = effect.getByLabelText('Label', { exact: true })
51
+ await userEvent.clear(label)
52
+ await userEvent.type(label, 'Updated preliminary')
53
+ },
54
+ (_, after) => after?.includes('Updated preliminary'),
55
+ () => {
56
+ expect(getIncomingPath(canvasElement)).toHaveAttribute('stroke-dasharray', '5 5')
57
+ expect(getIncomingPath(canvasElement)).toHaveAttribute('d', originalPath)
58
+ }
59
+ )
60
+ }
61
+ }
62
+
63
+ export const SavedOutgoingRule: Story = {
64
+ args: {
65
+ config: {
66
+ ...config,
67
+ preliminaryData: config.preliminaryData.map(rule => ({ ...rule, lineEffectScope: 'outgoing' }))
68
+ },
69
+ isEditor: false
70
+ },
71
+ play: async ({ canvasElement }) => {
72
+ await assertVisualizationRendered(canvasElement)
73
+ expect(getIncomingPath(canvasElement)).toHaveAttribute('stroke-dasharray', '0')
74
+ const outgoingPath = canvasElement.querySelector<SVGPathElement>('.line-chart-group path[stroke-dasharray="5 5"]')
75
+ expect(outgoingPath).not.toBeNull()
76
+ expect(outgoingPath.getTotalLength()).toBeGreaterThan(1)
77
+ }
78
+ }
79
+
80
+ export const NewlyAddedRule: Story = {
81
+ args: { config: { ...config, preliminaryData: [] }, isEditor: true },
82
+ play: async ({ canvasElement }) => {
83
+ const canvas = within(canvasElement)
84
+ await waitForEditor(canvas)
85
+ await assertVisualizationRendered(canvasElement)
86
+ await openAccordion(canvas, 'Data Series')
87
+ await userEvent.click(canvas.getByRole('button', { name: 'Add Special Line' }))
88
+ const effect = within(
89
+ canvasElement.querySelector<HTMLElement>('[data-rfd-draggable-id="preliminaryData-0"] .accordion')
90
+ )
91
+ await openAccordion(effect, 'Effect Data 1')
92
+ await userEvent.click(effect.getByRole('button', { name: 'Expand', exact: true }))
93
+ await userEvent.click(within(effect.getByRole('list')).getByRole('option', { name: 'Value', exact: true }))
94
+ await userEvent.selectOptions(effect.getByLabelText('COLUMN WITH CONFIGURATION VALUE'), 'Status')
95
+ await userEvent.type(effect.getByLabelText(/VALUE TO TRIGGER/), 'preliminary')
96
+ await performAndAssert(
97
+ 'A newly added rule styles only the outgoing interval',
98
+ () => canvasElement.querySelectorAll('.line-chart-group path[stroke-dasharray="5 5"]').length,
99
+ () => userEvent.selectOptions(effect.getByLabelText('Style', { exact: true }), 'Dashed Small'),
100
+ (before, after) => before === 0 && after > 0,
101
+ () => {
102
+ expect(getIncomingPath(canvasElement)).toHaveAttribute('stroke-dasharray', '0')
103
+ const outgoingPath = canvasElement.querySelector<SVGPathElement>(
104
+ '.line-chart-group path[stroke-dasharray="5 5"]'
105
+ )
106
+ expect(outgoingPath.getTotalLength()).toBeGreaterThan(1)
107
+ }
108
+ )
109
+ expect(effect.getByLabelText('Apply style to')).toHaveValue('outgoing')
110
+ await performAndAssert(
111
+ 'A newly added rule can also style the incoming interval',
112
+ () => getIncomingPath(canvasElement)?.getAttribute('stroke-dasharray'),
113
+ () => userEvent.selectOptions(effect.getByLabelText('Apply style to'), ''),
114
+ (before, after) => before === '0' && after === '5 5',
115
+ () => expect(getIncomingPath(canvasElement).getTotalLength()).toBeGreaterThan(1)
116
+ )
117
+ await userEvent.selectOptions(effect.getByLabelText('Style', { exact: true }), 'Open Circles')
118
+ expect(effect.queryByLabelText('Apply style to')).not.toBeInTheDocument()
119
+ }
120
+ }
@@ -450,8 +450,8 @@
450
450
  },
451
451
  "heatmap": {
452
452
  "cellPadding": 1,
453
- "rowLabelGap": 32,
454
- "columnLabelGap": 56,
453
+ "rowLabelGap": 0,
454
+ "columnLabelGap": 15,
455
455
  "xAxisPosition": "top",
456
456
  "showCellValues": false
457
457
  },
@@ -283,23 +283,29 @@
283
283
  "visualizationType": "Sankey",
284
284
  "theme": "theme-purple",
285
285
  "fontSize": "medium",
286
+ "enableTooltips": true,
287
+ "general": {
288
+ "palette": {
289
+ "isReversed": true,
290
+ "version": "2.0",
291
+ "name": "sequential_bluereverse"
292
+ }
293
+ },
286
294
  "sankey": {
287
- "title": {
288
- "defaultColor": "black"
289
- },
290
295
  "iterations": 1,
291
296
  "rxValue": 0.9,
292
- "overallSize": {
293
- "width": 900,
294
- "height": 700
295
- },
297
+ "horizontalScrollWidth": 900,
296
298
  "margin": {
297
299
  "margin_y": 25,
298
300
  "margin_x": 0
299
301
  },
302
+ "columns": {
303
+ "source": "example_source",
304
+ "target": "example_target",
305
+ "value": "example_value"
306
+ },
300
307
  "nodeSize": {
301
- "nodeWidth": 26,
302
- "nodeHeight": 40
308
+ "nodeWidth": 26
303
309
  },
304
310
  "nodePadding": 55,
305
311
  "nodeFontColor": "black",
@@ -314,8 +320,12 @@
314
320
  "opacity": {
315
321
  "nodeOpacityDefault": 1,
316
322
  "nodeOpacityInactive": 0.1,
317
- "LinkOpacityDefault": 1,
323
+ "LinkOpacityDefault": 0.6,
318
324
  "LinkOpacityInactive": 0.1
325
+ },
326
+ "nodeValueStyle": {
327
+ "textBefore": "(",
328
+ "textAfter": ")"
319
329
  }
320
330
  },
321
331
  "visual": {
@@ -346,48 +346,44 @@
346
346
 
347
347
  "sankey_test_data": [
348
348
  {
349
- "links": [
350
- {
351
- "source": "Initial Population",
352
- "target": "Screened",
353
- "value": 10000
354
- },
355
- {
356
- "source": "Initial Population",
357
- "target": "Not Screened",
358
- "value": 5000
359
- },
360
- {
361
- "source": "Screened",
362
- "target": "Positive Result",
363
- "value": 1500
364
- },
365
- {
366
- "source": "Screened",
367
- "target": "Negative Result",
368
- "value": 8500
369
- },
370
- {
371
- "source": "Positive Result",
372
- "target": "Treated",
373
- "value": 1200
374
- },
375
- {
376
- "source": "Positive Result",
377
- "target": "Not Treated",
378
- "value": 300
379
- },
380
- {
381
- "source": "Treated",
382
- "target": "Recovery",
383
- "value": 1000
384
- },
385
- {
386
- "source": "Treated",
387
- "target": "No Recovery",
388
- "value": 200
389
- }
390
- ]
349
+ "example_source": "Initial Population",
350
+ "example_target": "Screened",
351
+ "example_value": 10000
352
+ },
353
+ {
354
+ "example_source": "Initial Population",
355
+ "example_target": "Not Screened",
356
+ "example_value": 5000
357
+ },
358
+ {
359
+ "example_source": "Screened",
360
+ "example_target": "Positive Result",
361
+ "example_value": 1500
362
+ },
363
+ {
364
+ "example_source": "Screened",
365
+ "example_target": "Negative Result",
366
+ "example_value": 8500
367
+ },
368
+ {
369
+ "example_source": "Positive Result",
370
+ "example_target": "Treated",
371
+ "example_value": 1200
372
+ },
373
+ {
374
+ "example_source": "Positive Result",
375
+ "example_target": "Not Treated",
376
+ "example_value": 300
377
+ },
378
+ {
379
+ "example_source": "Treated",
380
+ "example_target": "Recovery",
381
+ "example_value": 1000
382
+ },
383
+ {
384
+ "example_source": "Treated",
385
+ "example_target": "No Recovery",
386
+ "example_value": 200
391
387
  }
392
388
  ],
393
389
 
@@ -268,8 +268,8 @@
268
268
  },
269
269
  "heatmap": {
270
270
  "cellPadding": 1,
271
- "rowLabelGap": 32,
272
- "columnLabelGap": 56,
271
+ "rowLabelGap": 0,
272
+ "columnLabelGap": 15,
273
273
  "xAxisPosition": "top",
274
274
  "showCellValues": false
275
275
  },
@@ -108,8 +108,8 @@
108
108
  },
109
109
  "heatmap": {
110
110
  "cellPadding": 1,
111
- "rowLabelGap": 32,
112
- "columnLabelGap": 56,
111
+ "rowLabelGap": 0,
112
+ "columnLabelGap": 15,
113
113
  "xAxisPosition": "top",
114
114
  "showCellValues": false
115
115
  },