@cdc/chart 4.26.2 → 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 (70) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +35674 -32430
  3. package/examples/data/data-with-metadata.json +10 -0
  4. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  5. package/examples/metadata-variables.json +58 -0
  6. package/package.json +3 -3
  7. package/src/CdcChart.tsx +8 -4
  8. package/src/CdcChartComponent.tsx +321 -288
  9. package/src/_stories/Chart.CustomColors.stories.tsx +74 -0
  10. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  11. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  12. package/src/_stories/Chart.stories.tsx +36 -2
  13. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  14. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  15. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  16. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  17. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  18. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  19. package/src/components/Annotations/components/AnnotationDraggable.styles.css +10 -10
  20. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -1
  21. package/src/components/Annotations/components/AnnotationList.styles.css +11 -11
  22. package/src/components/Axis/BottomAxis.tsx +10 -3
  23. package/src/components/Axis/PairedBarAxis.tsx +10 -4
  24. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  25. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  26. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  27. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  28. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  29. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  30. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  31. package/src/components/Brush/BrushSelector.tsx +2 -1
  32. package/src/components/Brush/MiniChartPreview.tsx +21 -26
  33. package/src/components/EditorPanel/EditorPanel.tsx +56 -43
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +9 -9
  35. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  36. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +39 -1
  37. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +24 -42
  38. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -2
  39. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +45 -42
  40. package/src/components/EditorPanel/editor-panel.scss +1 -1
  41. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  42. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  43. package/src/components/Legend/helpers/createFormatLabels.tsx +3 -2
  44. package/src/components/LinearChart/tests/LinearChart.test.tsx +77 -0
  45. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -0
  46. package/src/components/LinearChart.tsx +26 -6
  47. package/src/components/PieChart/PieChart.tsx +19 -4
  48. package/src/components/RadarChart/RadarChart.tsx +1 -1
  49. package/src/components/Regions/components/Regions.tsx +6 -6
  50. package/src/components/Sankey/components/Sankey.tsx +3 -3
  51. package/src/components/Sankey/sankey.scss +1 -1
  52. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  53. package/src/components/Sparkline/index.scss +4 -2
  54. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  55. package/src/data/initial-state.js +23 -14
  56. package/src/data/legacy-defaults.ts +18 -0
  57. package/src/helpers/abbreviateNumber.ts +24 -17
  58. package/src/helpers/getChartPatternId.ts +17 -0
  59. package/src/helpers/getMinMax.ts +16 -2
  60. package/src/helpers/seriesColumnSettings.ts +114 -0
  61. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  62. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  63. package/src/hooks/useRightAxis.ts +14 -0
  64. package/src/hooks/useScales.ts +92 -56
  65. package/src/hooks/useTooltip.tsx +20 -3
  66. package/src/scss/main.scss +152 -79
  67. package/src/test/CdcChart.test.jsx +2 -2
  68. package/src/types/ChartConfig.ts +4 -0
  69. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  70. package/tests/fixtures/data-with-metadata.json +10 -0
@@ -1,7 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite'
2
2
  import Chart from '../CdcChartComponent'
3
3
  import scatterPlotCustomColorConfig from './_mock/scatterplot_mock.json'
4
+ import pieCustomColorConfig from './_mock/pie_custom_colors.json'
4
5
  import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+ import { expect, userEvent, waitFor } from 'storybook/test'
5
7
 
6
8
  const meta: Meta<typeof Chart> = {
7
9
  title: 'Components/Templates/Chart/Custom Colors',
@@ -20,4 +22,76 @@ export const ScatterPlot: Story = {
20
22
  }
21
23
  }
22
24
 
