@cdc/chart 4.26.4 → 4.26.6

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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -0,0 +1,574 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { createStyles, filterCircles, createDataSegments } from './helpers'
3
+ import { PreliminaryDataItem } from '../../types/ChartConfig'
4
+
5
+ describe('LineChart helpers', () => {
6
+ describe('createStyles', () => {
7
+ const mockHandleLineType = vi.fn((style: string) => {
8
+ switch (style) {
9
+ case 'Dashed Small':
10
+ return '5 5'
11
+ case 'Dashed Medium':
12
+ return '10 5'
13
+ case 'Dashed Large':
14
+ return '15 5'
15
+ default:
16
+ return 0
17
+ }
18
+ })
19
+
20
+ const basePreliminaryData: PreliminaryDataItem[] = [
21
+ {
22
+ type: 'effect',
23
+ seriesKeys: ['COVID-19'],
24
+ column: 'Attribute',
25
+ value: 'Dotted',
26
+ style: 'Dashed Small',
27
+ label: 'COVID Dotted',
28
+ displayTooltip: true,
29
+ displayLegend: true,
30
+ displayTable: true,
31
+ symbol: '',
32
+ iconCode: '',
33
+ lineCode: '',
34
+ hideBarSymbol: false,
35
+ hideLineStyle: false,
36
+ circleSize: 6,
37
+ displayGray: false
38
+ },
39
+ {
40
+ type: 'effect',
41
+ seriesKeys: ['Influenza'],
42
+ column: 'Attribute',
43
+ value: 'Dotted',
44
+ style: 'Dashed Medium',
45
+ label: 'Flu Dotted',
46
+ displayTooltip: true,
47
+ displayLegend: true,
48
+ displayTable: true,
49
+ symbol: '',
50
+ iconCode: '',
51
+ lineCode: '',
52
+ hideBarSymbol: false,
53
+ hideLineStyle: false,
54
+ circleSize: 6,
55
+ displayGray: false
56
+ }
57
+ ]
58
+
59
+ describe('with dynamic category data (single-row/tall format)', () => {
60
+ const dynamicCategoryData = [
61
+ { Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: 'Dotted' },
62
+ { Date: '10/12/2025', Category: 'COVID-19', Value: '40.7', Attribute: '' },
63
+ { Date: '10/19/2025', Category: 'COVID-19', Value: '42.6', Attribute: 'Dotted' }
64
+ ]
65
+
66
+ it('should apply dashed style when Attribute matches effect value', () => {
67
+ const styles = createStyles({
68
+ preliminaryData: basePreliminaryData,
69
+ data: dynamicCategoryData,
70
+ stroke: '#000',
71
+ strokeWidth: 2,
72
+ handleLineType: mockHandleLineType,
73
+ lineType: 'solid-line',
74
+ seriesKey: 'COVID-19', // Runtime series key from Category column
75
+ dynamicCategory: 'Category',
76
+ originalSeriesKey: 'Value' // Data value column
77
+ })
78
+
79
+ expect(styles).toHaveLength(3)
80
+ // First point has Attribute="Dotted" - should be dashed
81
+ expect(styles[0].strokeDasharray).toBe('5 5')
82
+ // Second point has Attribute="" - but previous point was dashed, so it inherits
83
+ // Actually, the logic applies dashed to current AND previous point when effect is found
84
+ // Let's check what happens
85
+ expect(styles[1].strokeDasharray).toBe('5 5') // Inherits from next point's effect
86
+ // Third point has Attribute="Dotted" - should be dashed
87
+ expect(styles[2].strokeDasharray).toBe('5 5')
88
+ })
89
+
90
+ it('should apply solid style when no effect matches', () => {
91
+ const dataNoEffects = [
92
+ { Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: '' },
93
+ { Date: '10/12/2025', Category: 'COVID-19', Value: '40.7', Attribute: '' }
94
+ ]
95
+
96
+ const styles = createStyles({
97
+ preliminaryData: basePreliminaryData,
98
+ data: dataNoEffects,
99
+ stroke: '#000',
100
+ strokeWidth: 2,
101
+ handleLineType: mockHandleLineType,
102
+ lineType: 'solid-line',
103
+ seriesKey: 'COVID-19',
104
+ dynamicCategory: 'Category',
105
+ originalSeriesKey: 'Value'
106
+ })
107
+
108
+ expect(styles).toHaveLength(2)
109
+ expect(styles[0].strokeDasharray).toBe(0)
110
+ expect(styles[1].strokeDasharray).toBe(0)
111
+ })
112
+
113
+ it('should only match effects for the correct series', () => {
114
+ // Using Influenza series key with COVID data should not match
115
+ const styles = createStyles({
116
+ preliminaryData: basePreliminaryData,
117
+ data: dynamicCategoryData,
118
+ stroke: '#000',
119
+ strokeWidth: 2,
120
+ handleLineType: mockHandleLineType,
121
+ lineType: 'solid-line',
122
+ seriesKey: 'Influenza', // Different series
123
+ dynamicCategory: 'Category',
124
+ originalSeriesKey: 'Value'
125
+ })
126
+
127
+ // No effect should match since seriesKeys is ['COVID-19'] not ['Influenza']
128
+ // (assuming we use the COVID effect config)
129
+ expect(styles.every(s => s.strokeDasharray === 0 || s.strokeDasharray === '10 5')).toBe(true)
130
+ })
131
+ })
132
+
133
+ describe('with multi-column data (standard format)', () => {
134
+ const multiColumnPreliminaryData: PreliminaryDataItem[] = [
135
+ {
136
+ type: 'effect',
137
+ seriesKeys: ['COVID-19'],
138
+ column: 'COVID-19-Attribute',
139
+ value: 'Dotted',
140
+ style: 'Dashed Small',
141
+ label: 'COVID Dotted',
142
+ displayTooltip: true,
143
+ displayLegend: true,
144
+ displayTable: true,
145
+ symbol: '',
146
+ iconCode: '',
147
+ lineCode: '',
148
+ hideBarSymbol: false,
149
+ hideLineStyle: false,
150
+ circleSize: 6,
151
+ displayGray: false
152
+ }
153
+ ]
154
+
155
+ const multiColumnData = [
156
+ { Date: '10/5/2025', 'COVID-19': '43.6', 'COVID-19-Attribute': 'Dotted' },
157
+ { Date: '10/12/2025', 'COVID-19': '40.7', 'COVID-19-Attribute': '' },
158
+ { Date: '10/19/2025', 'COVID-19': '42.6', 'COVID-19-Attribute': 'Dotted' }
159
+ ]
160
+
161
+ it('should apply dashed style when attribute column matches', () => {
162
+ const styles = createStyles({
163
+ preliminaryData: multiColumnPreliminaryData,
164
+ data: multiColumnData,
165
+ stroke: '#000',
166
+ strokeWidth: 2,
167
+ handleLineType: mockHandleLineType,
168
+ lineType: 'solid-line',
169
+ seriesKey: 'COVID-19',
170
+ dynamicCategory: '', // No dynamic category
171
+ originalSeriesKey: ''
172
+ })
173
+
174
+ expect(styles).toHaveLength(3)
175
+ // First point has attribute - dashed
176
+ expect(styles[0].strokeDasharray).toBe('5 5')
177
+ // Second point inherits from third point's effect
178
+ expect(styles[1].strokeDasharray).toBe('5 5')
179
+ // Third point has attribute - dashed
180
+ expect(styles[2].strokeDasharray).toBe('5 5')
181
+ })
182
+ })
183
+
184
+ describe('with custom weight', () => {
185
+ it('should apply custom weight from preliminaryData item', () => {
186
+ const preliminaryDataWithWeight: PreliminaryDataItem[] = [
187
+ {
188
+ type: 'effect',
189
+ seriesKeys: ['COVID-19'],
190
+ column: 'Attribute',
191
+ value: 'Dotted',
192
+ style: 'Dashed Small',
193
+ label: 'COVID Dotted',
194
+ displayTooltip: true,
195
+ displayLegend: true,
196
+ displayTable: true,
197
+ symbol: '',
198
+ iconCode: '',
199
+ lineCode: '',
200
+ hideBarSymbol: false,
201
+ hideLineStyle: false,
202
+ circleSize: 6,
203
+ displayGray: false,
204
+ weight: 5 // Custom weight
205
+ }
206
+ ]
207
+
208
+ const data = [
209
+ { Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: 'Dotted' },
210
+ { Date: '10/12/2025', Category: 'COVID-19', Value: '40.7', Attribute: '' },
211
+ { Date: '10/19/2025', Category: 'COVID-19', Value: '42.6', Attribute: 'Dotted' }
212
+ ]
213
+
214
+ const styles = createStyles({
215
+ preliminaryData: preliminaryDataWithWeight,
216
+ data,
217
+ stroke: '#000',
218
+ strokeWidth: 2, // Default weight
219
+ handleLineType: mockHandleLineType,
220
+ lineType: 'solid-line',
221
+ seriesKey: 'COVID-19',
222
+ dynamicCategory: 'Category',
223
+ originalSeriesKey: 'Value'
224
+ })
225
+
226
+ expect(styles).toHaveLength(3)
227
+ // First point has effect with custom weight
228
+ expect(styles[0].strokeWidth).toBe(5)
229
+ // Second point inherits custom weight from next point's effect
230
+ expect(styles[1].strokeWidth).toBe(5)
231
+ // Third point has effect with custom weight
232
+ expect(styles[2].strokeWidth).toBe(5)
233
+ })
234
+
235
+ it('should use default strokeWidth when preliminaryData has no weight', () => {
236
+ const preliminaryDataNoWeight: PreliminaryDataItem[] = [
237
+ {
238
+ type: 'effect',
239
+ seriesKeys: ['COVID-19'],
240
+ column: 'Attribute',
241
+ value: 'Dotted',
242
+ style: 'Dashed Small',
243
+ label: 'COVID Dotted',
244
+ displayTooltip: true,
245
+ displayLegend: true,
246
+ displayTable: true,
247
+ symbol: '',
248
+ iconCode: '',
249
+ lineCode: '',
250
+ hideBarSymbol: false,
251
+ hideLineStyle: false,
252
+ circleSize: 6,
253
+ displayGray: false
254
+ // No weight property
255
+ }
256
+ ]
257
+
258
+ const data = [{ Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: 'Dotted' }]
259
+
260
+ const styles = createStyles({
261
+ preliminaryData: preliminaryDataNoWeight,
262
+ data,
263
+ stroke: '#000',
264
+ strokeWidth: 2,
265
+ handleLineType: mockHandleLineType,
266
+ lineType: 'solid-line',
267
+ seriesKey: 'COVID-19',
268
+ dynamicCategory: 'Category',
269
+ originalSeriesKey: 'Value'
270
+ })
271
+
272
+ expect(styles).toHaveLength(1)
273
+ // Should use default strokeWidth when no custom weight
274
+ expect(styles[0].strokeWidth).toBe(2)
275
+ })
276
+
277
+ it.each([
278
+ { weight: 5, expected: 5, label: 'applies a valid custom weight' },
279
+ { weight: 1, expected: 1, label: 'applies the minimum custom weight of 1' },
280
+ { weight: 9, expected: 9, label: 'applies the maximum custom weight of 9' },
281
+ { weight: 0.5, expected: 2, label: 'falls back to default for a weight below 1' },
282
+ { weight: 0, expected: 2, label: 'falls back to default for a weight of 0' },
283
+ { weight: -1, expected: 2, label: 'falls back to default for negative weight' },
284
+ { weight: '', expected: 2, label: 'falls back to default for empty string' },
285
+ { weight: 'abc', expected: 2, label: 'falls back to default for non-numeric weight' }
286
+ ])('$label', ({ weight, expected }) => {
287
+ const preliminaryData: PreliminaryDataItem[] = [
288
+ {
289
+ type: 'effect',
290
+ seriesKeys: ['COVID-19'],
291
+ column: 'Attribute',
292
+ value: 'Dotted',
293
+ style: 'Dashed Small',
294
+ label: 'COVID Dotted',
295
+ displayTooltip: true,
296
+ displayLegend: true,
297
+ displayTable: true,
298
+ symbol: '',
299
+ iconCode: '',
300
+ lineCode: '',
301
+ hideBarSymbol: false,
302
+ hideLineStyle: false,
303
+ circleSize: 6,
304
+ displayGray: false,
305
+ weight: weight as PreliminaryDataItem['weight']
306
+ }
307
+ ]
308
+
309
+ const data = [{ Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: 'Dotted' }]
310
+
311
+ const styles = createStyles({
312
+ preliminaryData,
313
+ data,
314
+ stroke: '#000',
315
+ strokeWidth: 2,
316
+ handleLineType: mockHandleLineType,
317
+ lineType: 'solid-line',
318
+ seriesKey: 'COVID-19',
319
+ dynamicCategory: 'Category',
320
+ originalSeriesKey: 'Value'
321
+ })
322
+
323
+ expect(styles).toHaveLength(1)
324
+ expect(styles[0].strokeWidth).toBe(expected)
325
+ })
326
+ })
327
+ })
328
+
329
+ describe('filterCircles', () => {
330
+ const circlesPreliminaryData: PreliminaryDataItem[] = [
331
+ {
332
+ type: 'effect',
333
+ seriesKeys: ['COVID-19'],
334
+ column: 'Attribute',
335
+ value: 'Marked',
336
+ style: 'Open Circles',
337
+ label: 'COVID Marked',
338
+ displayTooltip: true,
339
+ displayLegend: true,
340
+ displayTable: true,
341
+ symbol: '',
342
+ iconCode: '',
343
+ lineCode: '',
344
+ hideBarSymbol: false,
345
+ hideLineStyle: false,
346
+ circleSize: 8,
347
+ displayGray: false
348
+ },
349
+ {
350
+ type: 'effect',
351
+ seriesKeys: ['Influenza'],
352
+ column: 'Attribute',
353
+ value: 'Marked',
354
+ style: 'Filled Circles',
355
+ label: 'Flu Marked',
356
+ displayTooltip: true,
357
+ displayLegend: true,
358
+ displayTable: true,
359
+ symbol: '',
360
+ iconCode: '',
361
+ lineCode: '',
362
+ hideBarSymbol: false,
363
+ hideLineStyle: false,
364
+ circleSize: 6,
365
+ displayGray: false
366
+ }
367
+ ]
368
+
369
+ describe('with dynamic category data', () => {
370
+ const dynamicData = [
371
+ { Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: 'Marked' },
372
+ { Date: '10/12/2025', Category: 'COVID-19', Value: '40.7', Attribute: '' },
373
+ { Date: '10/19/2025', Category: 'COVID-19', Value: '42.6', Attribute: 'Marked' }
374
+ ]
375
+
376
+ it('should find open circles for matching dynamic category data', () => {
377
+ const circles = filterCircles(
378
+ circlesPreliminaryData,
379
+ dynamicData,
380
+ 'COVID-19', // Runtime series key
381
+ 'Category', // Dynamic category column
382
+ 'Value' // Value column for data existence check
383
+ )
384
+
385
+ expect(circles).toHaveLength(2) // Two data points with Attribute='Marked'
386
+ expect(circles[0].isFilled).toBe(false) // Open circles
387
+ expect(circles[0].size).toBe(8)
388
+ expect(circles[1].isFilled).toBe(false)
389
+ })
390
+
391
+ it('should skip open circles for nonnumeric values while preserving zero values', () => {
392
+ const data = [
393
+ { Date: '10/5/2025', Category: 'COVID-19', Value: 'N/A', Attribute: 'Marked' },
394
+ { Date: '10/12/2025', Category: 'COVID-19', Value: null, Attribute: 'Marked' },
395
+ { Date: '10/19/2025', Category: 'COVID-19', Value: 0, Attribute: 'Marked' },
396
+ { Date: '10/26/2025', Category: 'COVID-19', Value: '12.3', Attribute: 'Marked' }
397
+ ]
398
+
399
+ const circles = filterCircles(circlesPreliminaryData, data, 'COVID-19', 'Category', 'Value')
400
+
401
+ expect(circles).toHaveLength(2)
402
+ expect(circles.map(circle => circle.data.Value)).toEqual([0, '12.3'])
403
+ expect(circles.every(circle => circle.isFilled === false)).toBe(true)
404
+ })
405
+
406
+ it('should return empty array when no matches', () => {
407
+ const circles = filterCircles(
408
+ circlesPreliminaryData,
409
+ dynamicData,
410
+ 'RSV', // Series not in preliminaryData
411
+ 'Category',
412
+ 'Value'
413
+ )
414
+
415
+ expect(circles).toHaveLength(0)
416
+ })
417
+ })
418
+
419
+ describe('with multi-column data (standard format)', () => {
420
+ const multiColumnCirclesConfig: PreliminaryDataItem[] = [
421
+ {
422
+ type: 'effect',
423
+ seriesKeys: ['COVID-19'],
424
+ column: 'COVID-19-Attribute',
425
+ value: 'Marked',
426
+ style: 'Open Circles',
427
+ label: 'COVID Marked',
428
+ displayTooltip: true,
429
+ displayLegend: true,
430
+ displayTable: true,
431
+ symbol: '',
432
+ iconCode: '',
433
+ lineCode: '',
434
+ hideBarSymbol: false,
435
+ hideLineStyle: false,
436
+ circleSize: 8,
437
+ displayGray: false
438
+ }
439
+ ]
440
+
441
+ const multiColumnData = [
442
+ { Date: '10/5/2025', 'COVID-19': '43.6', 'COVID-19-Attribute': 'Marked' },
443
+ { Date: '10/12/2025', 'COVID-19': '40.7', 'COVID-19-Attribute': '' },
444
+ { Date: '10/19/2025', 'COVID-19': '42.6', 'COVID-19-Attribute': 'Marked' }
445
+ ]
446
+
447
+ it('should find circles in standard multi-column format', () => {
448
+ const circles = filterCircles(
449
+ multiColumnCirclesConfig,
450
+ multiColumnData,
451
+ 'COVID-19'
452
+ // No dynamicCategory or originalSeriesKey - standard mode
453
+ )
454
+
455
+ expect(circles).toHaveLength(2)
456
+ expect(circles[0].isFilled).toBe(false)
457
+ expect(circles[0].size).toBe(8)
458
+ })
459
+
460
+ it('should be backwards compatible with 3-argument call', () => {
461
+ // Existing code that doesn't pass dynamicCategory should still work
462
+ const circles = filterCircles(multiColumnCirclesConfig, multiColumnData, 'COVID-19')
463
+
464
+ expect(circles).toHaveLength(2)
465
+ })
466
+ })
467
+
468
+ describe('filled circles', () => {
469
+ const filledCirclesConfig: PreliminaryDataItem[] = [
470
+ {
471
+ type: 'effect',
472
+ seriesKeys: ['Influenza'],
473
+ column: 'Attribute',
474
+ value: 'Marked',
475
+ style: 'Filled Circles',
476
+ label: 'Flu Marked',
477
+ displayTooltip: true,
478
+ displayLegend: true,
479
+ displayTable: true,
480
+ symbol: '',
481
+ iconCode: '',
482
+ lineCode: '',
483
+ hideBarSymbol: false,
484
+ hideLineStyle: false,
485
+ circleSize: 6,
486
+ displayGray: false
487
+ }
488
+ ]
489
+
490
+ it('should find filled circles for dynamic category data', () => {
491
+ const data = [
492
+ { Date: '10/5/2025', Category: 'Influenza', Value: '23.1', Attribute: 'Marked' },
493
+ { Date: '10/12/2025', Category: 'Influenza', Value: '31.7', Attribute: '' }
494
+ ]
495
+
496
+ const circles = filterCircles(filledCirclesConfig, data, 'Influenza', 'Category', 'Value')
497
+
498
+ expect(circles).toHaveLength(1)
499
+ expect(circles[0].isFilled).toBe(true)
500
+ expect(circles[0].size).toBe(6)
501
+ })
502
+
503
+ it('should skip filled circles for nonnumeric values while preserving zero values', () => {
504
+ const data = [
505
+ { Date: '10/5/2025', Category: 'Influenza', Value: 'N/A', Attribute: 'Marked' },
506
+ { Date: '10/12/2025', Category: 'Influenza', Value: null, Attribute: 'Marked' },
507
+ { Date: '10/19/2025', Category: 'Influenza', Value: 0, Attribute: 'Marked' },
508
+ { Date: '10/26/2025', Category: 'Influenza', Value: '12.3', Attribute: 'Marked' }
509
+ ]
510
+
511
+ const circles = filterCircles(filledCirclesConfig, data, 'Influenza', 'Category', 'Value')
512
+
513
+ expect(circles).toHaveLength(2)
514
+ expect(circles.map(circle => circle.data.Value)).toEqual([0, '12.3'])
515
+ })
516
+ })
517
+ })
518
+
519
+ describe('createDataSegments', () => {
520
+ const mockColorScale = vi.fn(() => '#000')
521
+
522
+ const makeSuppression = (overrides: Partial<PreliminaryDataItem> = {}): PreliminaryDataItem => ({
523
+ type: 'suppression',
524
+ seriesKeys: ['COVID-19'],
525
+ column: '',
526
+ value: 'Suppressed',
527
+ style: 'Dashed Small',
528
+ label: 'Suppressed',
529
+ displayTooltip: true,
530
+ displayLegend: true,
531
+ displayTable: true,
532
+ symbol: '',
533
+ iconCode: '',
534
+ lineCode: '',
535
+ hideBarSymbol: false,
536
+ hideLineStyle: false,
537
+ circleSize: 6,
538
+ displayGray: false,
539
+ ...overrides
540
+ })
541
+
542
+ const suppressionData = [
543
+ { Date: '10/5/2025', 'COVID-19': '43.6' },
544
+ { Date: '10/12/2025', 'COVID-19': 'Suppressed' },
545
+ { Date: '10/19/2025', 'COVID-19': '42.6' }
546
+ ]
547
+
548
+ it('propagates the suppression weight onto the generated segment', () => {
549
+ const segments = createDataSegments({
550
+ data: suppressionData,
551
+ seriesKey: 'COVID-19',
552
+ preliminaryData: [makeSuppression({ weight: 5 })],
553
+ colorScale: mockColorScale
554
+ })
555
+
556
+ const suppressedSegments = segments.filter(segment => segment.style)
557
+ expect(suppressedSegments.length).toBeGreaterThan(0)
558
+ expect(suppressedSegments.every(segment => segment.weight === 5)).toBe(true)
559
+ })
560
+
561
+ it('leaves the segment weight undefined when no custom weight is set', () => {
562
+ const segments = createDataSegments({
563
+ data: suppressionData,
564
+ seriesKey: 'COVID-19',
565
+ preliminaryData: [makeSuppression()],
566
+ colorScale: mockColorScale
567
+ })
568
+
569
+ const suppressedSegments = segments.filter(segment => segment.style)
570
+ expect(suppressedSegments.length).toBeGreaterThan(0)
571
+ expect(suppressedSegments.every(segment => segment.weight === undefined)).toBe(true)
572
+ })
573
+ })
574
+ })