@cdc/chart 4.26.1 → 4.26.3

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 (173) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  4. package/dist/cdcchart.js +54742 -49796
  5. package/examples/data/data-with-metadata.json +10 -0
  6. package/examples/default.json +378 -0
  7. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  8. package/examples/feature/annotations/index.json +3 -6
  9. package/examples/feature/horizon/horizon-chart.json +395 -0
  10. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  11. package/examples/line-chart-states.json +1085 -0
  12. package/examples/metadata-variables.json +58 -0
  13. package/examples/private/123.json +694 -0
  14. package/examples/private/anchor-issue.json +4094 -0
  15. package/examples/private/backwards-slider.json +10430 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/timeline-data.json +1 -0
  18. package/examples/private/timeline.json +389 -0
  19. package/examples/radar-chart-simple.json +133 -0
  20. package/examples/radar-chart.json +148 -0
  21. package/index.html +1 -31
  22. package/package.json +57 -59
  23. package/src/CdcChart.tsx +8 -4
  24. package/src/CdcChartComponent.tsx +398 -284
  25. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  26. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  27. package/src/_stories/Chart.CI.stories.tsx +13 -0
  28. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  29. package/src/_stories/Chart.CustomColors.stories.tsx +78 -0
  30. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  31. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  32. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  33. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  34. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  35. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  36. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  37. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  38. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  39. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  40. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  41. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  42. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  43. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  44. package/src/_stories/Chart.stories.tsx +72 -1
  45. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  46. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  47. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  48. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  49. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  50. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  51. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  52. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  53. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  54. package/src/_stories/ChartBrush.stories.tsx +7 -0
  55. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  56. package/src/_stories/ChartEditor.stories.tsx +7 -0
  57. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  58. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  59. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  60. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  61. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  62. package/src/_stories/_mock/brush_continuous.json +86 -0
  63. package/src/_stories/_mock/brush_date_large.json +176 -0
  64. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  65. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  66. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  67. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  68. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  69. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  70. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  71. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  72. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  73. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  74. package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
  75. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  76. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  77. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  78. package/src/components/Annotations/components/AnnotationList.styles.css +12 -18
  79. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  80. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  81. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  82. package/src/components/Axis/BottomAxis.tsx +277 -0
  83. package/src/components/Axis/LeftAxis.tsx +404 -0
  84. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  85. package/src/components/Axis/PairedBarAxis.tsx +192 -0
  86. package/src/components/Axis/README.md +94 -0
  87. package/src/components/Axis/RightAxis.tsx +108 -0
  88. package/src/components/Axis/axis.constants.ts +21 -0
  89. package/src/components/Axis/index.ts +7 -0
  90. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  91. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  92. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  93. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  94. package/src/components/BarChart/components/BarChart.tsx +7 -1
  95. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  96. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  97. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  98. package/src/components/Brush/BrushSelector.tsx +155 -22
  99. package/src/components/Brush/MiniChartPreview.tsx +133 -21
  100. package/src/components/EditorPanel/EditorPanel.tsx +81 -54
  101. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
  102. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  103. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
  104. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
  105. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  106. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
  107. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
  108. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  109. package/src/components/EditorPanel/editor-panel.scss +1 -1
  110. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  111. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  112. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  113. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  114. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  115. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  116. package/src/components/HorizonChart/index.tsx +3 -0
  117. package/src/components/Legend/Legend.Component.tsx +52 -4
  118. package/src/components/Legend/Legend.tsx +1 -1
  119. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  120. package/src/components/Legend/LegendValueRange.tsx +77 -0
  121. package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
  122. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  123. package/src/components/LineChart/helpers/README.md +292 -0
  124. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  125. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  126. package/src/components/LineChart/index.tsx +44 -8
  127. package/src/components/LinearChart/README.md +109 -0
  128. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  129. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  130. package/src/components/LinearChart/tests/LinearChart.test.tsx +278 -0
  131. package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
  132. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  133. package/src/components/LinearChart.tsx +268 -1057
  134. package/src/components/PieChart/PieChart.tsx +20 -5
  135. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  136. package/src/components/RadarChart/RadarChart.tsx +298 -0
  137. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  138. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  139. package/src/components/RadarChart/helpers.ts +83 -0
  140. package/src/components/RadarChart/index.tsx +3 -0
  141. package/src/components/Regions/components/Regions.tsx +6 -6
  142. package/src/components/Sankey/components/Sankey.tsx +3 -3
  143. package/src/components/Sankey/sankey.scss +1 -1
  144. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  145. package/src/components/Sparkline/index.scss +4 -2
  146. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  147. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  148. package/src/data/initial-state.js +37 -15
  149. package/src/data/legacy-defaults.ts +18 -0
  150. package/src/helpers/abbreviateNumber.ts +24 -17
  151. package/src/helpers/getChartPatternId.ts +17 -0
  152. package/src/helpers/getExcludedData.ts +4 -0
  153. package/src/helpers/getMinMax.ts +16 -2
  154. package/src/helpers/handleChartAriaLabels.ts +19 -19
  155. package/src/helpers/handleLineType.ts +22 -18
  156. package/src/helpers/seriesColumnSettings.ts +114 -0
  157. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  158. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  159. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  160. package/src/hooks/useRightAxis.ts +14 -0
  161. package/src/hooks/useScales.ts +99 -56
  162. package/src/hooks/useTooltip.tsx +23 -3
  163. package/src/scss/main.scss +157 -79
  164. package/src/selectors/README.md +68 -0
  165. package/src/store/chart.reducer.ts +2 -0
  166. package/src/test/CdcChart.test.jsx +2 -2
  167. package/src/types/ChartConfig.ts +22 -0
  168. package/src/types/ChartContext.ts +1 -0
  169. package/src/types/Horizon.ts +64 -0
  170. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  171. package/tests/fixtures/data-with-metadata.json +10 -0
  172. package/preview.html +0 -1616
  173. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -4,6 +4,7 @@ import longXLabelsConfig from './_mock/large_x_axis_labels.json'