25
+ const pieCustomColors = ['#0B6E4F', '#C75000', '#7C4DFF', '#0077B6', '#D7263D']
26
+ const pieFallbackCustomColors = ['#004E89', '#1A659E', '#47A8BD', '#D1495B', '#EDAE49']
27
+
28
+ const normalizeColor = (color: string) => {
29
+ const swatch = document.createElement('div')
30
+ swatch.style.color = color
31
+ document.body.appendChild(swatch)
32
+ const normalized = getComputedStyle(swatch).color
33
+ swatch.remove()
34
+ return normalized
35
+ }
36
+
37
+ const assertPieCustomColorStory = async (canvasElement: HTMLElement, expectedColors: string[]) => {
38
+ await assertVisualizationRendered(canvasElement)
39
+
40
+ const chart = canvasElement.querySelector('svg.animated-pie')
41
+ const legend = canvasElement.querySelector('.legend-container')
42
+ expect(chart).toBeTruthy()
43
+ expect(legend).toBeTruthy()
44
+
45
+ await waitFor(() => {
46
+ expect(chart?.querySelectorAll('path')).toHaveLength(expectedColors.length)
47
+ })
48
+
49
+ const legendItems = Array.from(legend!.querySelectorAll('[role="button"]')) as HTMLElement[]
50
+ expect(legendItems).toHaveLength(expectedColors.length)
51
+
52
+ const sliceColors = Array.from(chart!.querySelectorAll('path')).map(path =>
53
+ normalizeColor(path.getAttribute('fill') || '')
54
+ )
55
+ expect(sliceColors).toEqual(expectedColors.map(normalizeColor))
56
+
57
+ const legendColors = legendItems.map(item => {
58
+ const swatch = item.querySelector('span.legend-item') as HTMLElement | null
59
+ return normalizeColor(swatch ? getComputedStyle(swatch).backgroundColor : '')
60
+ })
61
+ expect(legendColors).toEqual(expectedColors.map(normalizeColor))
62
+
63
+ await userEvent.click(legendItems[0])
64
+
65
+ await waitFor(() => {
66
+ expect(chart?.querySelectorAll('path')).toHaveLength(1)
67
+ expect(legendItems[0].className).toContain('highlighted')
68
+ expect(legendItems[1].className).toContain('inactive')
69
+ })
70
+ }
71
+
72
+ export const PieChart: Story = {
73
+ args: {
74
+ config: pieCustomColorConfig,
75
+ isEditor: false
76
+ },
77
+ play: async ({ canvasElement }) => {
78
+ await assertPieCustomColorStory(canvasElement, pieCustomColors)
79
+ }
80
+ }
81
+
82
+ const pieChartCustomColorsFallbackConfig = JSON.parse(JSON.stringify(pieCustomColorConfig))
83
+ pieChartCustomColorsFallbackConfig.title = 'Pie Chart Custom Colors Fallback'
84
+ pieChartCustomColorsFallbackConfig.general.palette.customColors = pieFallbackCustomColors
85
+ delete pieChartCustomColorsFallbackConfig.general.palette.customColorsOrdered
86
+
87
+ export const PieChartCustomColorsFallback: Story = {
88
+ args: {
89
+ config: pieChartCustomColorsFallbackConfig,
90
+ isEditor: false
91
+ },
92
+ play: async ({ canvasElement }) => {
93
+ await assertPieCustomColorStory(canvasElement, pieFallbackCustomColors)
94
+ }
95
+ }
96
+
23
97
  export default meta
