@cdc/chart 4.25.11 → 4.26.1

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 (77) hide show
  1. package/dist/cdcchart.js +38898 -40013
  2. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  3. package/examples/private/DEV-12100.json +1303 -0
  4. package/examples/private/cat-y.json +1235 -0
  5. package/examples/private/data-points.json +228 -0
  6. package/examples/private/height.json +3915 -0
  7. package/examples/private/links.json +569 -0
  8. package/examples/private/quadrant.txt +30 -0
  9. package/examples/private/test-forecast.json +5510 -0
  10. package/examples/private/warming-stripe-test.json +2578 -0
  11. package/examples/private/warming-stripes.json +4763 -0
  12. package/examples/tech-adoption-with-links.json +560 -0
  13. package/index.html +15 -20
  14. package/package.json +5 -4
  15. package/preview.html +1616 -0
  16. package/src/CdcChartComponent.tsx +111 -75
  17. package/src/_stories/Chart.Regions.Categorical.stories.tsx +148 -0
  18. package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
  19. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
  20. package/src/_stories/Chart.stories.tsx +8 -0
  21. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  22. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  23. package/src/_stories/ChartBrush.stories.tsx +50 -0
  24. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  25. package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
  26. package/src/_stories/_mock/brush_enabled.json +326 -0
  27. package/src/_stories/_mock/brush_mock.json +2 -69
  28. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  29. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  30. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  31. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  32. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  33. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  34. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  35. package/src/components/BarChart/components/context.tsx +1 -0
  36. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  37. package/src/components/Brush/BrushSelector.tsx +1258 -0
  38. package/src/components/Brush/MiniChartPreview.tsx +283 -0
  39. package/src/components/DeviationBar.jsx +9 -7
  40. package/src/components/EditorPanel/EditorPanel.tsx +2711 -2586
  41. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  42. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +57 -30
  43. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  44. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  45. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -27
  46. package/src/components/EditorPanel/useEditorPermissions.ts +31 -18
  47. package/src/components/Legend/Legend.tsx +3 -2
  48. package/src/components/Legend/helpers/createFormatLabels.tsx +151 -2
  49. package/src/components/Legend/helpers/index.ts +10 -6
  50. package/src/components/LinearChart.tsx +495 -430
  51. package/src/components/PairedBarChart.jsx +20 -3
  52. package/src/components/Regions/components/Regions.tsx +365 -122
  53. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  54. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  55. package/src/components/WarmingStripes/WarmingStripes.tsx +160 -0
  56. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  57. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  58. package/src/components/WarmingStripes/index.tsx +3 -0
  59. package/src/data/initial-state.js +3 -1
  60. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  61. package/src/helpers/getMinMax.ts +12 -7
  62. package/src/helpers/sizeHelpers.ts +0 -20
  63. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  64. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  65. package/src/hooks/useScales.ts +11 -1
  66. package/src/hooks/useTooltip.tsx +31 -10
  67. package/src/scss/DataTable.scss +0 -4
  68. package/src/scss/main.scss +17 -3
  69. package/src/test/CdcChart.test.jsx +1 -1
  70. package/src/types/ChartConfig.ts +3 -0
  71. package/src/types/Label.ts +1 -0
  72. package/src/utils/analyticsTracking.ts +19 -0
  73. package/LICENSE +0 -201
  74. package/src/components/Brush/BrushChart.tsx +0 -128
  75. package/src/components/Brush/BrushController.tsx +0 -71
  76. package/src/components/Brush/types.tsx +0 -8
  77. package/src/components/BrushChart.tsx +0 -223
