@cdc/chart 4.26.5 → 4.26.7

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 (128) hide show
  1. package/CONFIG.md +20 -14
  2. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  3. package/dist/cdcchart.js +36554 -34320
  4. package/examples/feature/heatmap/average-age-categorical.json +1 -1
  5. package/examples/feature/heatmap/calendar-heatmap.json +1 -1
  6. package/examples/feature/heatmap/sparse-aggregation.json +1 -1
  7. package/examples/private/bad-y-axis.json +543 -0
  8. package/examples/private/bar-chart-negative-value.json +455 -0
  9. package/examples/private/flueview.json +22051 -0
  10. package/package.json +3 -11
  11. package/src/CdcChart.tsx +7 -6
  12. package/src/CdcChartComponent.tsx +71 -18
  13. package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
  14. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  15. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  16. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  17. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  18. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  19. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  20. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  21. package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
  22. package/src/_stories/_mock/heatmap.json +1 -1
  23. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  24. package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
  25. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  26. package/src/components/Axis/LeftAxis.tsx +3 -0
  27. package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
  28. package/src/components/Axis/RightAxis.tsx +3 -0
  29. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  30. package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
  31. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  32. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
  33. package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
  34. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  35. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  36. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
  37. package/src/components/BarChart/helpers/useBarChart.ts +4 -5
  38. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  39. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  40. package/src/components/BoxPlot/helpers/index.ts +4 -3
  41. package/src/components/Brush/BrushSelector.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +457 -165
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  49. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
  51. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  52. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  53. package/src/components/EditorPanel/components/panels.scss +10 -0
  54. package/src/components/EditorPanel/editor-panel.scss +47 -0
  55. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
  56. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
  57. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
  58. package/src/components/EditorPanel/useEditorPermissions.test.tsx +288 -11
  59. package/src/components/EditorPanel/useEditorPermissions.ts +46 -6
  60. package/src/components/Forecasting/Forecasting.tsx +1 -1
  61. package/src/components/HeatMap/components/HeatMap.tsx +22 -17
  62. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
  63. package/src/components/HeatMap/heatmap.css +15 -1
  64. package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
  65. package/src/components/Legend/Legend.Component.tsx +56 -61
  66. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  67. package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
  68. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  69. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  70. package/src/components/LineChart/helpers.test.ts +230 -1
  71. package/src/components/LineChart/helpers.ts +25 -14
  72. package/src/components/LineChart/index.tsx +23 -7
  73. package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
  74. package/src/components/LinearChart.tsx +51 -15
  75. package/src/components/PieChart/PieChart.tsx +117 -10
  76. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  77. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  78. package/src/components/ScatterPlot/ScatterPlot.jsx +5 -13
  79. package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
  80. package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
  81. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  82. package/src/data/initial-state.js +3 -0
  83. package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
  84. package/src/helpers/categoryOrder.ts +60 -0
  85. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  86. package/src/helpers/dataHelpers.ts +11 -1
  87. package/src/helpers/getAxisMaxOverride.ts +16 -0
  88. package/src/helpers/getBoxPlotConfig.ts +19 -12
  89. package/src/helpers/getCleanTopTickMax.ts +64 -0
  90. package/src/helpers/getColorScale.ts +18 -9
  91. package/src/helpers/getComboChartConfig.ts +0 -1
  92. package/src/helpers/getExcludedData.ts +0 -1
  93. package/src/helpers/getHasBoundarySuppression.ts +27 -0
  94. package/src/helpers/getMinMax.ts +25 -22
  95. package/src/helpers/getNewRuntime.ts +0 -2
  96. package/src/helpers/getPiePercent.ts +3 -3
  97. package/src/helpers/getYAxisDomainData.ts +38 -0
  98. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  99. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  100. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  101. package/src/helpers/seriesColumnSettings.ts +26 -7
  102. package/src/helpers/sizeHelpers.ts +0 -2
  103. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  104. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  105. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  106. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  107. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  108. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  109. package/src/helpers/tests/getColorScale.test.ts +197 -0
  110. package/src/helpers/tests/getMinMax.test.ts +211 -0
  111. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  112. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  113. package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
  114. package/src/helpers/tests/seriesColumnSettings.test.ts +76 -2
  115. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  116. package/src/hooks/tests/useRightAxis.test.tsx +268 -0
  117. package/src/hooks/tests/useScales.test.tsx +302 -27
  118. package/src/hooks/tests/useTooltip.test.tsx +132 -0
  119. package/src/hooks/useRightAxis.ts +35 -25
  120. package/src/hooks/useScales.ts +45 -28
  121. package/src/hooks/useTooltip.tsx +17 -11
  122. package/src/store/chart.reducer.ts +0 -1
  123. package/src/test/CdcChart.dataTable.test.tsx +216 -0
  124. package/src/types/ChartConfig.ts +6 -1
  125. package/src/types/ChartContext.ts +3 -0
  126. package/src/helpers/getYAxisAutoPadding.ts +0 -55
  127. package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
  128. package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
