@cdc/chart 4.26.3 → 4.26.5

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 (192) hide show
  1. package/CONFIG.md +489 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39024 -36587
  4. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  5. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  6. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  7. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  8. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  9. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  10. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  11. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  12. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  13. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  14. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  15. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  16. package/examples/metadata-variables.json +1 -1
  17. package/examples/minimal-example.json +24 -0
  18. package/examples/private/bump-chart.json +1116 -0
  19. package/examples/private/test-123.json +624 -0
  20. package/examples/private/test2.json +35012 -0
  21. package/package.json +5 -4
  22. package/src/CdcChartComponent.tsx +144 -60
  23. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  24. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  25. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  26. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  27. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  28. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
  29. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  30. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  31. package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
  32. package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
  33. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  34. package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
  35. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  36. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  37. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  38. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  39. package/src/_stories/_mock/heatmap.json +156 -0
  40. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  41. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  42. package/src/_stories/_mock/paired-bar.json +1 -1
  43. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  44. package/src/_stories/_mock/sankey_mock.json +78 -0
  45. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  46. package/src/components/Annotations/components/AnnotationDraggable.tsx +2 -2
  47. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  48. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  49. package/src/components/Axis/BottomAxis.tsx +1 -1
  50. package/src/components/Axis/LeftAxis.tsx +15 -14
  51. package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
  52. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  53. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  54. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  55. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  56. package/src/components/BarChart/components/BarChart.tsx +2 -6
  57. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  58. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  59. package/src/components/Brush/BrushSelector.tsx +88 -187
  60. package/src/components/Brush/brushInteraction.ts +22 -0
  61. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  62. package/src/components/EditorPanel/EditorPanel.tsx +946 -960
  63. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  64. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
  65. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  66. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
  67. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
  68. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  69. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
  70. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  71. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  72. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  73. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  74. package/src/components/EditorPanel/useEditorPermissions.ts +32 -21
  75. package/src/components/Forecasting/Forecasting.tsx +2 -2
  76. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  77. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  78. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  79. package/src/components/HeatMap/heatmap.css +71 -0
  80. package/src/components/HeatMap/helpers/index.ts +261 -0
  81. package/src/components/HeatMap/index.tsx +5 -0
  82. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  83. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  84. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  85. package/src/components/HoverLine/HoverLine.tsx +6 -8
  86. package/src/components/Legend/Legend.Component.tsx +4 -4
  87. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  88. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  89. package/src/components/Legend/helpers/index.ts +0 -2
  90. package/src/components/LineChart/LineChartProps.ts +1 -0
  91. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
  92. package/src/components/LineChart/helpers.test.ts +345 -0
  93. package/src/components/LineChart/helpers.ts +15 -5
  94. package/src/components/LineChart/index.tsx +18 -5
  95. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  96. package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
  97. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  98. package/src/components/LinearChart.tsx +155 -77
  99. package/src/components/PairedBarChart.jsx +2 -2
  100. package/src/components/PieChart/PieChart.tsx +15 -10
  101. package/src/components/RadarChart/RadarChart.tsx +3 -1
  102. package/src/components/Regions/components/Regions.tsx +199 -35
  103. package/src/components/Sankey/components/Sankey.tsx +7 -3
  104. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  105. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  106. package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -3
  107. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  108. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  109. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  110. package/src/components/Sparkline/components/SparkLine.tsx +5 -10
  111. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  112. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  113. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  114. package/src/data/initial-state.js +12 -0
  115. package/src/helpers/axisLabelFontSize.ts +8 -0
  116. package/src/helpers/bumpChartTooltip.ts +54 -0
  117. package/src/helpers/calculateLeftYAxisWidth.ts +197 -0
  118. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  119. package/src/helpers/getMinMax.ts +2 -3
  120. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  121. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  122. package/src/helpers/handleChartAriaLabels.ts +23 -10
  123. package/src/helpers/seriesHighlight.ts +26 -0
  124. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  125. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  126. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  127. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  128. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  129. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  130. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  131. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  132. package/src/helpers/tooltipHelpers.ts +176 -0
  133. package/src/hooks/tests/useScales.test.tsx +105 -0
  134. package/src/hooks/useHighlightedBars.ts +9 -16
  135. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  136. package/src/hooks/useScales.ts +19 -9
  137. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  138. package/src/hooks/useTooltip.tsx +167 -38
  139. package/src/index.jsx +5 -2
  140. package/src/scss/main.scss +32 -49
  141. package/src/test/CdcChart.test.jsx +24 -0
  142. package/src/types/ChartConfig.ts +19 -0
  143. package/src/types/ChartContext.ts +2 -0
  144. package/examples/chart-regression-2.json +0 -2360
  145. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  146. package/examples/feature/__data__/city-temperature.json +0 -2198
  147. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  148. package/examples/feature/boxplot/box-plot-data.json +0 -71
  149. package/examples/feature/boxplot/boxplot-data.json +0 -156
  150. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  151. package/examples/feature/forest-plot/log.json +0 -26
  152. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  153. package/examples/region-issue.json +0 -2065
  154. package/examples/xaxis.json +0 -493
  155. package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
  156. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  157. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  158. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  159. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  160. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  161. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  162. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  163. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  164. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  165. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  166. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  167. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  168. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  169. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  170. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  171. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  172. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  173. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  174. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  175. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  176. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  177. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  178. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  179. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  180. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  181. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  182. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  183. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  184. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  185. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  186. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  187. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  188. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  189. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  190. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  191. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  192. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -0,0 +1,345 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { createStyles, filterCircles } 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