@@ -0,0 +1,297 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChart'
3
+
4
+ const meta: Meta<typeof Chart> = {
5
+ title: 'Components/Templates/Chart/Regions/Date-Time Scale (Continuous)',
6
+ component: Chart
7
+ }
8
+
9
+ type Story = StoryObj<typeof Chart>
10
+
11
+ const dateData = [
12
+ { date: '2024-01-01', value: 10 },
13
+ { date: '2024-01-08', value: 25 },
14
+ { date: '2024-01-15', value: 35 },
15
+ { date: '2024-01-22', value: 45 },
16
+ { date: '2024-01-29', value: 55 },
17
+ { date: '2024-02-05', value: 40 },
18
+ { date: '2024-02-12', value: 60 },
19
+ { date: '2024-02-19', value: 75 },
20
+ { date: '2024-02-26', value: 65 },
21
+ { date: '2024-03-04', value: 80 }
22
+ ]
23
+
24
+ const baseDateTimeConfig = {
25
+ type: 'chart',
26
+ visualizationType: 'Line',
27
+ orientation: 'vertical',
28
+ showTitle: true,
29
+ theme: 'theme-blue',
30
+ animate: false,
31
+ xAxis: {
32
+ type: 'date-time',
33
+ dataKey: 'date',
34
+ dateParseFormat: '%Y-%m-%d',
35
+ dateDisplayFormat: '%b %-d',
36
+ size: '0',
37
+ hideAxis: false,
38
+ hideTicks: false,
39
+ numTicks: '6'
40
+ },
41
+ yAxis: {
42
+ size: '50',
43
+ hideAxis: false,
44
+ hideTicks: false,
45
+ gridLines: true,
46
+ min: '0',
47
+ max: '100'
48
+ },
49
+ series: [{ dataKey: 'value', type: 'Line', axis: 'Left', tooltip: true, name: 'Value' }],
50
+ legend: { hide: true },
51
+ data: dateData,
52
+ regions: []
53
+ }
54
+
55
+ // LINE CHARTS
56
+
57
+ export const Line_Fixed_From_Fixed_To: Story = {
58
+ args: {
59
+ config: {
60
+ ...baseDateTimeConfig,
61
+ title: 'Date-Time Scale - Line: Fixed From + Fixed To',
62
+ regions: [
63
+ {
64
+ from: '2024-01-15',
65
+ to: '2024-02-11',
66
+ fromType: 'Fixed',
67
+ toType: 'Fixed',
68
+ label: 'Fixed Region',
69
+ background: '#0077cc',
70
+ color: '#000000',
71
+ range: 'Custom'
72
+ }
73
+ ]
74
+ },
75
+ isEditor: true
76
+ }
77
+ }
78
+
79
+ export const Line_Fixed_From_Last_Date: Story = {
80
+ args: {
81
+ config: {
82
+ ...baseDateTimeConfig,
83
+ title: 'Date-Time Scale - Line: Fixed From + Last Date',
84
+ regions: [
85
+ {
86
+ from: '2024-02-05',
87
+ to: '',
88
+ fromType: 'Fixed',
89
+ toType: 'Last Date',
90
+ label: 'To Last Date',
91
+ background: '#00aa55',
92
+ color: '#000000',
93
+ range: 'Custom'
94
+ }
95
+ ]
96
+ },
97
+ isEditor: true
98
+ }
99
+ }
100
+
101
+ export const Line_Previous_Days_Last_Date: Story = {
102
+ args: {
103
+ config: {
104
+ ...baseDateTimeConfig,
105
+ title: 'Date-Time Scale - Line: Previous Days + Last Date',
106
+ regions: [
107
+ {
108
+ from: '8',
109
+ to: '',
110
+ fromType: 'Previous Days',
111
+ toType: 'Last Date',
112
+ label: 'Last 8 Days',
113
+ background: '#aa0077',
114
+ color: '#000000',
115
+ range: 'Custom'
116
+ }
117
+ ]
118
+ },
119
+ isEditor: true
120
+ }
121
+ }
122
+
123
+ // BAR CHARTS
124
+
125
+ export const Bar_Fixed_From_Fixed_To: Story = {
126
+ args: {
127
+ config: {
128
+ ...baseDateTimeConfig,
129
+ visualizationType: 'Bar',
130
+ barThickness: 0.7,
131
+ title: 'Date-Time Scale - Bar: Fixed From + Fixed To',
132
+ regions: [
133
+ {
134
+ from: '2024-01-15',
135
+ to: '2024-02-12',
136
+ fromType: 'Fixed',
137
+ toType: 'Fixed',
138
+ label: 'Fixed Region',
139
+ background: '#0077cc',
140
+ color: '#000000',
141
+ range: 'Custom'
142
+ }
143
+ ]
144
+ },
145
+ isEditor: true
146
+ }
147
+ }
148
+
149
+ export const Bar_Fixed_From_Last_Date: Story = {
150
+ args: {
151
+ config: {
152
+ ...baseDateTimeConfig,
153
+ visualizationType: 'Bar',
154
+ barThickness: 0.7,
155
+ title: 'Date-Time Scale - Bar: Fixed From + Last Date',
156
+ regions: [
157
+ {
158
+ from: '2024-02-05',
159
+ to: '',
160
+ fromType: 'Fixed',
161
+ toType: 'Last Date',
162
+ label: 'To Last Date',
163
+ background: '#00aa55',
164
+ color: '#000000',
165
+ range: 'Custom'
166
+ }
167
+ ]
168
+ },
169
+ isEditor: true
170
+ }
171
+ }
172
+
173
+ export const Bar_Previous_Days_Last_Date: Story = {
174
+ args: {
175
+ config: {
176
+ ...baseDateTimeConfig,
177
+ visualizationType: 'Bar',
178
+ barThickness: 0.7,
179
+ title: 'Date-Time Scale - Bar: Previous Days + Last Date',
180
+ regions: [
181
+ {
182
+ from: '28',
183
+ to: '',
184
+ fromType: 'Previous Days',
185
+ toType: 'Last Date',
186
+ label: 'Last 28 Days',
187
+ background: '#aa0077',
188
+ color: '#000000',
189
+ range: 'Custom'
190
+ }
191
+ ]
192
+ },
193
+ isEditor: true
194
+ }
195
+ }
196
+
197
+ // EDGE CASES
198
+
199
+ export const Edge_Region_At_Start: Story = {
200
+ args: {
201
+ config: {
202
+ ...baseDateTimeConfig,
203
+ title: 'Edge Case: Region at Start',
204
+ regions: [
205
+ {
206
+ from: '2024-01-01',
207
+ to: '2024-01-21',
208
+ fromType: 'Fixed',
209
+ toType: 'Fixed',
210
+ label: 'At Start',
211
+ background: '#0077cc',
212
+ color: '#000000',
213
+ range: 'Custom'
214
+ }
215
+ ]
216
+ },
217
+ isEditor: true
218
+ }
219
+ }
220
+
221
+ export const Edge_Region_At_End: Story = {
222
+ args: {
223
+ config: {
224
+ ...baseDateTimeConfig,
225
+ title: 'Edge Case: Region at End',
226
+ regions: [
227
+ {
228
+ from: '2024-02-19',
229
+ to: '2024-03-04',
230
+ fromType: 'Fixed',
231
+ toType: 'Fixed',
232
+ label: 'At End',
233
+ background: '#00aa55',
234
+ color: '#000000',
235
+ range: 'Custom'
236
+ }
237
+ ]
238
+ },
239
+ isEditor: true
240
+ }
241
+ }
242
+
243
+ export const Edge_Full_Coverage: Story = {
244
+ args: {
245
+ config: {
246
+ ...baseDateTimeConfig,
247
+ title: 'Edge Case: Full Chart Coverage',
248
+ regions: [
249
+ {
250
+ from: '2024-01-01',
251
+ to: '',
252
+ fromType: 'Fixed',
253
+ toType: 'Last Date',
254
+ label: 'Full Coverage',
255
+ background: '#cc7700',
256
+ color: '#000000',
257
+ range: 'Custom'
258
+ }
259
+ ]
260
+ },
261
+ isEditor: true
262
+ }
263
+ }
264
+
265
+ export const Multiple_Regions: Story = {
266
+ args: {
267
+ config: {
268
+ ...baseDateTimeConfig,
269
+ title: 'Multiple Regions',
270
+ regions: [
271
+ {
272
+ from: '2024-01-08',
273
+ to: '2024-01-21',
274
+ fromType: 'Fixed',
275
+ toType: 'Fixed',
276
+ label: 'Region 1',
277
+ background: '#0077cc',
278
+ color: '#000000',
279
+ range: 'Custom'
280
+ },
281
+ {
282
+ from: '2024-02-05',
283
+ to: '2024-02-18',
284
+ fromType: 'Fixed',
285
+ toType: 'Fixed',
286
+ label: 'Region 2',
287
+ background: '#00aa55',
288
+ color: '#000000',
289
+ range: 'Custom'
290
+ }
291
+ ]
292
+ },
293
+ isEditor: true
294
+ }
295
+ }
296
+
297
+ export default meta
@@ -7,6 +7,7 @@ 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
9
  import horizontalBarConfig from './_mock/horizontal_bar.json'