4
4
  import pairedBarConfig from './_mock/paired-bar.json'
5
5
  import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
6
6
  import { ChartConfig } from '../types/ChartConfig'
7
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
7
8
 
8
9
  const meta: Meta<typeof Chart> = {
9
10
  title: 'Components/Templates/Chart/Axis Titles',
@@ -17,6 +18,9 @@ type Story = StoryObj<typeof Chart>
17
18
  export const Dynamic_Labels: Story = {
18
19
  args: {
19
20
  config: editConfigKeys(longXLabelsConfig, [{ path: ['xAxis', 'label'], value: 'This is the title' }])
21
+ },
22
+ play: async ({ canvasElement }) => {
23
+ await assertVisualizationRendered(canvasElement)
20
24
  }
21
25
  }
22
26
 
@@ -43,11 +47,17 @@ export const Rotated_Labels: StoryObj<{ config: ChartConfig; tickRotation: numbe
43
47
  ])
44
48
 
45
49
  return <Chart config={config} />
50
+ },
51
+ play: async ({ canvasElement }) => {
52
+ await assertVisualizationRendered(canvasElement)
46
53
  }
47
54
  }
48
55
 
49
56
  export const Paired_Bar: Story = {
50
57
  args: {
51
58
  config: pairedBarConfig
59
+ },
60
+ play: async ({ canvasElement }) => {
61
+ await assertVisualizationRendered(canvasElement)
52
62
  }
53
63
  }