+
185
+ describe('filterCircles', () => {
186
+ const circlesPreliminaryData: PreliminaryDataItem[] = [
187
+ {
188
+ type: 'effect',
189
+ seriesKeys: ['COVID-19'],
190
+ column: 'Attribute',
191
+ value: 'Marked',
192
+ style: 'Open Circles',
193
+ label: 'COVID Marked',
194
+ displayTooltip: true,
195
+ displayLegend: true,
196
+ displayTable: true,
197
+ symbol: '',
198
+ iconCode: '',
199
+ lineCode: '',
200
+ hideBarSymbol: false,
201
+ hideLineStyle: false,
202
+ circleSize: 8,
203
+ displayGray: false
204
+ },
205
+ {
206
+ type: 'effect',
207
+ seriesKeys: ['Influenza'],
208
+ column: 'Attribute',
209
+ value: 'Marked',
210
+ style: 'Filled Circles',
211
+ label: 'Flu Marked',
212
+ displayTooltip: true,
213
+ displayLegend: true,
214
+ displayTable: true,
215
+ symbol: '',
216
+ iconCode: '',
217
+ lineCode: '',
218
+ hideBarSymbol: false,
219
+ hideLineStyle: false,
220
+ circleSize: 6,
221
+ displayGray: false
222
+ }
223
+ ]
224
+
225
+ describe('with dynamic category data', () => {
226
+ const dynamicData = [
227
+ { Date: '10/5/2025', Category: 'COVID-19', Value: '43.6', Attribute: 'Marked' },
228
+ { Date: '10/12/2025', Category: 'COVID-19', Value: '40.7', Attribute: '' },
229
+ { Date: '10/19/2025', Category: 'COVID-19', Value: '42.6', Attribute: 'Marked' }
230
+ ]
231
+
232
+ it('should find open circles for matching dynamic category data', () => {
233
+ const circles = filterCircles(
234
+ circlesPreliminaryData,
235
+ dynamicData,
236
+ 'COVID-19', // Runtime series key
237
+ 'Category', // Dynamic category column
238
+ 'Value' // Value column for data existence check
239
+ )
240
+
241
+ expect(circles).toHaveLength(2) // Two data points with Attribute='Marked'
242
+ expect(circles[0].isFilled).toBe(false) // Open circles
243
+ expect(circles[0].size).toBe(8)
244
+ expect(circles[1].isFilled).toBe(false)
245
+ })
246
+
247
+ it('should return empty array when no matches', () => {
248
+ const circles = filterCircles(
249
+ circlesPreliminaryData,
250
+ dynamicData,
251
+ 'RSV', // Series not in preliminaryData
252
+ 'Category',
253
+ 'Value'
254
+ )
255
+
256
+ expect(circles).toHaveLength(0)
257
+ })
258
+ })
259
+
260
+ describe('with multi-column data (standard format)', () => {
261
+ const multiColumnCirclesConfig: PreliminaryDataItem[] = [
262
+ {
263
+ type: 'effect',
264
+ seriesKeys: ['COVID-19'],
265
+ column: 'COVID-19-Attribute',
266
+ value: 'Marked',
267
+ style: 'Open Circles',
268
+ label: 'COVID Marked',
269
+ displayTooltip: true,
270
+ displayLegend: true,
271
+ displayTable: true,
272
+ symbol: '',
273
+ iconCode: '',
274
+ lineCode: '',
275
+ hideBarSymbol: false,
276
+ hideLineStyle: false,
277
+ circleSize: 8,
278
+ displayGray: false
279
+ }
280
+ ]
281
+
282
+ const multiColumnData = [
283
+ { Date: '10/5/2025', 'COVID-19': '43.6', 'COVID-19-Attribute': 'Marked' },
284
+ { Date: '10/12/2025', 'COVID-19': '40.7', 'COVID-19-Attribute': '' },
285
+ { Date: '10/19/2025', 'COVID-19': '42.6', 'COVID-19-Attribute': 'Marked' }
286
+ ]
287
+
288
+ it('should find circles in standard multi-column format', () => {
289
+ const circles = filterCircles(
290
+ multiColumnCirclesConfig,
291
+ multiColumnData,
292
+ 'COVID-19'
293
+ // No dynamicCategory or originalSeriesKey - standard mode
294
+ )
295
+
296
+ expect(circles).toHaveLength(2)
297
+ expect(circles[0].isFilled).toBe(false)
298
+ expect(circles[0].size).toBe(8)
299
+ })
300
+
301
+ it('should be backwards compatible with 3-argument call', () => {
302
+ // Existing code that doesn't pass dynamicCategory should still work
303
+ const circles = filterCircles(multiColumnCirclesConfig, multiColumnData, 'COVID-19')
304
+
305
+ expect(circles).toHaveLength(2)
306
+ })
307
+ })
308
+
309
+ describe('filled circles', () => {
310
+ const filledCirclesConfig: PreliminaryDataItem[] = [
311
+ {
312
+ type: 'effect',
313
+ seriesKeys: ['Influenza'],
314
+ column: 'Attribute',
315
+ value: 'Marked',
316
+ style: 'Filled Circles',
317
+ label: 'Flu Marked',
318
+ displayTooltip: true,
319
+ displayLegend: true,
320
+ displayTable: true,
321
+ symbol: '',
322
+ iconCode: '',
323
+ lineCode: '',
324
+ hideBarSymbol: false,
325
+ hideLineStyle: false,
326
+ circleSize: 6,
327
+ displayGray: false
328
+ }
329
+ ]
330
+
331
+ it('should find filled circles for dynamic category data', () => {
332
+ const data = [
333
+ { Date: '10/5/2025', Category: 'Influenza', Value: '23.1', Attribute: 'Marked' },
334
+ { Date: '10/12/2025', Category: 'Influenza', Value: '31.7', Attribute: '' }
335
+ ]
336
+
337
+ const circles = filterCircles(filledCirclesConfig, data, 'Influenza', 'Category', 'Value')
338
+
339
+ expect(circles).toHaveLength(1)
340
+ expect(circles[0].isFilled).toBe(true)
341
+ expect(circles[0].size).toBe(6)
342
+ })
343
+ })
344
+ })
345
+ })
@@ -15,17 +15,18 @@ export const createStyles = (props: StyleProps): Style[] => {
15
15
  originalSeriesKey
16
16
  } = props