@@ -0,0 +1,95 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { expect } from 'storybook/test'
3
+ import Chart from '../CdcChart'
4
+ import simplifiedLine from './_mock/simplified_line.json'
5
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
6
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
7
+
8
+ const meta: Meta<typeof Chart> = {
9
+ title: 'Components/Templates/Chart/Defaults',
10
+ component: Chart
11
+ }
12
+
13
+ type Story = StoryObj<typeof Chart>
14
+
15
+ const oldConfig = editConfigKeys(simplifiedLine, [
16
+ { path: ['yAxis', 'hideAxis'], value: false },
17
+ { path: ['yAxis', 'hideTicks'], value: false },
18
+ { path: ['yAxis', 'gridLines'], value: false },
19
+ { path: ['yAxis', 'numTicks'], value: '' },
20
+ { path: ['xAxis', 'numTicks'], value: '' },
21
+ { path: ['table', 'expanded'], value: true },
22
+ { path: ['legend', 'position'], value: 'right' },
23
+ { path: ['dataFormat', 'commas'], value: false },
24
+ { path: ['tooltips', 'dateDisplayFormat'], value: '' }
25
+ ])
26
+
27
+ const newConfig = editConfigKeys(simplifiedLine, [
28
+ { path: ['yAxis', 'hideAxis'], value: true },
29
+ { path: ['yAxis', 'hideTicks'], value: true },
30
+ { path: ['yAxis', 'gridLines'], value: true },
31
+ { path: ['yAxis', 'numTicks'], value: 4 },
32
+ { path: ['xAxis', 'numTicks'], value: 6 },
33
+ { path: ['table', 'expanded'], value: false },
34
+ { path: ['legend', 'position'], value: 'top' },
35
+ { path: ['dataFormat', 'commas'], value: true },
36
+ { path: ['tooltips', 'dateDisplayFormat'], value: '%B %-d, %Y' }
37
+ ])
38
+
39
+ export const OldConfig_Preserves_Legacy_Defaults: Story = {
40
+ args: {
41
+ config: oldConfig,
42
+ isEditor: false
43
+ },
44
+ play: async ({ canvasElement }) => {
45
+ await assertVisualizationRendered(canvasElement)
46
+
47
+ await waitForPresence('.legend-container', canvasElement)
48
+ const legend = canvasElement.querySelector('.legend-container')
49
+ expect(legend).toBeInTheDocument()
50
+ expect(legend?.classList.contains('right')).toBe(true)
51
+
52
+ const axisLine = canvasElement.querySelector('.left-axis line[stroke="#000"]')
53
+ expect(axisLine).toBeInTheDocument()
54
+
55
+ const gridlines = canvasElement.querySelectorAll('.left-axis line[stroke="#d6d6d6"]')
56
+ expect(gridlines.length).toBe(0)
57
+
58
+ const dataTable = canvasElement.querySelector('.data-table')
59
+ expect(dataTable).toBeInTheDocument()
60
+ expect(dataTable?.getAttribute('hidden')).toBeNull()
61
+
62
+ const tickTexts = canvasElement.querySelectorAll('.left-axis .vx-axis-tick text')
63
+ for (const tick of tickTexts) {
64
+ expect(tick.textContent).not.toMatch(/,/)
65
+ }
66
+ }
67
+ }
68
+
69
+ export const NewConfig_Gets_New_Defaults: Story = {
70
+ args: {
71
+ config: newConfig,
72
+ isEditor: false
73
+ },
74
+ play: async ({ canvasElement }) => {
75
+ await assertVisualizationRendered(canvasElement)
76
+
77
+ await waitForPresence('.legend-container', canvasElement)
78
+ const legend = canvasElement.querySelector('.legend-container')
79
+ expect(legend).toBeInTheDocument()
80
+ expect(legend?.classList.contains('top')).toBe(true)
81
+
82
+ const axisLine = canvasElement.querySelector('.left-axis line[stroke="#000"]')
83
+ expect(axisLine).toBeNull()
84
+
85
+ await waitForPresence('.left-axis line[stroke="#d6d6d6"]', canvasElement)
86
+ const gridlines = canvasElement.querySelectorAll('.left-axis line[stroke="#d6d6d6"]')
87
+ expect(gridlines.length).toBeGreaterThan(0)
88
+
89
+ const dataTable = canvasElement.querySelector('.data-table')
90
+ expect(dataTable).toBeInTheDocument()
91
+ expect(dataTable?.getAttribute('hidden')).not.toBeNull()
92
+ }
93
+ }
94
+
95
+ export default meta
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
4
+ import smallestLeftAxisMaxConfig from './_mock/smallest_left_axis_max.json'
5
+ import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
6
+
7
+ const meta: Meta<typeof Chart> = {
8
+ title: 'Components/Templates/Chart/Smallest Left Axis Maximum',
9
+ component: Chart
10
+ }
11
+
12
+ type Story = StoryObj<typeof Chart>
13
+
14
+ /**
15
+ * Region B is the default filter — data only goes up to 1.
16
+ * Without smallestLeftAxisMax, the Y axis would show decimal ticks (0, 0.2, 0.4…).
17
+ * With smallestLeftAxisMax: 5, the axis extends to at least 5, producing clean integer ticks.
18
+ */
19
+ export const WithSmallestLeftAxisMax: Story = {
20
+ args: {
21
+ config: smallestLeftAxisMaxConfig
22
+ },
23
+ play: async ({ canvasElement }) => {
24
+ await assertVisualizationRendered(canvasElement)
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Same chart without the smallestLeftAxisMax setting.
30
+ * Region B data (max 1) causes decimal ticks on the Y axis — the problem this feature solves.
31
+ */
32
+ export const WithoutSmallestLeftAxisMax: Story = {
33
+ args: {
34
+ config: editConfigKeys(smallestLeftAxisMaxConfig, [{ path: ['yAxis', 'smallestLeftAxisMax'], value: undefined }])
35
+ },
36
+ play: async ({ canvasElement }) => {
37
+ await assertVisualizationRendered(canvasElement)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Region A has data up to 28, well above the smallestLeftAxisMax of 5.
43
+ * The axis naturally scales to the data — the setting has no effect here.
44
+ */
45
+ export const LargerDataUnaffected: Story = {
46
+ args: {
47
+ config: editConfigKeys(smallestLeftAxisMaxConfig, [{ path: ['filters', '0', 'active'], value: 'Region A' }])
48
+ },
49
+ play: async ({ canvasElement }) => {
50
+ await assertVisualizationRendered(canvasElement)
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Editor view so you can see the Smallest Left Axis Maximum field in the Left Value Axis section.
56
+ */
57
+ export const Editor: Story = {
58
+ args: {
59
+ config: smallestLeftAxisMaxConfig,
60
+ isEditor: true
61
+ }
62
+ }
63
+
64
+ export default meta
@@ -6,6 +6,7 @@ import lineChartTwoPointsNewChart from './_mock/line_chart_two_points_new_chart.
6
6
  import lollipop from './_mock/lollipop.json'
7
7
  import forestPlot from '../../examples/feature/forest-plot/forest-plot.json'
8
8
  import pairedBar from './_mock/paired-bar.json'
9
+ import pairedBarAbbreviated from './_mock/paired-bar-abbr.json'
9
10
  import horizontalBarConfig from './_mock/horizontal_bar.json'
10
11
  import horizontalBarsDynamicYAxis from './_mock/horizontal-bars-dynamic-y-axis.json'
11
12
  import barChartLabels from './_mock/barchart_labels.mock.json'
@@ -14,7 +15,8 @@ import pieCalculatedArea from './_mock/pie_calculated_area.json'
14
15
  import areaChartStacked from './_mock/area_chart_stacked.json'
15
16
  import multipleLines from './_mock/short_dates.json'
16
17
  import { editConfigKeys } from '@cdc/core/helpers/configHelpers'
17
- import { assertVisualizationRendered } from '@cdc/core/helpers/testing'
18
+ import { assertVisualizationRendered, waitForPresence } from '@cdc/core/helpers/testing'
19
+ import { expect } from 'storybook/test'
18
20
 
19
21
  const meta: Meta<typeof Chart> = {
20
22
  title: 'Components/Templates/Chart',
@@ -63,7 +65,8 @@ export const Lollipop: Story = {
63
65
 
64
66
  export const Forest_Plot: Story = {
65
67
  args: {
66
- config: forestPlot
68
+ config: forestPlot,
69
+ isEditor: true
67
70
  },
68
71
  play: async ({ canvasElement }) => {
69
72
  await assertVisualizationRendered(canvasElement)
@@ -127,6 +130,16 @@ export const Paired_Bar: Story = {
127
130
  }
128
131
  }
129
132
 
133
+ export const Paired_Bar_Year_Tick_Format_Regression: Story = {
134
+ args: {
135
+ config: pairedBarAbbreviated,
136
+ isEditor: true
137
+ },
138
+ play: async ({ canvasElement }) => {
139
+ await assertVisualizationRendered(canvasElement)
140
+ }
141
+ }
142
+
130
143
  export const Area_Chart_stacked: Story = {
131
144
  args: {
132
145
  config: areaChartStacked,
@@ -137,4 +150,25 @@ export const Area_Chart_stacked: Story = {
137
150
  }
138
151
  }
139
152
 
153
+ export const Metadata_In_Description: Story = {
154
+ args: {
155
+ configUrl: '/packages/chart/tests/fixtures/chart-config-with-metadata.json'
156
+ },
157
+ play: async ({ canvasElement }) => {
158
+ await assertVisualizationRendered(canvasElement)
159
+ const subtext = await waitForPresence('.subtext', canvasElement)
160
+ expect(subtext?.textContent).toContain('January 15, 2026')
161
+ }
162
+ }
163
+
164
+ export const Metadata_Backward_Compat_Plain_Array: Story = {
165
+ args: {
166
+ config: lineChartTwoPointsRegressionTest,
167
+ isEditor: false
168
+ },
169
+ play: async ({ canvasElement }) => {
170
+ await assertVisualizationRendered(canvasElement)
171
+ }
172
+ }
173
+
140
174
  export default meta
@@ -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)