@@ -92,7 +92,7 @@ export const BarGeneralTests: Story = {
92
92
 
93
93
  const getChartSubtypeVisualization = () => {
94
94
  // Target the chart visualization SVG specifically, not editor UI icons
95
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
95
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
96
96
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
97
97
  const legendContainer = canvasElement.querySelector('.legend, [class*="legend"]')
98
98
 
@@ -160,7 +160,7 @@ export const BarGeneralTests: Story = {
160
160
 
161
161
  const getOrientationVisualization = () => {
162
162
  // Target the chart visualization SVG specifically, not editor UI icons
163
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
163
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
164
164
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
165
165
 
166
166
  // Look for bar elements - different structures for horizontal vs vertical
@@ -273,7 +273,7 @@ export const BarGeneralTests: Story = {
273
273
 
274
274
  const getBarStyleVisualization = () => {
275
275
  // Target the chart visualization SVG specifically, not editor UI icons
276
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
276
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
277
277
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
278
278
 
279
279
  return {
@@ -528,7 +528,7 @@ export const BarLeftValueAxisTests: Story = {
528
528
 
529
529
  const getAxisTypeVisualization = () => {
530
530
  // Target the chart visualization SVG specifically, not editor UI icons
531
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
531
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
532
532
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
533
533
 
534
534
  // Find the left axis specifically
@@ -693,7 +693,7 @@ export const BarLeftValueAxisTests: Story = {
693
693
 
694
694
  const getAxisLabelVisualization = () => {
695
695
  // Target the chart visualization SVG specifically, not editor UI icons
696
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
696
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
697
697
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
698
698
 
699
699
  // Find Y-axis label elements with multiple possible selectors
@@ -795,7 +795,9 @@ export const BarLeftValueAxisTests: Story = {
795
795
  expect(yAxisLabelInput.value).toBe('Custom Y-Axis Label')
796
796
 
797
797
  // Debug: Log the label element to confirm selection
798
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
798
+ const chartContainer = canvasElement.querySelector(
799
+ '.cove-visualization__body, .chart-container, .visualization'
800
+ )
799
801
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
800
802
  const labelElement = svg?.querySelector('text.y-label')
801
803
 
@@ -853,7 +855,7 @@ export const BarLeftValueAxisTests: Story = {
853
855
 
854
856
  const getInlineLabelVisualization = () => {
855
857
  // Target the chart visualization SVG specifically, not editor UI icons
856
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
858
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
857
859
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
858
860
 
859
861
  // Find the Y-axis area to locate the top tick area
@@ -989,7 +991,7 @@ export const BarLeftValueAxisTests: Story = {
989
991
 
990
992
  const getNumTicksVisualization = () => {
991
993
  // Target the chart visualization SVG specifically, not editor UI icons
992
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
994
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
993
995
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
994
996
 
995
997
  // Find the left axis specifically
@@ -1190,7 +1192,7 @@ export const DateCategoryAxisSectionTests: StoryObj<typeof Chart> = {
1190
1192
  // Method 2: Look for SVG in chart container areas
1191
1193
  if (!svgElement) {
1192
1194
  const chartContainer = canvasElement.querySelector(
1193
- '.cove-component__content, .chart-container, .visualization, .linear'
1195
+ '.cove-visualization__body, .chart-container, .visualization, .linear'
1194
1196
  )
1195
1197
  if (chartContainer) {
1196
1198
  svgElement = chartContainer.querySelector('svg')
@@ -1541,7 +1543,7 @@ export const BarRegionsSectionTests: Story = {
1541
1543
  await performAndAssert(
1542
1544
  'Configure Fixed-to-Fixed region with label and colors',
1543
1545
  () => {
1544
- const chartContainer = canvasElement.querySelector('.cove-component__content')
1546
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body')
1545
1547
  const chartSvg = chartContainer?.querySelector('svg')
1546
1548
  const regionElements = chartSvg?.querySelectorAll('rect[fill*="rgba"], rect[style*="rgba"]') || []
1547
1549
 
@@ -1680,6 +1682,17 @@ export const BarColumnsSectionTests: Story = {
1680
1682
  // Open Columns accordion
1681
1683
  await openAccordion(canvas, 'Columns')
1682
1684
 
1685
+ const getFirstColumnConfig = async () => {
1686
+ const columnSelect = (await canvas.findAllByLabelText(/^column$/i))[0]
1687
+ const fieldset = columnSelect.closest('fieldset')
1688
+
1689
+ if (!fieldset) {
1690
+ throw new Error('Unable to find the first column configuration fieldset')
1691
+ }
1692
+
1693
+ return within(fieldset as HTMLElement)
1694
+ }
1695
+
1683
1696
  // Test 1: Add first column configuration and verify fields appear
1684
1697
  await performAndAssert(
1685
1698
  'Add first column configuration and verify fields appear',
@@ -1712,9 +1725,10 @@ export const BarColumnsSectionTests: Story = {
1712
1725
  await performAndAssert(
1713
1726
  'Configure column with data column and custom label',
1714
1727
  () => {
1715
- const labelInputs = canvas.queryAllByLabelText(/^label$/i)
1728
+ const fieldsets = canvasElement.querySelectorAll('fieldset.edit-block')
1729
+ const labelInput = fieldsets[0]?.querySelector('input[name*="label"]') as HTMLInputElement | null
1716
1730
  return {
1717
- labelValue: (labelInputs[0] as HTMLInputElement)?.value || ''
1731
+ labelValue: labelInput?.value || ''
1718
1732
  }
1719
1733
  },
1720
1734
  async () => {
@@ -1724,8 +1738,8 @@ export const BarColumnsSectionTests: Story = {
1724
1738
  await userEvent.selectOptions(columnSelect, 'Year')
1725
1739
 
1726
1740
  // Set custom label
1727
- const labelInputs = await canvas.findAllByLabelText(/^label$/i)
1728
- const labelInput = labelInputs[0]
1741
+ const firstColumnConfig = await getFirstColumnConfig()
1742
+ const labelInput = await firstColumnConfig.findByLabelText(/^label$/i)
1729
1743
  await userEvent.clear(labelInput)
1730
1744
  await userEvent.type(labelInput, 'Report Year')
1731
1745
  },
@@ -1741,39 +1755,42 @@ export const BarColumnsSectionTests: Story = {
1741
1755
  await performAndAssert(
1742
1756
  'Enable tooltip display and configure number formatting',
1743
1757
  () => {
1744
- const tooltipCheckboxes = canvas.queryAllByLabelText(/show in tooltip/i)
1745
- const commasCheckboxes = canvas.queryAllByLabelText(/add commas to numbers/i)
1758
+ const firstColumnSelect = canvas.queryAllByLabelText(/^column$/i)[0]
1759
+ const fieldset = firstColumnSelect?.closest('fieldset')
1760
+ const scopedFieldset = fieldset ? within(fieldset as HTMLElement) : null
1761
+ const tooltipCheckbox = scopedFieldset?.queryByLabelText(/show in tooltip/i) as HTMLInputElement | null
1762
+ const commasCheckbox = scopedFieldset?.queryByLabelText(/add commas to numbers/i) as HTMLInputElement | null
1763
+ const prefixInput = scopedFieldset?.queryByLabelText(/^prefix$/i) as HTMLInputElement | null
1764
+ const suffixInput = scopedFieldset?.queryByLabelText(/^suffix$/i) as HTMLInputElement | null
1746
1765
 
1747
1766
  return {
1748
- tooltipChecked: (tooltipCheckboxes[0] as HTMLInputElement)?.checked || false,
1749
- commasChecked: (commasCheckboxes[0] as HTMLInputElement)?.checked || false
1767
+ tooltipChecked: tooltipCheckbox?.checked || false,
1768
+ commasChecked: commasCheckbox?.checked || false,
1769
+ prefixValue: prefixInput?.value || '',
1770
+ suffixValue: suffixInput?.value || ''
1750
1771
  }
1751
1772
  },
1752
1773
  async () => {
1774
+ const firstColumnConfig = await getFirstColumnConfig()
1775
+
1753
1776
  // Enable tooltip display
1754
- const tooltipCheckboxes = await canvas.findAllByLabelText(/show in tooltip/i)
1755
- const tooltipCheckbox = tooltipCheckboxes[0] as HTMLInputElement
1777
+ const tooltipCheckbox = (await firstColumnConfig.findByLabelText(/show in tooltip/i)) as HTMLInputElement
1756
1778
  if (!tooltipCheckbox.checked) {
1757
1779
  await userEvent.click(tooltipCheckbox)
1758
1780
  }
1759
1781
 
1760
1782
  // Enable commas for numbers
1761
- const commasCheckboxes = await canvas.findAllByLabelText(/add commas to numbers/i)
1762
- const commasCheckbox = commasCheckboxes[0] as HTMLInputElement
1783
+ const commasCheckbox = (await firstColumnConfig.findByLabelText(/add commas to numbers/i)) as HTMLInputElement
1763
1784
  if (!commasCheckbox.checked) {
1764
1785
  await userEvent.click(commasCheckbox)
1765
1786
  }
1766
1787
 
1767
1788
  // Add prefix and suffix
1768
- const prefixInputs = await canvas.findAllByLabelText(/prefix/i)
1769
-
1770
- const prefixInput = prefixInputs[1]
1771
-
1789
+ const prefixInput = await firstColumnConfig.findByLabelText(/^prefix$/i)
1772
1790
  await userEvent.clear(prefixInput)
1773
1791
  await userEvent.type(prefixInput, 'Year: ')
1774
1792
 
1775
- const suffixInputs = await canvas.findAllByLabelText(/suffix/i)
1776
- const suffixInput = suffixInputs[1]
1793
+ const suffixInput = await firstColumnConfig.findByLabelText(/^suffix$/i)
1777
1794
  await userEvent.clear(suffixInput)
1778
1795
  await userEvent.type(suffixInput, ' AD')
1779
1796
  },
@@ -1781,6 +1798,8 @@ export const BarColumnsSectionTests: Story = {
1781
1798
  // Checkboxes should be enabled
1782
1799
  expect(after.tooltipChecked).toBe(true)
1783
1800
  expect(after.commasChecked).toBe(true)
1801
+ expect(after.prefixValue).toBe('Year: ')
1802
+ expect(after.suffixValue).toBe(' AD')
1784
1803
 
1785
1804
  return true
1786
1805
  }
@@ -1905,7 +1924,7 @@ export const BarLegendTests: Story = {
1905
1924
  const rightLegend = canvasElement.querySelector('.legend-container.right')
1906
1925
  const bottomLegend = canvasElement.querySelector('.legend-container.bottom')
1907
1926
  const topLegend = canvasElement.querySelector('.legend-container.top')
1908
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
1927
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
1909
1928
 
1910
1929
  return {
1911
1930
  hasLeftLegend: !!leftLegend,
@@ -2385,7 +2404,7 @@ export const BarFiltersTests: Story = {
2385
2404
  // Helper function to get chart data visualization state
2386
2405
  // Tests VISUALIZATION OUTPUT (filtered data in chart) not control state
2387
2406
  const getChartDataState = () => {
2388
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
2407
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
2389
2408
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
2390
2409
  const bars = svg?.querySelectorAll('rect[class*="bar"], rect[data-testid*="bar"], g[class*="bar"] rect') || []
2391
2410
  const filtersList = canvasElement.querySelector('.draggable-field-list')
@@ -2687,7 +2706,7 @@ export const BarVisualTests: Story = {
2687
2706
  // Helper function to capture animation visualization state
2688
2707
  const getAnimationVisualizationState = () => {
2689
2708
  // Find the actual chart SVG, not UI icons or other SVGs
2690
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
2709
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
2691
2710
  const chartSvg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
2692
2711
 
2693
2712
  // Animation affects SVG classes and chart elements
@@ -2802,12 +2821,12 @@ export const BarVisualTests: Story = {
2802
2821
 
2803
2822
  // Helper function to capture bar border visualization state
2804
2823
  const getBarBorderVisualizationState = () => {
2805
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
2824
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
2806
2825
  const chartSvg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
2807
2826
 
2808
2827
  // Find bar elements in the chart
2809
2828
  const barElements =
2810
- chartSvg?.querySelectorAll('rect[class*="bar"], path[class*="bar"], g[class*="bar"] rect') || []
2829
+ chartSvg?.querySelectorAll('rect[class*="bar"], path[class*="bar"], g[class*="bar"] path') || []
2811
2830
 
2812
2831
  // Check for border-related styles and attributes
2813
2832
  const barsWithStroke = Array.from(barElements).filter(bar => {
@@ -2910,10 +2929,11 @@ export const BarPatternSettingsTests: Story = {
2910
2929
  type: 'continuous',
2911
2930
  dataKey: 'y1'
2912
2931
  },
2932
+ series: [{ dataKey: 'y1' }, { dataKey: 'y2' }, { dataKey: 'y3' }, { dataKey: 'y4' }],
2913
2933
  // Override with data suitable for pattern testing
2914
2934
  data: [
2915
2935
  { category: 'Q1', y1: 19000, y2: 47000, y3: 59000, y4: 91000 },
2916
- { category: 'Q2', y1: 18000, y2: 32000, y3: 68000, y4: 89000 },
2936
+ { category: 'Q2', y1: 18000, y2: 32000, y3: 19000, y4: 89000 },
2917
2937
  { category: 'Q3', y1: 7000, y2: 38000, y3: 74000, y4: 89000 },
2918
2938
  { category: 'Q4', y1: 15000, y2: 41000, y3: 67000, y4: 95000 }
2919
2939
  ],
@@ -2938,7 +2958,7 @@ export const BarPatternSettingsTests: Story = {
2938
2958
 
2939
2959
  // Helper function to capture SVG pattern visualization state
2940
2960
  const getPatternVisualizationState = () => {
2941
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
2961
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
2942
2962
  const chartSvg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
2943
2963
 
2944
2964
  // Find SVG <defs> section with pattern definitions
@@ -2948,8 +2968,10 @@ export const BarPatternSettingsTests: Story = {
2948
2968
  // Find pattern overlays (visual application of patterns)
2949
2969
  const patternOverlays = chartSvg?.querySelectorAll('.pattern-overlay') || []
2950
2970
 
2951
- // Find bars with pattern fills
2952
- const barsWithPatterns = chartSvg?.querySelectorAll('rect[fill*="url(#chart-pattern-"]') || []
2971
+ // Find bars with pattern fills (works for both fragment refs and absolute URL refs)
2972
+ const barsWithPatterns = Array.from(chartSvg?.querySelectorAll('path, rect') || []).filter(shape =>
2973
+ (shape.getAttribute('fill') || '').includes('chart-pattern-')
2974
+ )
2953
2975
 
2954
2976
  // Get pattern configuration UI state
2955
2977
  const patternConfigSections = canvasElement.querySelectorAll('.accordion__panel .accordion .accordion__item')
@@ -3103,6 +3125,43 @@ export const BarPatternSettingsTests: Story = {
3103
3125
  }
3104
3126
  )
3105
3127
 
3128
+ await performAndAssert(
3129
+ 'Clear Pattern Data Key - Existing data value applies across all series',
3130
+ getPatternVisualizationState,
3131
+ async () => {
3132
+ const dataKeyDropdown = canvasElement.querySelector('select[id*="pattern-datakey-"]') as HTMLSelectElement
3133
+
3134
+ if (dataKeyDropdown) {
3135
+ await userEvent.selectOptions(dataKeyDropdown, '')
3136
+ }
3137
+ },
3138
+ (before, after) => {
3139
+ // Clearing data key should keep value matching active and broaden the match across series.
3140
+ expect(after.hasBarsWithPatterns).toBe(true)
3141
+ expect(after.barsWithPatternsCount).toBeGreaterThan(before.barsWithPatternsCount)
3142
+
3143
+ return true
3144
+ }
3145
+ )
3146
+
3147
+ await performAndAssert(
3148
+ 'Clear Pattern Data Value - Pattern stops rendering when value is empty',
3149
+ getPatternVisualizationState,
3150
+ async () => {
3151
+ const dataValueInput = canvasElement.querySelector('input[id*="pattern-datavalue-"]') as HTMLInputElement
3152
+
3153
+ if (dataValueInput) {
3154
+ await userEvent.clear(dataValueInput)
3155
+ }
3156
+ },
3157
+ (before, after) => {
3158
+ expect(before.barsWithPatternsCount).toBeGreaterThan(0)
3159
+ expect(after.barsWithPatternsCount).toBe(0)
3160
+
3161
+ return true
3162
+ }
3163
+ )
3164
+
3106
3165
  // ========================================================================
3107
3166
  // Test Pattern Size Configuration - Pattern Density Changes
3108
3167
  // Tests how pattern size affects visual pattern rendering
@@ -3402,7 +3461,7 @@ export const BarTextAnnotationsTests: Story = {
3402
3461
 
3403
3462
  // Helper function to capture SVG annotation visualization state
3404
3463
  const getAnnotationVisualizationState = () => {
3405
- const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
3464
+ const chartContainer = canvasElement.querySelector('.cove-visualization__body, .chart-container, .visualization')
3406
3465
  const chartSvg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
3407
3466
 
3408
3467
  // Find annotation accordion sections (nested accordions for each annotation)
@@ -121,7 +121,12 @@ export const BrushDefaultSelectionTests: Story = {
121
121
  const getBrushSelectionState = () => {
122
122
  // The brush selection is represented by the visx-brush extent rect
123
123
  const brushExtent = canvasElement.querySelector('.visx-brush rect[class*="selection"]') as SVGRectElement
124
- const brushSvg = canvasElement.querySelector('svg[style*="border: 1px solid"]') as SVGSVGElement
124
+ const brushSvg =
125
+ (brushExtent?.closest('svg') as SVGSVGElement) ||
126
+ (canvasElement.querySelector('.visx-brush svg') as SVGSVGElement)
127
+ const brushWidthAttr = brushSvg ? parseFloat(brushSvg.getAttribute('width') || '0') : 0
128
+ const totalBrushWidth = brushSvg ? brushSvg.clientWidth || brushWidthAttr : 0
129
+ const countInput = canvasElement.querySelector('input[id*="brushDefaultRecentDateCount"]') as HTMLInputElement
125
130
 
126
131
  // Get the visible data points in the main chart (lines or bars)
127
132
  const chartSvg = canvasElement.querySelector('.linear-chart svg, .cove-chart svg') as SVGSVGElement
@@ -130,11 +135,12 @@ export const BrushDefaultSelectionTests: Story = {
130
135
  return {
131
136
  brushWidth: brushExtent ? parseFloat(brushExtent.getAttribute('width') || '0') : 0,
132
137
  brushX: brushExtent ? parseFloat(brushExtent.getAttribute('x') || '0') : 0,
133
- totalBrushWidth: brushSvg ? brushSvg.clientWidth : 0,
138
+ totalBrushWidth,
134
139
  visibleDataPoints: dataPointsInChart,
140
+ defaultRecentDateCountValue: countInput?.value || '',
135
141
  selectionPercentage:
136
- brushExtent && brushSvg
137
- ? (parseFloat(brushExtent.getAttribute('width') || '0') / brushSvg.clientWidth) * 100
142
+ brushExtent && totalBrushWidth > 0
143
+ ? (parseFloat(brushExtent.getAttribute('width') || '0') / totalBrushWidth) * 100
138
144
  : 0
139
145
  }
140
146
  }
@@ -176,7 +182,7 @@ export const BrushDefaultSelectionTests: Story = {
176
182
  },
177
183
  (before, after) => {
178
184
  // Changing from 30 to 50 should expand the brush width
179
- return after.brushWidth > before.brushWidth
185
+ return after.defaultRecentDateCountValue === '50' && after.brushWidth > before.brushWidth
180
186
  }
181
187
  )
182
188
 
@@ -226,26 +232,6 @@ export const BrushDefaultSelectionTests: Story = {
226
232
  expect(afterSettingCount.dataPointCount).toBeGreaterThan(0)
227
233
  expect(afterSettingCount.dataPointCount).toBeLessThanOrEqual(35) // 30 + tolerance
228
234
  }
229
-
230
- // ============================================================================
231
- // TEST: Clear Recent Date Count Returns to Default 35%
232
- // Verifies: When the count is cleared, brush returns to percentage-based default
233
- // ============================================================================
234
-
235
- await performAndAssert(
236
- 'Clear Default Recent Date Count (return to 35%)',
237
- getBrushSelectionState,
238
- async () => {
239
- await userEvent.clear(recentDateCountInput)
240
- await userEvent.tab()
241
- },
242
- (before, after) => {
243
- // The selection should return to approximately 35% of the total width
244
- // Since we had set it to 30 points (~9% with 329 data points),
245
- // clearing should expand it back to ~35%
246
- return after.brushWidth > before.brushWidth || after.selectionPercentage > before.selectionPercentage
247
- }
248
- )
249
235
  }
250
236
  }
251
237
 
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
4
+ import brushContinuousConfig from './_mock/brush_continuous.json'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+
7
+ const meta: Meta<typeof Chart> = {
8
+ title: 'Components/Templates/Chart/BrushSlider/Matrix/Continuous',
9
+ component: Chart
10
+ }
11
+
12
+ export default meta
13
+
14
+ type Story = StoryObj<typeof Chart>
15
+
16
+ export const Default: Story = {
17
+ args: { config: brushContinuousConfig, isEditor: false },
18
+ parameters: {
19
+ docs: {
20
+ description: { story: 'xAxis.type = "continuous", default ascending numeric order.' }
21
+ }
22
+ },
23
+ play: async ({ canvasElement }) => {
24
+ await assertVisualizationRendered(canvasElement)
25
+ }
26
+ }
27
+
28
+ export const Reversed: Story = {
29
+ args: {
30
+ config: editConfigKeys(brushContinuousConfig, [{ path: ['xAxis', 'sortByRecentDate'], value: true }]),
31
+ isEditor: false
32
+ },
33
+ parameters: {
34
+ docs: {
35
+ description: { story: 'xAxis.type = "continuous", sortByRecentDate = true.' }
36
+ }
37
+ },
38
+ play: async ({ canvasElement }) => {
39
+ await assertVisualizationRendered(canvasElement)
40
+ }
41
+ }
@@ -0,0 +1,114 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
4
+ import brushDateLargeConfig from './_mock/brush_date_large.json'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+
7
+ // Helper: add a "Category" column to every row (duplicating for two categories) and add a dropdown filter
8
+ function addFilter(config: any) {
9
+ const dataWithCategory = [
10
+ ...config.data.map((row: any) => ({ ...row, Category: 'Group A' })),
11
+ ...config.data.map((row: any) => ({ ...row, Category: 'Group B' }))
12
+ ]
13
+ return editConfigKeys(config, [
14
+ { path: ['data'], value: dataWithCategory },
15
+ {
16
+ path: ['filters'],
17
+ value: [
18
+ {
19
+ values: ['Group A', 'Group B'],
20
+ filterStyle: 'dropdown',
21
+ showDropdown: false,
22
+ active: 'Group A',
23
+ columnName: 'Category',
24
+ orderedValues: ['Group A', 'Group B'],
25
+ defaultValue: 'Group A'
26
+ }
27
+ ]
28
+ }
29
+ ])
30
+ }
31
+
32
+ const reversedDataConfig = editConfigKeys(brushDateLargeConfig, [
33
+ { path: ['data'], value: [...brushDateLargeConfig.data].reverse() }
34
+ ])
35
+
36
+ const meta: Meta<typeof Chart> = {
37
+ title: 'Components/Templates/Chart/BrushSlider/Matrix/Date',
38
+ component: Chart
39
+ }
40
+
41
+ export default meta
42
+
43
+ type Story = StoryObj<typeof Chart>
44
+
45
+ export const Default: Story = {
46
+ args: { config: brushDateLargeConfig, isEditor: false },
47
+ parameters: {
48
+ docs: {
49
+ description: {
50
+ story: 'xAxis.type = "date", default chronological order. ~104 weekly data points.'
51
+ }
52
+ }
53
+ },
54
+ play: async ({ canvasElement }) => {
55
+ await assertVisualizationRendered(canvasElement)
56
+ }
57
+ }
58
+
59
+ export const Reversed: Story = {
60
+ args: {
61
+ config: editConfigKeys(brushDateLargeConfig, [{ path: ['xAxis', 'sortByRecentDate'], value: true }]),
62
+ isEditor: false
63
+ },
64
+ parameters: {
65
+ docs: {
66
+ description: { story: 'xAxis.type = "date", sortByRecentDate = true. Most recent dates appear first.' }
67
+ }
68
+ },
69
+ play: async ({ canvasElement }) => {
70
+ await assertVisualizationRendered(canvasElement)
71
+ }
72
+ }
73
+
74
+ export const ReversedDataOrder: Story = {
75
+ args: { config: reversedDataConfig, isEditor: false },
76
+ parameters: {
77
+ docs: {
78
+ description: {
79
+ story:
80
+ 'Data array in reverse chronological order (newest-first in JSON). Verifies the brush sorts its domain correctly regardless of input order.'
81
+ }
82
+ }
83
+ },
84
+ play: async ({ canvasElement }) => {
85
+ await assertVisualizationRendered(canvasElement)
86
+ }
87
+ }
88
+
89
+ export const WithFilter: Story = {
90
+ args: { config: addFilter(brushDateLargeConfig), isEditor: false },
91
+ parameters: {
92
+ docs: {
93
+ description: { story: 'xAxis.type = "date" with a dropdown filter applied.' }
94
+ }
95
+ },
96
+ play: async ({ canvasElement }) => {
97
+ await assertVisualizationRendered(canvasElement)
98
+ }
99
+ }
100
+
101
+ export const Reversed_WithFilter: Story = {
102
+ args: {
103
+ config: addFilter(editConfigKeys(brushDateLargeConfig, [{ path: ['xAxis', 'sortByRecentDate'], value: true }])),
104
+ isEditor: false
105
+ },
106
+ parameters: {
107
+ docs: {
108
+ description: { story: 'sortByRecentDate = true with a dropdown filter.' }
109
+ }
110
+ },
111
+ play: async ({ canvasElement }) => {
112
+ await assertVisualizationRendered(canvasElement)
113
+ }
114
+ }
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
4
+ import brushEnabledConfig from './_mock/brush_enabled.json'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+
7
+ // Helper: add a "Category" column to every row (duplicating for two categories) and add a dropdown filter
8
+ function addFilter(config: any) {
9
+ const dataWithCategory = [
10
+ ...config.data.map((row: any) => ({ ...row, Category: 'Group A' })),
11
+ ...config.data.map((row: any) => ({ ...row, Category: 'Group B' }))
12
+ ]
13
+ return editConfigKeys(config, [
14
+ { path: ['data'], value: dataWithCategory },
15
+ {
16
+ path: ['filters'],
17
+ value: [
18
+ {
19
+ values: ['Group A', 'Group B'],
20
+ filterStyle: 'dropdown',
21
+ showDropdown: false,
22
+ active: 'Group A',
23
+ columnName: 'Category',
24
+ orderedValues: ['Group A', 'Group B'],
25
+ defaultValue: 'Group A'
26
+ }
27
+ ]
28
+ }
29
+ ])
30
+ }
31
+
32
+ const meta: Meta<typeof Chart> = {
33
+ title: 'Components/Templates/Chart/BrushSlider/Matrix/DateTime',
34
+ component: Chart
35
+ }
36
+
37
+ export default meta
38
+
39
+ type Story = StoryObj<typeof Chart>
40
+
41
+ export const Default: Story = {
42
+ args: { config: brushEnabledConfig, isEditor: false },
43
+ parameters: {
44
+ docs: {
45
+ description: { story: 'xAxis.type = "date-time", default chronological order.' }
46
+ }
47
+ },
48
+ play: async ({ canvasElement }) => {
49
+ await assertVisualizationRendered(canvasElement)
50
+ }
51
+ }
52
+
53
+ export const Reversed: Story = {
54
+ args: {
55
+ config: editConfigKeys(brushEnabledConfig, [{ path: ['xAxis', 'sortByRecentDate'], value: true }]),
56
+ isEditor: false
57
+ },
58
+ parameters: {
59
+ docs: {
60
+ description: { story: 'xAxis.type = "date-time", sortByRecentDate = true.' }
61
+ }
62
+ },
63
+ play: async ({ canvasElement }) => {
64
+ await assertVisualizationRendered(canvasElement)
65
+ }
66
+ }
67
+
68
+ export const WithFilter: Story = {
69
+ args: { config: addFilter(brushEnabledConfig), isEditor: false },
70
+ parameters: {
71
+ docs: {
72
+ description: { story: 'xAxis.type = "date-time" with a dropdown filter applied.' }
73
+ }
74
+ },
75
+ play: async ({ canvasElement }) => {
76
+ await assertVisualizationRendered(canvasElement)
77
+ }
78
+ }