17
17
 
18
- const dynamicSeriesKey = dynamicCategory ? originalSeriesKey : seriesKey
19
18
  const validPreliminaryData: PreliminaryDataItem[] = preliminaryData.filter(
20
19
  pd => pd.seriesKeys?.length && pd.column && pd.value && pd.type && pd.style && pd.type === 'effect'
21
20
  )
22
21
  const isEffectLine = (pd, dataPoint) => {
23
22
  if (dynamicCategory) {
23
+ // In dynamic category mode, check the attribute column (pd.column) for the effect marker
24
+ // seriesKey is the runtime series key derived from the category column
24
25
  return (
25
26
  pd.type === 'effect' &&
26
27
  pd.style !== 'Open Circles' &&
27
28
  pd.seriesKeys.includes(seriesKey) &&
28
- String(dataPoint[dynamicSeriesKey]) === String(pd.value)
29
+ dataPoint[pd.column] === pd.value
29
30
  )
30
31
  } else {
31
32
  return (
@@ -67,8 +68,17 @@ export const createStyles = (props: StyleProps): Style[] => {
67
68
  export const filterCircles = (
68
69
  preliminaryData: PreliminaryDataItem[],
69
70
  data: DataItem[],
70
- seriesKey: string
71
+ seriesKey: string,
72
+ dynamicCategory?: string,
73
+ originalSeriesKey?: string
71
74
  ): DataItem[] => {
75
+ // In dynamic category mode:
76
+ // - seriesKey is the runtime series key for matching against seriesKeys
77
+ // - originalSeriesKey is the data value column (e.g., "Value") for checking if data exists
78
+ // In standard mode:
79
+ // - seriesKey is used for both purposes
80
+ const valueKey = dynamicCategory ? originalSeriesKey : seriesKey
81
+
72
82
  // Filter and map preliminaryData to get circlesFiltered
73
83
  const circlesFiltered = preliminaryData
74
84
  ?.filter(item => item.style.includes('Circles') && item.type === 'effect' && item.seriesKeys?.length)
@@ -86,7 +96,7 @@ export const filterCircles = (
86
96
  if (
87
97
  item[fc.column] === fc.value &&
88
98
  fc.seriesKeys.includes(seriesKey) &&
89
- item[seriesKey] &&
99
+ item[valueKey] &&
90
100
  fc.style === 'Open Circles'
91
101
  ) {
92
102
  const result = {
@@ -99,7 +109,7 @@ export const filterCircles = (
99
109
  if (
100
110
  (!fc.value || item[fc.column] === fc.value) &&
101
111
  fc.seriesKeys.includes(seriesKey) &&
102
- item[seriesKey] &&
112
+ item[valueKey] &&
103
113
  fc.style === 'Filled Circles'
104
114
  ) {
105
115
  const result = {
@@ -36,6 +36,7 @@ const LineChart = (props: LineChartProps) => {
36
36
  xScale,
37
37
  yMax,
38
38
  yScale,
39
+ yAxisWidth,
39
40
  } = props
40
41
 
41
42
  // prettier-ignore
@@ -55,7 +56,7 @@ const LineChart = (props: LineChartProps) => {
55
56
 
56
57
  return (
57
58
  <ErrorBoundary component='LineChart'>
58
- <Group left={Number(config.runtime.yAxis.size)} className='line-chart-group'>
59
+ <Group left={yAxisWidth} className='line-chart-group'>
59
60
  {/* left - expects a number not a string */}
60
61
  {(config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map((seriesKey, index) => {
61
62
  const seriesData = config.runtime.series.find(item => item.dataKey === seriesKey)
@@ -79,7 +80,13 @@ const LineChart = (props: LineChartProps) => {
79
80
  const _data = seriesData.dynamicCategory
80
81
  ? data.filter(d => d[seriesData.dynamicCategory] === seriesKey)
81
82
  : data
82
- const circleData = filterCircles(config?.preliminaryData, tableData, _seriesKey)
83
+ const circleData = filterCircles(
84
+ config?.preliminaryData,
85
+ tableData,
86
+ seriesKey,
87
+ seriesData.dynamicCategory,
88
+ _seriesKey
89
+ )
83
90
 
84
91
  // Prepare sorted data for line and area rendering
85
92
  const sortedData =
@@ -264,7 +271,7 @@ const LineChart = (props: LineChartProps) => {
264
271
  }
265
272
  styles={createStyles({
266
273
  preliminaryData: config.preliminaryData,
267
- data: tableData,
274
+ data: _data,
268
275
  stroke: colorScale(config.runtime.seriesLabels[seriesKey]),
269
276
  strokeWidth: seriesData.weight || 2,
270
277
  handleLineType,
@@ -496,7 +503,7 @@ const LineChart = (props: LineChartProps) => {
496
503
  </Text>
497
504
  )}
498
505
  </Group>
499
- <Group left={Number(config.runtime.yAxis.size)} className='glyph-tooltip-group'>
506
+ <Group left={yAxisWidth} className='glyph-tooltip-group'>
500
507
  <Group key={`tooltip-group`} display={'block'}>
501
508
  {/* tooltips */}
502
509
  <Bar
@@ -543,7 +550,13 @@ const LineChart = (props: LineChartProps) => {
543
550
  </Group>
544
551
  </Group>
545
552
  {config.visualizationType === 'Bump Chart' && (
546
- <LineChartBumpCircle config={config} xScale={xScale} yScale={yScale} />
553
+ <LineChartBumpCircle
554
+ config={config}
555
+ xScale={xScale}
556
+ yScale={yScale}
557
+ parseDate={parseDate}
558
+ yAxisWidth={yAxisWidth}
559
+ />
547
560
  )}
548
561
  </ErrorBoundary>
549
562
  )
@@ -76,13 +76,16 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
76
76
  )}
77
77
 
78
78
  {/* Paired Bar Chart */}
79
- {visualizationType === 'Paired Bar' && <PairedBarChart originalWidth={parentWidth} width={xMax} height={yMax} />}
79
+ {visualizationType === 'Paired Bar' && (
80
+ <PairedBarChart originalWidth={parentWidth} width={xMax} height={yMax} yAxisWidth={yAxisWidth} />
81
+ )}
80
82
 
81
83
  {/* Scatter Plot */}
82
84
  {visualizationType === 'Scatter Plot' && (
83
85
  <ScatterPlot
84
86
  xScale={xScale}
85
87
  yScale={yScale}
88
+ yAxisWidth={yAxisWidth}
86
89
  getXAxisData={getXAxisData}
87
90
  getYAxisData={getYAxisData}
88
91
  xMax={xMax}
@@ -102,6 +105,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
102
105
  yScale={yScale}
103
106
  xMax={xMax}
104
107
  yMax={yMax}
108
+ yAxisWidth={yAxisWidth}
105
109
  synchronizedXValue={synchronizedXValue}
106
110
  showTooltip={showTooltip}
107
111
  handleTooltipMouseOff={handleTooltipMouseOff}
@@ -115,6 +119,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
115
119
  yScale={yScale}
116
120
  xMax={xMax}
117
121
  yMax={yMax}
122
+ yAxisWidth={yAxisWidth}
118
123
  handleTooltipMouseOver={handleTooltipMouseOver}
119
124
  handleTooltipMouseOff={handleTooltipMouseOff}
120
125
  tooltipData={tooltipData}
@@ -132,6 +137,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
132
137
  max={max}
133
138
  xScale={xScale}
134
139
  yScale={yScale}
140
+ yAxisWidth={yAxisWidth}
135
141
  />
136
142
  )}
137
143
 
@@ -156,6 +162,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
156
162
  yScale={yScale}
157
163
  yMax={yMax}
158
164
  xMax={xMax}
165
+ yAxisWidth={yAxisWidth}
159
166
  chartRef={svgRef}
160
167
  width={xMax}
161
168
  height={yMax}
@@ -193,6 +200,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
193
200
  <LineChart
194
201
  xScale={xScale}
195
202
  yScale={yScale}
203
+ yAxisWidth={yAxisWidth}
196
204
  getXAxisData={getXAxisData}
197
205
  getYAxisData={getYAxisData}
198
206
  xMax={xMax}
@@ -211,6 +219,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
211
219
  <LineChart
212
220
  xScale={xScale}
213
221
  yScale={yScale}
222
+ yAxisWidth={yAxisWidth}
214
223
  getXAxisData={getXAxisData}
215
224
  getYAxisData={getYAxisData}
216
225
  xMax={xMax}
@@ -229,6 +238,7 @@ const VisualizationRenderer: React.FC<VisualizationRendererProps> = ({
229
238
  tooltipData={tooltipData}
230
239
  xScale={xScale}
231
240
  yScale={yScale}
241
+ yAxisWidth={yAxisWidth}
232
242
  width={xMax}
233
243
  height={yMax}
234
244
  xScaleNoPadding={xScaleNoPadding}