@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,197 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getColorScale } from '../getColorScale'
3
+ import { ChartConfig } from '../../types/ChartConfig'
4
+
5
+ const buildConfig = (overrides: Partial<ChartConfig> = {}): ChartConfig =>
6
+ ({
7
+ visualizationType: 'Line',
8
+ visualizationSubType: 'regular',
9
+ general: {
10
+ palette: {
11
+ customColors: ['#111111', '#222222', '#333333'],
12
+ colorAssignmentMode: 'by-value',
13
+ colorAssignments: [
14
+ { key: 'series_a', color: '#aa0000' },
15
+ { key: 'series_c', color: '#00cc00' }
16
+ ]
17
+ }
18
+ },
19
+ legend: {},
20
+ series: [
21
+ { dataKey: 'series_a', name: 'Series A', type: 'Line' },
22
+ { dataKey: 'series_b', name: 'Series B', type: 'Line' },
23
+ { dataKey: 'series_c', name: 'Series C', type: 'Line' }
24
+ ],
25
+ runtime: {
26
+ seriesKeys: ['series_a', 'series_b', 'series_c'],
27
+ seriesLabels: {
28
+ series_a: 'Series A',
29
+ series_b: 'Series B',
30
+ series_c: 'Series C'
31
+ },
32
+ seriesLabelsAll: ['Series A', 'Series B', 'Series C']
33
+ },
34
+ ...overrides
35
+ } as ChartConfig)
36
+
37
+ describe('getColorScale series color assignments', () => {
38
+ it('keeps assigned series colors stable when runtime series order changes', () => {
39
+ const colorScale = getColorScale(
40
+ buildConfig({
41
+ runtime: {
42
+ seriesKeys: ['series_c', 'series_a'],
43
+ seriesLabels: {
44
+ series_a: 'Series A',
45
+ series_c: 'Series C'
46
+ },
47
+ seriesLabelsAll: ['Series C', 'Series A']
48
+ } as any
49
+ })
50
+ )
51
+
52
+ expect(colorScale('Series A')).toBe('#aa0000')
53
+ expect(colorScale('Series C')).toBe('#00cc00')
54
+ })
55
+
56
+ it('keeps assigned colors when missing series later reappear', () => {
57
+ const filteredScale = getColorScale(
58
+ buildConfig({
59
+ runtime: {
60
+ seriesKeys: ['series_a'],
61
+ seriesLabels: { series_a: 'Series A' },
62
+ seriesLabelsAll: ['Series A']
63
+ } as any
64
+ })
65
+ )
66
+ const restoredScale = getColorScale(buildConfig())
67
+
68
+ expect(filteredScale('Series A')).toBe('#aa0000')
69
+ expect(restoredScale('Series C')).toBe('#00cc00')
70
+ })
71
+
72
+ it('uses ordered palette/custom fallback for unassigned series', () => {
73
+ const colorScale = getColorScale(buildConfig())
74
+
75
+ expect(colorScale('Series A')).toBe('#aa0000')
76
+ expect(colorScale('Series B')).toBe('#222222')
77
+ expect(colorScale('Series C')).toBe('#00cc00')
78
+ })
79
+
80
+ it('resolves assignments by dataKey when display labels differ', () => {
81
+ const colorScale = getColorScale(
82
+ buildConfig({
83
+ series: [{ dataKey: 'raw_value', name: 'Display Value', type: 'Line' }] as any,
84
+ runtime: {
85
+ seriesKeys: ['raw_value'],
86
+ seriesLabels: { raw_value: 'Display Value' },
87
+ seriesLabelsAll: ['Display Value']
88
+ } as any,
89
+ general: {
90
+ palette: {
91
+ customColors: ['#111111'],
92
+ colorAssignmentMode: 'by-value',
93
+ colorAssignments: [{ key: 'raw_value', color: '#123456' }]
94
+ }
95
+ } as any
96
+ })
97
+ )
98
+
99
+ expect(colorScale('Display Value')).toBe('#123456')
100
+ })
101
+
102
+ it('preserves ordered mode behavior when assignments are present', () => {
103
+ const colorScale = getColorScale(
104
+ buildConfig({
105
+ general: {
106
+ palette: {
107
+ customColors: ['#111111', '#222222', '#333333'],
108
+ colorAssignmentMode: 'ordered',
109
+ colorAssignments: [{ key: 'series_a', color: '#aa0000' }]
110
+ }
111
+ } as any
112
+ })
113
+ )
114
+
115
+ expect(colorScale('Series A')).toBe('#111111')
116
+ expect(colorScale('Series B')).toBe('#222222')
117
+ })
118
+
119
+ it('preserves scatter plot customColors through the shared scale', () => {
120
+ const colorScale = getColorScale(
121
+ buildConfig({
122
+ visualizationType: 'Scatter Plot',
123
+ general: {
124
+ palette: {
125
+ customColors: ['#101010', '#202020', '#303030']
126
+ }
127
+ } as any
128
+ })
129
+ )
130
+
131
+ expect(colorScale('Series A')).toBe('#101010')
132
+ expect(colorScale('Series B')).toBe('#202020')
133
+ })
134
+
135
+ it('uses v2 customColors in authored order when customColorsOrdered is absent', () => {
136
+ const colorScale = getColorScale(
137
+ buildConfig({
138
+ general: {
139
+ palette: {
140
+ name: 'qualitative_standard',
141
+ version: '2.0',
142
+ customColors: [
143
+ '#111111',
144
+ '#222222',
145
+ '#333333',
146
+ '#444444',
147
+ '#555555',
148
+ '#666666',
149
+ '#777777',
150
+ '#888888',
151
+ '#999999'
152
+ ]
153
+ }
154
+ } as any
155
+ })
156
+ )
157
+
158
+ expect(colorScale('Series A')).toBe('#111111')
159
+ expect(colorScale('Series B')).toBe('#222222')
160
+ expect(colorScale('Series C')).toBe('#333333')
161
+ })
162
+
163
+ it('falls back to customColors when customColorsOrdered is empty', () => {
164
+ const colorScale = getColorScale(
165
+ buildConfig({
166
+ general: {
167
+ palette: {
168
+ customColorsOrdered: [],
169
+ customColors: ['#101010', '#202020', '#303030']
170
+ }
171
+ } as any
172
+ })
173
+ )
174
+
175
+ expect(colorScale('Series A')).toBe('#101010')
176
+ expect(colorScale('Series B')).toBe('#202020')
177
+ expect(colorScale('Series C')).toBe('#303030')
178
+ })
179
+
180
+ it('falls back to the named palette when customColors is empty', () => {
181
+ const colorScale = getColorScale(
182
+ buildConfig({
183
+ general: {
184
+ palette: {
185
+ name: 'qualitative_standard',
186
+ version: '2.0',
187
+ customColors: []
188
+ }
189
+ } as any
190
+ })
191
+ )
192
+
193
+ expect(colorScale('Series A')).toBe('#0057B7')
194
+ expect(colorScale('Series B')).toBe('#00B1CE')
195
+ expect(colorScale('Series C')).toBe('#5A8E3F')
196
+ })
197
+ })
@@ -0,0 +1,211 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import getMinMax from '../getMinMax'
3
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
+ import { ChartConfig } from '../../types/ChartConfig'
5
+
6
+ const leftSeries = { dataKey: 'Cases', type: 'Line', axis: 'Left', tooltip: true }
7
+ const rightSeries = { dataKey: 'Rate', type: 'Line', axis: 'Right', tooltip: true }
8
+
9
+ const createConfig = (overrides: Partial<ChartConfig> = {}) =>
10
+ createMockConfig({
11
+ visualizationType: 'Line',
12
+ visualizationSubType: 'regular',
13
+ yAxis: {
14
+ ...createMockConfig().yAxis,
15
+ type: 'linear',
16
+ enablePadding: false,
17
+ scalePadding: 0,
18
+ min: '',
19
+ max: '',
20
+ autoMaxStrategy: 'clean-top-tick'
21
+ },
22
+ runtime: {
23
+ ...createMockConfig().runtime,
24
+ yAxis: {
25
+ ...createMockConfig().runtime.yAxis,
26
+ min: '',
27
+ max: ''
28
+ },
29
+ series: [leftSeries],
30
+ seriesKeys: ['Cases']
31
+ } as any,
32
+ series: [leftSeries],
33
+ ...overrides
34
+ })
35
+
36
+ const getResult = (config: ChartConfig, maxValue = 25) =>
37
+ getMinMax({
38
+ config,
39
+ minValue: 0,
40
+ maxValue,
41
+ existPositiveValue: maxValue > 0,
42
+ data: [{ Cases: maxValue }],
43
+ tableData: [{ Cases: maxValue }],
44
+ isAllLine: true
45
+ })
46
+
47
+ describe('getMinMax automatic max strategy', () => {
48
+ it('uses clean-top-tick for automatic left-axis max when enabled', () => {
49
+ expect(getResult(createConfig(), 25).max).toBe(25)
50
+ expect(getResult(createConfig(), 101).max).toBe(120)
51
+ expect(getResult(createConfig(), 1434).max).toBe(1500)
52
+ })
53
+
54
+ it('uses the raw automatic max when strategy is omitted or default', () => {
55
+ expect(getResult(createConfig({ yAxis: { ...createConfig().yAxis, autoMaxStrategy: undefined } }), 101).max).toBe(
56
+ 101
57
+ )
58
+ expect(getResult(createConfig({ yAxis: { ...createConfig().yAxis, autoMaxStrategy: 'default' } }), 101).max).toBe(
59
+ 101
60
+ )
61
+ })
62
+
63
+ it('bypasses clean-top-tick when the left-axis max is explicit', () => {
64
+ const config = createConfig({
65
+ yAxis: {
66
+ ...createConfig().yAxis,
67
+ max: '100'
68
+ },
69
+ runtime: {
70
+ ...createConfig().runtime,
71
+ yAxis: {
72
+ ...createConfig().runtime.yAxis,
73
+ min: '',
74
+ max: '100'
75
+ }
76
+ } as any
77
+ })
78
+
79
+ expect(getResult(config, 25).max).toBe(100)
80
+ })
81
+
82
+ it('uses clean-top-tick when the entered left-axis max is invalid', () => {
83
+ const createConfigWithRuntimeMax = (max: string) =>
84
+ createConfig({
85
+ yAxis: {
86
+ ...createConfig().yAxis,
87
+ max
88
+ },
89
+ runtime: {
90
+ ...createConfig().runtime,
91
+ yAxis: {
92
+ ...createConfig().runtime.yAxis,
93
+ min: '',
94
+ max
95
+ }
96
+ } as any
97
+ })
98
+
99
+ expect(getResult(createConfigWithRuntimeMax('50'), 101).max).toBe(120)
100
+ expect(getResult(createConfigWithRuntimeMax('not-a-number'), 101).max).toBe(120)
101
+ })
102
+
103
+ it('applies smallestLeftAxisMax as a final author-provided floor without cleaning it', () => {
104
+ const config = createConfig({
105
+ yAxis: {
106
+ ...createConfig().yAxis,
107
+ autoMaxStrategy: 'clean-top-tick',
108
+ smallestLeftAxisMax: 12
109
+ }
110
+ })
111
+
112
+ expect(getResult(config, 3).max).toBe(12)
113
+ })
114
+
115
+ it('allows axis padding to increase the domain after the automatic max strategy', () => {
116
+ const config = createConfig({
117
+ yAxis: {
118
+ ...createConfig().yAxis,
119
+ autoMaxStrategy: 'clean-top-tick',
120
+ enablePadding: true,
121
+ scalePadding: 10
122
+ }
123
+ })
124
+
125
+ expect(getResult(config, 25).max).toBeCloseTo(27.5)
126
+ })
127
+
128
+ it('uses clean-top-tick for combo left-axis max while leaving right-axis strategy to useRightAxis', () => {
129
+ const config = createConfig({
130
+ visualizationType: 'Combo',
131
+ series: [leftSeries, rightSeries],
132
+ runtime: {
133
+ ...createConfig().runtime,
134
+ yAxis: {
135
+ ...createConfig().runtime.yAxis,
136
+ min: '',
137
+ max: ''
138
+ },
139
+ series: [leftSeries, rightSeries],
140
+ seriesKeys: ['Cases', 'Rate']
141
+ } as any
142
+ })
143
+
144
+ const result = getMinMax({
145
+ config,
146
+ minValue: 0,
147
+ maxValue: 101,
148
+ existPositiveValue: true,
149
+ data: [{ Cases: 101, Rate: 89 }],
150
+ tableData: [{ Cases: 101, Rate: 89 }],
151
+ isAllLine: false
152
+ })
153
+
154
+ expect(result.leftMax).toBe(120)
155
+ expect(result.rightMax).toBe(89)
156
+ })
157
+
158
+ it('forces a zero min for line charts when boundary suppression is present', () => {
159
+ const config = createConfig({
160
+ visualizationType: 'Line',
161
+ runtime: {
162
+ ...createConfig().runtime,
163
+ yAxis: {
164
+ ...createConfig().runtime.yAxis,
165
+ min: '',
166
+ max: ''
167
+ }
168
+ } as any
169
+ })
170
+
171
+ const result = getMinMax({
172
+ config,
173
+ minValue: 70,
174
+ maxValue: 120,
175
+ existPositiveValue: true,
176
+ data: [{ Cases: 70 }, { Cases: 120 }],
177
+ tableData: [{ Cases: 70 }, { Cases: 120 }],
178
+ isAllLine: true,
179
+ hasBoundarySuppression: true
180
+ })
181
+
182
+ expect(result.min).toBe(0)
183
+ })
184
+
185
+ it('keeps data min for line charts when boundary suppression is not present', () => {
186
+ const config = createConfig({
187
+ visualizationType: 'Line',
188
+ runtime: {
189
+ ...createConfig().runtime,
190
+ yAxis: {
191
+ ...createConfig().runtime.yAxis,
192
+ min: '',
193
+ max: ''
194
+ }
195
+ } as any
196
+ })
197
+
198
+ const result = getMinMax({
199
+ config,
200
+ minValue: 70,
201
+ maxValue: 120,
202
+ existPositiveValue: true,
203
+ data: [{ Cases: 70 }, { Cases: 120 }],
204
+ tableData: [{ Cases: 70 }, { Cases: 120 }],
205
+ isAllLine: true,
206
+ hasBoundarySuppression: false
207
+ })
208
+
209
+ expect(result.min).toBe(70)
210
+ })
211
+ })
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getYAxisDomainData, getYAxisFilterDomainBehavior } from '../getYAxisDomainData'
3
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
+
5
+ describe('getYAxisDomainData', () => {
6
+ const filteredRows = [{ value: 3 }]
7
+ const tableRows = [{ value: 3 }, { value: 9 }]
8
+ const postExclusionRows = [{ value: 3 }, { value: 12 }]
9
+
10
+ const buildConfig = ({
11
+ brushActive = false,
12
+ brushDynamicYAxis = false,
13
+ filterDomainBehavior
14
+ }: {
15
+ brushActive?: boolean
16
+ brushDynamicYAxis?: boolean
17
+ filterDomainBehavior?: 'dynamic' | 'stable'
18
+ }) => {
19
+ const baseConfig = createMockConfig()
20
+ return createMockConfig({
21
+ xAxis: {
22
+ ...baseConfig.xAxis,
23
+ brushActive,
24
+ brushDynamicYAxis
25
+ },
26
+ yAxis: {
27
+ ...baseConfig.yAxis,
28
+ filterDomainBehavior
29
+ }
30
+ })
31
+ }
32
+
33
+ it('defaults omitted filterDomainBehavior to dynamic', () => {
34
+ const config = createMockConfig()
35
+
36
+ expect(getYAxisFilterDomainBehavior(config)).toBe('dynamic')
37
+ })
38
+
39
+ it.each([
40
+ {
41
+ name: 'brush off with dynamic filter domains',
42
+ configOptions: { brushActive: false, filterDomainBehavior: 'dynamic' as const },
43
+ expectedRows: filteredRows
44
+ },
45
+ {
46
+ name: 'brush off with stable filter domains',
47
+ configOptions: { brushActive: false, filterDomainBehavior: 'stable' as const },
48
+ expectedRows: postExclusionRows
49
+ },
50
+ {
51
+ name: 'dynamic brush Y-axis with dynamic filter domains',
52
+ configOptions: { brushActive: true, brushDynamicYAxis: true, filterDomainBehavior: 'dynamic' as const },
53
+ expectedRows: filteredRows
54
+ },
55
+ {
56
+ name: 'dynamic brush Y-axis with stable filter domains',
57
+ configOptions: { brushActive: true, brushDynamicYAxis: true, filterDomainBehavior: 'stable' as const },
58
+ expectedRows: filteredRows
59
+ },
60
+ {
61
+ name: 'non-dynamic brush Y-axis with dynamic filter domains',
62
+ configOptions: { brushActive: true, brushDynamicYAxis: false, filterDomainBehavior: 'dynamic' as const },
63
+ expectedRows: tableRows
64
+ },
65
+ {
66
+ name: 'non-dynamic brush Y-axis with stable filter domains',
67
+ configOptions: { brushActive: true, brushDynamicYAxis: false, filterDomainBehavior: 'stable' as const },
68
+ expectedRows: postExclusionRows
69
+ }
70
+ ])('selects the expected domain data for $name', ({ configOptions, expectedRows }) => {
71
+ expect(
72
+ getYAxisDomainData({
73
+ config: buildConfig(configOptions),
74
+ data: filteredRows,
75
+ tableData: tableRows,
76
+ fullEligibleDomainData: postExclusionRows
77
+ })
78
+ ).toBe(expectedRows)
79
+ })
80
+
81
+ it('falls back to rendered data for non-dynamic brush domains when table data is unavailable', () => {
82
+ expect(
83
+ getYAxisDomainData({
84
+ config: buildConfig({
85
+ brushActive: true,
86
+ brushDynamicYAxis: false,
87
+ filterDomainBehavior: 'dynamic'
88
+ }),
89
+ data: filteredRows
90
+ })
91
+ ).toBe(filteredRows)
92
+ })
93
+
94
+ it('falls back to rendered data for stable filter domains when full eligible data is unavailable', () => {
95
+ expect(
96
+ getYAxisDomainData({
97
+ config: buildConfig({
98
+ filterDomainBehavior: 'stable'
99
+ }),
100
+ data: filteredRows,
101
+ tableData: tableRows
102
+ })
103
+ ).toBe(filteredRows)
104
+ })
105
+
106
+ it('falls back to table data for non-dynamic brush stable domains when full eligible data is unavailable', () => {
107
+ expect(
108
+ getYAxisDomainData({
109
+ config: buildConfig({
110
+ brushActive: true,
111
+ brushDynamicYAxis: false,
112
+ filterDomainBehavior: 'stable'
113
+ }),
114
+ data: filteredRows,
115
+ tableData: tableRows
116
+ })
117
+ ).toBe(tableRows)
118
+ })
119
+ })
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
3
+ import { getYAxisFinalizationEligibility } from '../getYAxisFinalizationEligibility'
4
+
5
+ const getEligibility = (overrides = {}) =>
6
+ getYAxisFinalizationEligibility({
7
+ config: createMockConfig(overrides),
8
+ hasSpacedInlineLabel: false,
9
+ hasValidExplicitLeftMax: false,
10
+ isHorizontal: false
11
+ })
12
+
13
+ describe('getYAxisFinalizationEligibility', () => {
14
+ it('finalizes automatic clean-top-tick linear Y axes', () => {
15
+ const result = getEligibility({
16
+ yAxis: {
17
+ ...createMockConfig().yAxis,
18
+ type: 'linear',
19
+ autoMaxStrategy: 'clean-top-tick'
20
+ }
21
+ })
22
+
23
+ expect(result.shouldUseCleanTopTick).toBe(true)
24
+ expect(result.shouldFinalizePrimaryYAxis).toBe(true)
25
+ expect(result.shouldUseInlineLabelHeadroom).toBe(false)
26
+ })
27
+
28
+ it('uses spaced inline labels as headroom for standard non-Combo Y axes', () => {
29
+ const result = getYAxisFinalizationEligibility({
30
+ config: createMockConfig({
31
+ yAxis: {
32
+ ...createMockConfig().yAxis,
33
+ type: 'linear',
34
+ autoMaxStrategy: 'default',
35
+ inlineLabel: ' percent'
36
+ }
37
+ }),
38
+ hasSpacedInlineLabel: true,
39
+ hasValidExplicitLeftMax: false,
40
+ isHorizontal: false
41
+ })
42
+
43
+ expect(result.shouldUseInlineLabelHeadroom).toBe(true)
44
+ expect(result.shouldUseCleanTopTick).toBe(true)
45
+ expect(result.shouldFinalizePrimaryYAxis).toBe(true)
46
+ })
47
+
48
+ it('does not use spaced inline labels as headroom for Combo charts', () => {
49
+ const result = getYAxisFinalizationEligibility({
50
+ config: createMockConfig({
51
+ visualizationType: 'Combo',
52
+ yAxis: {
53
+ ...createMockConfig().yAxis,
54
+ type: 'linear',
55
+ autoMaxStrategy: 'default',
56
+ inlineLabel: ' percent'
57
+ }
58
+ }),
59
+ hasSpacedInlineLabel: true,
60
+ hasValidExplicitLeftMax: false,
61
+ isHorizontal: false
62
+ })
63
+
64
+ expect(result.supportsInlineLabelHeadroom).toBe(false)
65
+ expect(result.shouldUseInlineLabelHeadroom).toBe(false)
66
+ expect(result.shouldFinalizePrimaryYAxis).toBe(false)
67
+ })
68
+
69
+ it('still finalizes Combo charts when clean-top-tick is explicitly enabled', () => {
70
+ const result = getEligibility({
71
+ visualizationType: 'Combo',
72
+ yAxis: {
73
+ ...createMockConfig().yAxis,
74
+ type: 'linear',
75
+ autoMaxStrategy: 'clean-top-tick'
76
+ }
77
+ })
78
+
79
+ expect(result.shouldUseCleanTopTick).toBe(true)
80
+ expect(result.shouldFinalizePrimaryYAxis).toBe(true)
81
+ expect(result.shouldUseInlineLabelHeadroom).toBe(false)
82
+ })
83
+
84
+ it('treats a valid explicit left max as a full finalization bypass', () => {
85
+ const result = getYAxisFinalizationEligibility({
86
+ config: createMockConfig({
87
+ yAxis: {
88
+ ...createMockConfig().yAxis,
89
+ type: 'linear',
90
+ autoMaxStrategy: 'clean-top-tick',
91
+ inlineLabel: ' percent'
92
+ }
93
+ }),
94
+ hasSpacedInlineLabel: true,
95
+ hasValidExplicitLeftMax: true,
96
+ isHorizontal: false
97
+ })
98
+
99
+ expect(result.shouldUseCleanTopTick).toBe(false)
100
+ expect(result.shouldFinalizePrimaryYAxis).toBe(false)
101
+ })
102
+
103
+ it('does not finalize horizontal, categorical, logarithmic, or special chart Y axes', () => {
104
+ expect(
105
+ getYAxisFinalizationEligibility({
106
+ config: createMockConfig({
107
+ yAxis: {
108
+ ...createMockConfig().yAxis,
109
+ type: 'linear',
110
+ autoMaxStrategy: 'clean-top-tick'
111
+ }
112
+ }),
113
+ hasSpacedInlineLabel: false,
114
+ hasValidExplicitLeftMax: false,
115
+ isHorizontal: true
116
+ }).shouldFinalizePrimaryYAxis
117
+ ).toBe(false)
118
+
119
+ expect(
120
+ getEligibility({
121
+ yAxis: {
122
+ ...createMockConfig().yAxis,
123
+ type: 'categorical',
124
+ autoMaxStrategy: 'clean-top-tick'
125
+ }
126
+ }).shouldFinalizePrimaryYAxis
127
+ ).toBe(false)
128
+
129
+ expect(
130
+ getEligibility({
131
+ yAxis: {
132
+ ...createMockConfig().yAxis,
133
+ type: 'logarithmic',
134
+ autoMaxStrategy: 'clean-top-tick'
135
+ }
136
+ }).shouldFinalizePrimaryYAxis
137
+ ).toBe(false)
138
+
139
+ expect(
140
+ getEligibility({
141
+ visualizationType: 'Box Plot',
142
+ yAxis: {
143
+ ...createMockConfig().yAxis,
144
+ type: 'linear',
145
+ autoMaxStrategy: 'clean-top-tick'
146
+ }
147
+ }).shouldFinalizePrimaryYAxis
148
+ ).toBe(false)
149
+
150
+ expect(
151
+ getEligibility({
152
+ visualizationType: 'Bump Chart',
153
+ yAxis: {
154
+ ...createMockConfig().yAxis,
155
+ type: 'linear',
156
+ autoMaxStrategy: 'clean-top-tick'
157
+ }
158
+ }).shouldFinalizePrimaryYAxis
159
+ ).toBe(false)
160
+
161
+ expect(
162
+ getEligibility({
163
+ visualizationType: 'Forest Plot',
164
+ yAxis: {
165
+ ...createMockConfig().yAxis,
166
+ type: 'linear',
167
+ autoMaxStrategy: 'clean-top-tick'
168
+ }
169
+ }).shouldFinalizePrimaryYAxis
170
+ ).toBe(false)
171
+ })
172
+
173
+ it('treats omitted legacy Y-axis type as linear', () => {
174
+ const result = getEligibility({
175
+ yAxis: {
176
+ ...createMockConfig().yAxis,
177
+ type: undefined,
178
+ autoMaxStrategy: 'clean-top-tick'
179
+ }
180
+ })
181
+
182
+ expect(result.hasStandardLinearPrimaryYAxis).toBe(true)
183
+ expect(result.shouldFinalizePrimaryYAxis).toBe(true)
184
+ })
185
+ })