@@ -1,11 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { getYAxisAutoPaddingMode } from '../getYAxisAutoPaddingMode'
2
+ import { hasSpacedInlineLabel } from '../hasSpacedInlineLabel'
3
3
  import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
4
 
5
- describe('getYAxisAutoPaddingMode', () => {
6
- it('returns inline-label for multi-word inline labels', () => {
5
+ describe('hasSpacedInlineLabel', () => {
6
+ it('returns true for multi-word inline labels', () => {
7
7
  expect(
8
- getYAxisAutoPaddingMode(
8
+ hasSpacedInlineLabel(
9
9
  createMockConfig({
10
10
  yAxis: {
11
11
  ...createMockConfig().yAxis,
@@ -14,12 +14,12 @@ describe('getYAxisAutoPaddingMode', () => {
14
14
  }
15
15
  })
16
16
  )
17
- ).toBe('inline-label')
17
+ ).toBe(true)
18
18
  })
19
19
 
20
- it('returns inline-label for inline labels with leading space', () => {
20
+ it('returns true for inline labels with leading space', () => {
21
21
  expect(
22
- getYAxisAutoPaddingMode(
22
+ hasSpacedInlineLabel(
23
23
  createMockConfig({
24
24
  yAxis: {
25
25
  ...createMockConfig().yAxis,
@@ -28,12 +28,12 @@ describe('getYAxisAutoPaddingMode', () => {
28
28
  }
29
29
  })
30
30
  )
31
- ).toBe('inline-label')
31
+ ).toBe(true)
32
32
  })
33
33
 
34
- it('returns top-title for top y-axis titles with label content', () => {
34
+ it('returns false for top y-axis titles with empty inline labels', () => {
35
35
  expect(
36
- getYAxisAutoPaddingMode(
36
+ hasSpacedInlineLabel(
37
37
  createMockConfig({
38
38
  yAxis: {
39
39
  ...createMockConfig().yAxis,
@@ -42,27 +42,12 @@ describe('getYAxisAutoPaddingMode', () => {
42
42
  }
43
43
  })
44
44
  )
45
- ).toBe('top-title')
45
+ ).toBe(false)
46
46
  })
47
47
 
48
- it('returns none for hidden top y-axis titles', () => {
48
+ it('returns false when the inline label has no spaces', () => {
49
49
  expect(
50
- getYAxisAutoPaddingMode(
51
- createMockConfig({
52
- hideYAxisLabel: true,
53
- yAxis: {
54
- ...createMockConfig().yAxis,
55
- titlePlacement: 'top',
56
- inlineLabel: ''
57
- }
58
- })
59
- )
60
- ).toBe('none')
61
- })
62
-
63
- it('returns none when neither trigger applies', () => {
64
- expect(
65
- getYAxisAutoPaddingMode(
50
+ hasSpacedInlineLabel(
66
51
  createMockConfig({
67
52
  yAxis: {
68
53
  ...createMockConfig().yAxis,
@@ -71,12 +56,12 @@ describe('getYAxisAutoPaddingMode', () => {
71
56
  }
72
57
  })
73
58
  )
74
- ).toBe('none')
59
+ ).toBe(false)
75
60
  })
76
61
 
77
- it('returns inline-label when both triggers are present', () => {
62
+ it('returns true for spaced inline labels regardless of title placement', () => {
78
63
  expect(
79
- getYAxisAutoPaddingMode(
64
+ hasSpacedInlineLabel(
80
65
  createMockConfig({
81
66
  yAxis: {
82
67
  ...createMockConfig().yAxis,
@@ -85,6 +70,6 @@ describe('getYAxisAutoPaddingMode', () => {
85
70
  }
86
71
  })
87
72
  )
88
- ).toBe('inline-label')
73
+ ).toBe(true)
89
74
  })
90
75
  })
@@ -53,6 +53,23 @@ describe('seriesColumnSettings', () => {
53
53
  })
54
54
  })
55
55
 
56
+ it('removes fields when an update value is undefined', () => {
57
+ const updatedColumns = upsertSeriesColumnConfig(
58
+ {
59
+ cases: { name: 'cases', label: 'Cases', roundToPlace: 2 }
60
+ },
61
+ 'cases',
62
+ { roundToPlace: undefined }
63
+ )
64
+
65
+ expect(updatedColumns).toEqual({
66
+ cases: {
67
+ name: 'cases',
68
+ label: 'Cases'
69
+ }
70
+ })
71
+ })
72
+
56
73
  it('does not persist display defaults when creating a new series-owned column config', () => {
57
74
  expect(upsertSeriesColumnConfig({}, 'cases', { label: 'Cases' })).toEqual({
58
75
  cases: {
@@ -62,22 +79,79 @@ describe('seriesColumnSettings', () => {
62
79
  })
63
80
  })
64
81
 
82
+ it('does not create a rounding override when updating unrelated display settings', () => {
83
+ expect(upsertSeriesColumnConfig({}, 'cases', { dataTable: false })).toEqual({
84
+ cases: {
85
+ name: 'cases',
86
+ dataTable: false
87
+ }
88
+ })
89
+ })
90
+
65
91
  it('only returns explicit formatting overrides and preserves falsey values', () => {
66
92
  expect(
67
93
  getSeriesColumnFormattingParams({
68
- prefix: '',
94
+ prefix: '$',
69
95
  suffix: ' units',
70
96
  roundToPlace: 0,
71
97
  commas: false
72
98
  })
73
99
  ).toEqual({
74
- addColPrefix: '',
100
+ addColPrefix: '$',
75
101
  addColSuffix: ' units',
76
102
  addColRoundTo: 0,
77
103
  addColCommas: false
78
104
  })
79
105
  })
80
106
 
107
+ it('ignores blank prefix and suffix overrides so global formatting can apply', () => {
108
+ expect(
109
+ getSeriesColumnFormattingParams({
110
+ prefix: '',
111
+ suffix: '',
112
+ roundToPlace: 0,
113
+ commas: false
114
+ })
115
+ ).toEqual({
116
+ addColRoundTo: 0,
117
+ addColCommas: false
118
+ })
119
+ })
120
+
121
+ it('ignores blank rounding overrides so global formatting can apply', () => {
122
+ expect(
123
+ getSeriesColumnFormattingParams({
124
+ roundToPlace: '',
125
+ commas: false
126
+ })
127
+ ).toEqual({
128
+ addColCommas: false
129
+ })
130
+ })
131
+
132
+ it('normalizes string rounding overrides to numbers', () => {
133
+ expect(
134
+ getSeriesColumnFormattingParams({
135
+ roundToPlace: '0',
136
+ commas: false
137
+ })
138
+ ).toEqual({
139
+ addColRoundTo: 0,
140
+ addColCommas: false
141
+ })
142
+ })
143
+
144
+ it('ignores invalid rounding overrides so global formatting can apply', () => {
145
+ expect(
146
+ getSeriesColumnFormattingParams({
147
+ roundToPlace: 'abc',
148
+ commas: false
149
+ })
150
+ ).toEqual({
151
+ addColCommas: false
152
+ })
153
+ })
154
+
81
155
  it('returns undefined when no formatting overrides were explicitly configured', () => {
82
156
  expect(getSeriesColumnFormattingParams({ label: 'Cases' })).toBeUndefined()
83
157
  })
@@ -0,0 +1,99 @@
1
+ import { scaleOrdinal } from '@visx/scale'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { createTileColorScale } from '../smallMultiplesHelpers'
4
+ import { ChartConfig } from '../../types/ChartConfig'
5
+
6
+ const buildConfig = (overrides: Partial<ChartConfig> = {}): ChartConfig =>
7
+ ({
8
+ visualizationType: 'Line',
9
+ visualizationSubType: 'regular',
10
+ general: {
11
+ palette: {
12
+ customColors: ['#111111', '#222222'],
13
+ colorAssignmentMode: 'ordered'
14
+ }
15
+ },
16
+ legend: {},
17
+ smallMultiples: {
18
+ mode: 'by-series',
19
+ colorMode: 'same'
20
+ },
21
+ series: [
22
+ { dataKey: 'series_a', name: 'Series A', type: 'Line' },
23
+ { dataKey: 'series_b', name: 'Series B', type: 'Line' }
24
+ ],
25
+ runtime: {
26
+ seriesKeys: ['series_a', 'series_b'],
27
+ seriesLabels: {
28
+ series_a: 'Series A',
29
+ series_b: 'Series B'
30
+ },
31
+ seriesLabelsAll: ['Series A', 'Series B']
32
+ },
33
+ ...overrides
34
+ } as ChartConfig)
35
+
36
+ describe('smallMultiplesHelpers createTileColorScale', () => {
37
+ it('uses the same first color for by-series tiles in same color mode by default', () => {
38
+ const colorScale = scaleOrdinal({
39
+ domain: ['Series A', 'Series B'],
40
+ range: ['#111111', '#222222'],
41
+ unknown: null
42
+ })
43
+
44
+ const tileScale = createTileColorScale(
45
+ { mode: 'by-series', seriesKey: 'series_b' },
46
+ buildConfig(),
47
+ colorScale,
48
+ 1,
49
+ 2
50
+ )
51
+
52
+ expect(tileScale('Series A')).toBe('#111111')
53
+ expect(tileScale('Series B')).toBe('#111111')
54
+ })
55
+
56
+ it('lets by-value assignments override same color mode for by-series tiles', () => {
57
+ const colorScale = scaleOrdinal({
58
+ domain: ['Series A', 'Series B'],
59
+ range: ['#aa0000', '#00bb00'],
60
+ unknown: null
61
+ })
62
+ const config = buildConfig({
63
+ general: {
64
+ palette: {
65
+ customColors: ['#111111', '#222222'],
66
+ colorAssignmentMode: 'by-value',
67
+ colorAssignments: [{ key: 'series_b', color: '#00bb00' }]
68
+ }
69
+ } as any
70
+ })
71
+
72
+ const tileScale = createTileColorScale({ mode: 'by-series', seriesKey: 'series_b' }, config, colorScale, 1, 2)
73
+
74
+ expect(tileScale('Series A')).toBe('#aa0000')
75
+ expect(tileScale('Series B')).toBe('#00bb00')
76
+ })
77
+
78
+ it('ignores stale by-value assignments when deciding whether to override same color mode', () => {
79
+ const colorScale = scaleOrdinal({
80
+ domain: ['Series A', 'Series B'],
81
+ range: ['#aa0000', '#00bb00'],
82
+ unknown: null
83
+ })
84
+ const config = buildConfig({
85
+ general: {
86
+ palette: {
87
+ customColors: ['#111111', '#222222'],
88
+ colorAssignmentMode: 'by-value',
89
+ colorAssignments: [{ key: 'removed_series', color: '#00bb00' }]
90
+ }
91
+ } as any
92
+ })
93
+
94
+ const tileScale = createTileColorScale({ mode: 'by-series', seriesKey: 'series_b' }, config, colorScale, 1, 2)
95
+
96
+ expect(tileScale('Series A')).toBe('#aa0000')
97
+ expect(tileScale('Series B')).toBe('#aa0000')
98
+ })
99
+ })
@@ -0,0 +1,268 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { describe, expect, it } from 'vitest'
3
+ import useRightAxis from '../useRightAxis'
4
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
5
+
6
+ const leftSeries = { dataKey: 'Cases', type: 'Bar', axis: 'Left', tooltip: true }
7
+ const rightSeries = { dataKey: 'Rate', type: 'Line', axis: 'Right', tooltip: true }
8
+
9
+ const createComboConfig = () =>
10
+ createMockConfig({
11
+ visualizationType: 'Combo',
12
+ orientation: 'vertical',
13
+ series: [leftSeries, rightSeries],
14
+ yAxis: {
15
+ ...createMockConfig().yAxis,
16
+ rightMax: undefined,
17
+ rightMin: undefined,
18
+ smallestRightAxisMax: undefined
19
+ },
20
+ runtime: {
21
+ ...createMockConfig().runtime,
22
+ series: [leftSeries, rightSeries],
23
+ seriesKeys: ['Cases', 'Rate'],
24
+ barSeriesKeys: ['Cases'],
25
+ lineSeriesKeys: ['Rate']
26
+ } as any
27
+ })
28
+
29
+ describe('useRightAxis', () => {
30
+ it('does not render a right axis for vertical Combo charts with only left-axis series', () => {
31
+ const config = createMockConfig({
32
+ visualizationType: 'Combo',
33
+ orientation: 'vertical',
34
+ series: [leftSeries],
35
+ yAxis: {
36
+ ...createMockConfig().yAxis,
37
+ rightHideAxis: true
38
+ },
39
+ runtime: {
40
+ ...createMockConfig().runtime,
41
+ series: [leftSeries],
42
+ seriesKeys: ['Cases'],
43
+ barSeriesKeys: ['Cases'],
44
+ lineSeriesKeys: []
45
+ } as any
46
+ })
47
+
48
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5 }] }))
49
+
50
+ expect(result.result.current.hasRightAxis).toBe(false)
51
+ })
52
+
53
+ it('builds the right-axis scale from the supplied domain rows', () => {
54
+ const config = createComboConfig()
55
+ const renderedRows = [{ Cases: 5, Rate: 12 }]
56
+ const stableDomainRows = [
57
+ { Cases: 5, Rate: 12 },
58
+ { Cases: 10, Rate: 90 }
59
+ ]
60
+
61
+ const renderedResult = renderHook(() => useRightAxis({ config, yMax: 100, data: renderedRows }))
62
+ const stableResult = renderHook(() => useRightAxis({ config, yMax: 100, data: stableDomainRows }))
63
+
64
+ expect(renderedResult.result.current.yScaleRight.domain()).toEqual([0, 12])
65
+ expect(stableResult.result.current.yScaleRight.domain()).toEqual([0, 90])
66
+ })
67
+
68
+ it('uses clean-top-tick for the automatic right-axis max when shared auto max strategy is enabled', () => {
69
+ const config = {
70
+ ...createComboConfig(),
71
+ yAxis: {
72
+ ...createComboConfig().yAxis,
73
+ autoMaxStrategy: 'clean-top-tick' as const
74
+ }
75
+ }
76
+
77
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 101 }] }))
78
+
79
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 120])
80
+ expect(result.result.current.rightTickValues).toBeUndefined()
81
+ })
82
+
83
+ it('auto-starts positive right-axis data at zero without depending on series-type runtime keys', () => {
84
+ const config = {
85
+ ...createComboConfig(),
86
+ runtime: {
87
+ ...createComboConfig().runtime,
88
+ barSeriesKeys: [],
89
+ lineSeriesKeys: []
90
+ }
91
+ }
92
+
93
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 25 }] }))
94
+
95
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 25])
96
+ })
97
+
98
+ it('does not let left-axis negative data affect the right-axis minimum', () => {
99
+ const config = {
100
+ ...createComboConfig(),
101
+ yAxis: {
102
+ ...createComboConfig().yAxis,
103
+ autoMaxStrategy: 'clean-top-tick' as const
104
+ }
105
+ }
106
+
107
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: -100, Rate: 6 }] }))
108
+
109
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 6])
110
+ expect(result.result.current.rightTickValues).toBeUndefined()
111
+ })
112
+
113
+ it('honors a positive rightMin below the right-axis data minimum', () => {
114
+ const config = {
115
+ ...createComboConfig(),
116
+ yAxis: {
117
+ ...createComboConfig().yAxis,
118
+ rightMin: '90',
119
+ rightMax: '100'
120
+ }
121
+ }
122
+
123
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 95 }] }))
124
+
125
+ expect(result.result.current.yScaleRight.domain()).toEqual([90, 100])
126
+ })
127
+
128
+ it('uses formatted numeric strings when calculating the right-axis domain', () => {
129
+ const config = createComboConfig()
130
+
131
+ const result = renderHook(() =>
132
+ useRightAxis({
133
+ config,
134
+ yMax: 100,
135
+ data: [
136
+ { Cases: 5, Rate: '$1,000' },
137
+ { Cases: 6, Rate: '2,500' }
138
+ ]
139
+ })
140
+ )
141
+
142
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 2500])
143
+ })
144
+
145
+ it('ignores a rightMin above the right-axis data minimum and defaults positive domains to zero', () => {
146
+ const config = {
147
+ ...createComboConfig(),
148
+ yAxis: {
149
+ ...createComboConfig().yAxis,
150
+ rightMin: '98'
151
+ }
152
+ }
153
+
154
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 95 }] }))
155
+
156
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 95])
157
+ })
158
+
159
+ it('uses a negative minimum when right-axis data is negative', () => {
160
+ const config = createComboConfig()
161
+
162
+ const result = renderHook(() =>
163
+ useRightAxis({
164
+ config,
165
+ yMax: 100,
166
+ data: [
167
+ { Cases: 5, Rate: -12 },
168
+ { Cases: 6, Rate: -3 }
169
+ ]
170
+ })
171
+ )
172
+
173
+ expect(result.result.current.yScaleRight.domain()).toEqual([-12, -3])
174
+ })
175
+
176
+ it('ignores values that are empty after right-axis numeric cleanup', () => {
177
+ const config = createComboConfig()
178
+
179
+ const result = renderHook(() =>
180
+ useRightAxis({
181
+ config,
182
+ yMax: 100,
183
+ data: [
184
+ { Cases: 5, Rate: '$' },
185
+ { Cases: 6, Rate: '-12' }
186
+ ]
187
+ })
188
+ )
189
+
190
+ expect(result.result.current.yScaleRight.domain()).toEqual([-12, -12])
191
+ })
192
+
193
+ it('honors a negative rightMin below the right-axis data minimum', () => {
194
+ const config = {
195
+ ...createComboConfig(),
196
+ yAxis: {
197
+ ...createComboConfig().yAxis,
198
+ rightMin: '-20'
199
+ }
200
+ }
201
+
202
+ const result = renderHook(() =>
203
+ useRightAxis({
204
+ config,
205
+ yMax: 100,
206
+ data: [
207
+ { Cases: 5, Rate: -12 },
208
+ { Cases: 6, Rate: -3 }
209
+ ]
210
+ })
211
+ )
212
+
213
+ expect(result.result.current.yScaleRight.domain()).toEqual([-20, -3])
214
+ })
215
+
216
+ it('does not round the right-axis max when rightMax is explicit', () => {
217
+ const config = {
218
+ ...createComboConfig(),
219
+ yAxis: {
220
+ ...createComboConfig().yAxis,
221
+ autoMaxStrategy: 'clean-top-tick' as const,
222
+ rightMax: '100'
223
+ }
224
+ }
225
+
226
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 25 }] }))
227
+
228
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 100])
229
+ expect(result.result.current.rightTickValues).toBeUndefined()
230
+ })
231
+
232
+ it('uses clean-top-tick when rightMax is invalid', () => {
233
+ const createConfigWithRightMax = (rightMax: string) => ({
234
+ ...createComboConfig(),
235
+ yAxis: {
236
+ ...createComboConfig().yAxis,
237
+ autoMaxStrategy: 'clean-top-tick' as const,
238
+ rightMax
239
+ }
240
+ })
241
+
242
+ const lowerThanData = renderHook(() =>
243
+ useRightAxis({ config: createConfigWithRightMax('50'), yMax: 100, data: [{ Cases: 5, Rate: 101 }] })
244
+ )
245
+ const nonNumeric = renderHook(() =>
246
+ useRightAxis({ config: createConfigWithRightMax('not-a-number'), yMax: 100, data: [{ Cases: 5, Rate: 101 }] })
247
+ )
248
+
249
+ expect(lowerThanData.result.current.yScaleRight.domain()).toEqual([0, 120])
250
+ expect(lowerThanData.result.current.rightTickValues).toBeUndefined()
251
+ expect(nonNumeric.result.current.yScaleRight.domain()).toEqual([0, 120])
252
+ expect(nonNumeric.result.current.rightTickValues).toBeUndefined()
253
+ })
254
+
255
+ it('ignores a non-numeric rightMin value', () => {
256
+ const config = {
257
+ ...createComboConfig(),
258
+ yAxis: {
259
+ ...createComboConfig().yAxis,
260
+ rightMin: 'not-a-number'
261
+ }
262
+ }
263
+
264
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 25 }] }))
265
+
266
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 25])
267
+ })
268
+ })