10
+ import horizontalBarsDynamicYAxis from './_mock/horizontal-bars-dynamic-y-axis.json'
10
11
  import barChartLabels from './_mock/barchart_labels.mock.json'
11
12
  import pieConfig from './_mock/pie_with_data.json'
12
13
  import pieCalculatedArea from './_mock/pie_calculated_area.json'
@@ -59,6 +60,13 @@ export const Horizontal_Bar: Story = {
59
60
  }
60
61
  }
61
62
 
63
+ export const Horizontal_Bars_Dynamic_Y_Axis: Story = {
64
+ args: {
65
+ config: horizontalBarsDynamicYAxis,
66
+ isEditor: false
67
+ }
68
+ }
69
+
62
70
  export const BarChart_Labels: Story = {
63
71
  args: {
64
72
  config: barChartLabels
@@ -2388,10 +2388,9 @@ export const BarFiltersTests: Story = {
2388
2388
  const chartContainer = canvasElement.querySelector('.cove-component__content, .chart-container, .visualization')
2389
2389
  const svg = chartContainer?.querySelector('svg') || canvasElement.querySelector('svg:not(.icon)')
2390
2390
  const bars = svg?.querySelectorAll('rect[class*="bar"], rect[data-testid*="bar"], g[class*="bar"] rect') || []
2391
- const filtersList = canvasElement.querySelector('.filters-list')
2391
+ const filtersList = canvasElement.querySelector('.draggable-field-list')
2392
2392
 
2393
- // Fix: filters can be either div.mb-1 (collapsed) or fieldset.edit-block.mb-1 (expanded)
2394
- const filterElements = filtersList?.querySelectorAll('div.mb-1, fieldset.edit-block.mb-1') || []
2393
+ const filterElements = filtersList?.querySelectorAll('.editor-field-item') || []
2395
2394
 
2396
2395
  // Get actual data visualization state for filtering verification
2397
2396
  // Method 1: Try X-axis tick labels (most direct)
@@ -2565,9 +2564,15 @@ export const BarFiltersTests: Story = {
2565
2564
  'Apply Filter Value - Chart data visually filtered to show only Q2',
2566
2565
  getChartDataState,
2567
2566
  async () => {
2568
- // Find the "Filter Default Value" dropdown - this sets filter.active which actually filters the data
2569
- const filterDefaultValueSelect = canvas.getByLabelText(/filter default value/i) as HTMLSelectElement
2570
-
2567
+ // Find all "Filter Default Value (category)" dropdowns
2568
+ const filterDefaultValueSelects = canvas.getAllByLabelText(
2569
+ /filter default value \(category\)/i
2570
+ ) as HTMLSelectElement[]
2571
+ // Select the dropdown that contains Q2 as an option
2572
+ const filterDefaultValueSelect = filterDefaultValueSelects.find(select =>
2573
+ Array.from(select.options).some(opt => opt.value === 'Q2')
2574
+ )
2575
+ if (!filterDefaultValueSelect) throw new Error('Could not find filter default value dropdown for Q2')
2571
2576
  // Select Q2 to filter the chart to only show Q2 data (different from current state)
2572
2577
  await userEvent.selectOptions(filterDefaultValueSelect, 'Q2')
2573
2578
  },
@@ -0,0 +1,295 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, userEvent, expect } from 'storybook/test'
3
+ import Chart from '../CdcChartComponent'
4
+
5
+ // Import testing helpers following best practices document
6
+ import { openAccordion, performAndAssert, waitForEditor, waitForPresence } from '@cdc/core/helpers/testing'
7
+
8
+ // Import working brush configuration
9
+ import brushEnabledConfig from './_mock/brush_enabled.json'
10
+
11
+ const meta: Meta<typeof Chart> = {
12
+ title: 'Components/Templates/Chart/Editor Tests/Brush',
13
+ component: Chart
14
+ }
15
+
16
+ export default meta
17
+ type Story = StoryObj<typeof Chart>
18
+
19
+ // ============================================================================
20
+ // BRUSH CHART EDITOR TESTS
21
+ // Tests the Brush Slider features in the Date/Category Axis accordion section
22
+ // Following best practices:
23
+ // - Tests visualization output changes, not control state
24
+ // - Uses performAndAssert pattern for all interactions
25
+ // - Tests specific visual changes in the brush selection
26
+ // ============================================================================
27
+
28
+ export const BrushDefaultSelectionTests: Story = {
29
+ name: 'Default Selection Mode Tests',
30
+ parameters: {
31
+ test: {
32
+ timeout: 30000
33
+ }
34
+ },
35
+ args: {
36
+ config: {
37
+ ...brushEnabledConfig,
38
+ xAxis: {
39
+ ...brushEnabledConfig.xAxis,
40
+ brushActive: true,
41
+ // New property: when set, shows last X data points instead of 35%
42
+ brushDefaultRecentDateCount: undefined
43
+ }
44
+ },
45
+ isEditor: true
46
+ },
47
+ play: async ({ canvasElement }) => {
48
+ const canvas = within(canvasElement)
49
+ await waitForEditor(canvas)
50
+ await openAccordion(canvas, 'Date/Category Axis')
51
+
52
+ // ============================================================================
53
+ // TEST: Brush Slider Toggle
54
+ // Verifies: Brush slider appears/disappears in the visualization
55
+ // ============================================================================
56
+
57
+ const getBrushVisibility = () => {
58
+ const brushSvg = canvasElement.querySelector('svg[style*="border: 1px solid"]')
59
+ const brushRect = canvasElement.querySelector('.visx-brush rect')
60
+ return {
61
+ hasBrushSvg: !!brushSvg,
62
+ hasBrushRect: !!brushRect,
63
+ brushContainerVisible: !!canvasElement.querySelector('[class*="brush"]')
64
+ }
65
+ }
66
+
67
+ // Verify brush is initially visible since config has brushActive: true
68
+ const initialBrushState = getBrushVisibility()
69
+ expect(initialBrushState.hasBrushSvg || initialBrushState.brushContainerVisible).toBe(true)
70
+
71
+ // Find and toggle the brush checkbox
72
+ const brushCheckbox = canvas.getByLabelText(/show brush slider/i) as HTMLInputElement
73
+ expect(brushCheckbox).toBeTruthy()
74
+ expect(brushCheckbox.checked).toBe(true)
75
+
76
+ // Toggle brush off and verify it disappears from visualization
77
+ await performAndAssert(
78
+ 'Brush Slider Toggle Off',
79
+ getBrushVisibility,
80
+ async () => await userEvent.click(brushCheckbox),
81
+ (before, after) => {
82
+ // Either brush SVG or brush container should disappear
83
+ return (
84
+ (before.hasBrushSvg && !after.hasBrushSvg) || (before.brushContainerVisible && !after.brushContainerVisible)
85
+ )
86
+ }
87
+ )
88
+
89
+ // Toggle brush back on
90
+ await performAndAssert(
91
+ 'Brush Slider Toggle On',
92
+ getBrushVisibility,
93
+ async () => await userEvent.click(brushCheckbox),
94
+ (before, after) => {
95
+ return (
96
+ (!before.hasBrushSvg && after.hasBrushSvg) || (!before.brushContainerVisible && after.brushContainerVisible)
97
+ )
98
+ }
99
+ )
100
+
101
+ // ============================================================================
102
+ // TEST: Default Recent Date Count Input
103
+ // Verifies: When "Show last X dates" is set, the brush selection changes
104
+ // to show exactly X data points instead of the default 35%
105
+ // ============================================================================
106
+
107
+ // This control should appear when brush is enabled
108
+ // Look for the new "Show last X dates by default" input
109
+ const recentDateCountInput = canvas.getByLabelText(/show last.*dates.*default/i) as HTMLInputElement
110
+
111
+ // If the control doesn't exist yet (TDD - test before implementation),
112
+ // the test will fail here, indicating we need to implement this feature
113
+ expect(recentDateCountInput).toBeTruthy()
114
+ expect(recentDateCountInput).toHaveAttribute('type', 'number')
115
+
116
+ // ============================================================================
117
+ // TEST: Setting Recent Date Count Dynamically Updates Brush Selection
118
+ // Verifies: Changing the value in the editor immediately updates the brush
119
+ // ============================================================================
120
+
121
+ const getBrushSelectionState = () => {
122
+ // The brush selection is represented by the visx-brush extent rect
123
+ const brushExtent = canvasElement.querySelector('.visx-brush rect[class*="selection"]') as SVGRectElement
124
+ const brushSvg = canvasElement.querySelector('svg[style*="border: 1px solid"]') as SVGSVGElement
125
+
126
+ // Get the visible data points in the main chart (lines or bars)
127
+ const chartSvg = canvasElement.querySelector('.linear-chart svg, .cove-chart svg') as SVGSVGElement
128
+ const dataPointsInChart = chartSvg?.querySelectorAll('circle, rect:not([fill="transparent"])').length || 0
129
+
130
+ return {
131
+ brushWidth: brushExtent ? parseFloat(brushExtent.getAttribute('width') || '0') : 0,
132
+ brushX: brushExtent ? parseFloat(brushExtent.getAttribute('x') || '0') : 0,
133
+ totalBrushWidth: brushSvg ? brushSvg.clientWidth : 0,
134
+ visibleDataPoints: dataPointsInChart,
135
+ selectionPercentage:
136
+ brushExtent && brushSvg
137
+ ? (parseFloat(brushExtent.getAttribute('width') || '0') / brushSvg.clientWidth) * 100
138
+ : 0
139
+ }
140
+ }
141
+
142
+ const initialSelectionState = getBrushSelectionState()
143
+
144
+ // Default is ~35% of the width
145
+ // With 329 data points in brush_enabled.json, 35% would be ~115 points
146
+ // Setting to 30 should make the selection much narrower AND update immediately
147
+ await performAndAssert(
148
+ 'Set Default Recent Date Count to 30 - Brush Updates Dynamically',
149
+ getBrushSelectionState,
150
+ async () => {
151
+ await userEvent.clear(recentDateCountInput)
152
+ await userEvent.type(recentDateCountInput, '30')
153
+ // Trigger change/blur to apply the value - this should update the brush immediately
154
+ await userEvent.tab()
155
+ },
156
+ (before, after) => {
157
+ // The brush selection width should decrease when we set a smaller count
158
+ // Since we have ~329 data points and default is 35% (~115 points),
159
+ // setting to 30 points should make the selection narrower
160
+ return after.brushWidth < before.brushWidth || after.selectionPercentage < before.selectionPercentage
161
+ }
162
+ )
163
+
164
+ // ============================================================================
165
+ // TEST: Changing Value Again Updates Selection Again
166
+ // Verifies: Multiple changes continue to update the brush dynamically
167
+ // ============================================================================
168
+
169
+ await performAndAssert(
170
+ 'Change to 50 dates - Brush Expands Dynamically',
171
+ getBrushSelectionState,
172
+ async () => {
173
+ await userEvent.clear(recentDateCountInput)
174
+ await userEvent.type(recentDateCountInput, '50')
175
+ await userEvent.tab()
176
+ },
177
+ (before, after) => {
178
+ // Changing from 30 to 50 should expand the brush width
179
+ return after.brushWidth > before.brushWidth
180
+ }
181
+ )
182
+
183
+ // ============================================================================
184
+ // TEST: Verify Exact Data Point Count in Selection
185
+ // Verifies: The number of selected data points matches the input value
186
+ // ============================================================================
187
+
188
+ // The brush selection should now show exactly 30 data points
189
+ // We can verify this by checking the filtered data in the visualization
190
+ const getSelectedDataPointCount = () => {
191
+ // When brush is active, only the selected data points are rendered in the main chart
192
+ // Look for the number of data points (circles, bars, or line path points)
193
+ const chartContainer = canvasElement.querySelector('.linear-chart, .cove-chart')
194
+ const svg = chartContainer?.querySelector('svg')
195
+
196
+ // For line charts, count the line path data points
197
+ const linePaths = svg?.querySelectorAll('path[class*="line"], .visx-linepath')
198
+ let dataPointCount = 0
199
+
200
+ if (linePaths && linePaths.length > 0) {
201
+ // Count points in the path by looking at the rendered circles
202
+ const circles = svg?.querySelectorAll('circle')
203
+ dataPointCount = circles?.length || 0
204
+ }
205
+
206
+ // For bar charts
207
+ const bars = svg?.querySelectorAll('rect[class*="bar"]')
208
+ if (bars && bars.length > 0) {
209
+ dataPointCount = bars.length
210
+ }
211
+
212
+ return {
213
+ dataPointCount,
214
+ hasData: dataPointCount > 0
215
+ }
216
+ }
217
+
218
+ // After setting to 30, the chart should show ~30 data points
219
+ // (exact count may vary based on how brush boundaries align with data points)
220
+ const afterSettingCount = getSelectedDataPointCount()
221
+
222
+ // The count should be close to 30 (allowing some tolerance for edge cases)
223
+ // Note: This assertion helps verify the feature works - if it fails,
224
+ // the implementation needs adjustment
225
+ if (afterSettingCount.hasData) {
226
+ expect(afterSettingCount.dataPointCount).toBeGreaterThan(0)
227
+ expect(afterSettingCount.dataPointCount).toBeLessThanOrEqual(35) // 30 + tolerance
228
+ }
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
+ }
250
+ }
251
+
252
+ // ============================================================================
253
+ // BRUSH SECTION ACCESSIBILITY TEST
254
+ // Verifies the brush controls are properly labeled and accessible
255
+ // ============================================================================
256
+
257
+ export const BrushAccessibilityTests: Story = {
258
+ name: 'Accessibility Tests',
259
+ parameters: {
260
+ test: {
261
+ timeout: 20000
262
+ }
263
+ },
264
+ args: {
265
+ config: {
266
+ ...brushEnabledConfig,
267
+ xAxis: {
268
+ ...brushEnabledConfig.xAxis,
269
+ brushActive: true
270
+ }
271
+ },
272
+ isEditor: true
273
+ },
274
+ play: async ({ canvasElement }) => {
275
+ const canvas = within(canvasElement)
276
+ await waitForEditor(canvas)
277
+ await openAccordion(canvas, 'Date/Category Axis')
278
+
279
+ // ============================================================================
280
+ // TEST: Brush Controls Have Proper Labels
281
+ // ============================================================================
282
+
283
+ // The brush slider checkbox should be findable by its label
284
+ const brushCheckbox = canvas.getByLabelText(/show brush slider/i)
285
+ expect(brushCheckbox).toBeTruthy()
286
+ expect(brushCheckbox).toHaveAttribute('type', 'checkbox')
287
+
288
+ // The recent date count input should have a descriptive label
289
+ const recentDateLabel = canvas.getByLabelText(/show last.*dates.*default/i)
290
+
291
+ // If this test fails, it indicates the control needs a proper accessible label
292
+ expect(recentDateLabel).toBeTruthy()
293
+ expect(recentDateLabel).toHaveAttribute('type', 'number')
294
+ }
295
+ }
@@ -0,0 +1,50 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import Chart from '../CdcChartComponent'
3
+ import brushEnabledConfig from './_mock/brush_enabled.json'
4
+
5
+ const meta: Meta<typeof Chart> = {
6
+ title: 'Components/Templates/Chart/BrushSlider',
7
+ component: Chart,
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component:
12
+ 'Brush Slider feature allows users to interactively filter data by dragging handles along the x-axis. Only works with date/time-based x-axis and vertical orientation.'
13
+ }
14
+ }
15
+ }
16
+ }
17
+
18
+ type Story = StoryObj<typeof Chart>
19
+
20
+ export const BrushSliderEnabled: Story = {
21
+ args: {
22
+ config: brushEnabledConfig,
23
+ isEditor: false
24
+ },
25
+ parameters: {
26
+ docs: {
27
+ description: {
28
+ story:
29
+ 'Line chart with brush slider enabled. Drag the handles on the slider below the chart to filter the data range.'
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ export const BrushSliderInEditor: Story = {
36
+ args: {
37
+ config: brushEnabledConfig,
38
+ isEditor: true
39
+ },
40
+ parameters: {
41
+ docs: {
42
+ description: {
43
+ story:
44
+ 'Brush slider in editor mode. You can toggle the "Brush Slider" checkbox in the X-Axis section to enable/disable it.'
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ export default meta