@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.
- package/CONFIG.md +353 -29
- package/LICENSE +201 -0
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +37885 -35117
- package/examples/feature/heatmap/average-age-categorical.json +313 -0
- package/examples/feature/heatmap/calendar-heatmap.json +134 -0
- package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
- package/examples/feature/heatmap/sparse-aggregation.json +158 -0
- package/examples/private/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/examples/private/bump-chart.json +1116 -0
- package/examples/private/test-123.json +624 -0
- package/package.json +4 -3
- package/src/CdcChart.tsx +5 -4
- package/src/CdcChartComponent.tsx +166 -62
- package/src/_stories/Chart.AltText.stories.tsx +136 -0
- package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
- package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
- package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
- package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
- package/src/_stories/_mock/alt_text_metadata.json +38 -0
- package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
- package/src/_stories/_mock/heatmap.json +156 -0
- package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
- package/src/_stories/_mock/sankey_mock.json +78 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
- package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +17 -11
- package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- package/src/components/Brush/BrushSelector.tsx +88 -187
- package/src/components/Brush/brushInteraction.ts +22 -0
- package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
- package/src/components/EditorPanel/EditorPanel.tsx +677 -185
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
- package/src/components/HeatMap/components/HeatMap.tsx +767 -0
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
- package/src/components/HeatMap/heatmap.constants.ts +13 -0
- package/src/components/HeatMap/heatmap.css +85 -0
- package/src/components/HeatMap/helpers/index.ts +261 -0
- package/src/components/HeatMap/index.tsx +5 -0
- package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
- package/src/components/HeatMap/tests/helpers.test.ts +167 -0
- package/src/components/Legend/Legend.Component.tsx +2 -2
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
- package/src/components/LineChart/helpers.test.ts +574 -0
- package/src/components/LineChart/helpers.ts +39 -16
- package/src/components/LineChart/index.tsx +28 -5
- package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
- package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
- package/src/components/LinearChart.tsx +103 -47
- package/src/components/PieChart/PieChart.tsx +4 -1
- package/src/components/RadarChart/RadarChart.tsx +3 -1
- package/src/components/Regions/components/Regions.tsx +195 -41
- package/src/components/Sankey/components/Sankey.tsx +7 -3
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
- package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
- package/src/components/SmallMultiples/SmallMultiples.css +12 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
- package/src/components/Sparkline/components/SparkLine.tsx +4 -9
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
- package/src/data/initial-state.js +12 -0
- package/src/helpers/axisLabelFontSize.ts +8 -0
- package/src/helpers/bumpChartTooltip.ts +54 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getHasBoundarySuppression.ts +26 -0
- package/src/helpers/getMinMax.ts +26 -25
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/handleChartAriaLabels.ts +23 -10
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/seriesColumnSettings.ts +5 -4
- package/src/helpers/seriesHighlight.ts +26 -0
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
- package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
- package/src/helpers/tests/seriesHighlight.test.ts +61 -0
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +127 -0
- package/src/hooks/tests/useScales.test.tsx +383 -0
- package/src/hooks/useRightAxis.ts +30 -13
- package/src/hooks/useScales.ts +48 -21
- package/src/hooks/useTooltip.tsx +3 -1
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +32 -49
- package/src/test/CdcChart.dataTable.test.tsx +122 -0
- package/src/types/ChartConfig.ts +24 -1
- package/src/types/ChartContext.ts +5 -0
- 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
|
+
})
|