@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,71 @@
1
+ import { handleChartAriaLabels } from '../handleChartAriaLabels'
2
+
3
+ describe('handleChartAriaLabels', () => {
4
+ it('returns auto-generated title from visualizationType and title', () => {
5
+ const result = handleChartAriaLabels({
6
+ visualizationType: 'Line',
7
+ title: 'COVID-19 Cases'
8
+ })
9
+ expect(result).toBe('Line chart with the title: COVID-19 Cases')
10
+ })
11
+
12
+ it('returns visualizationType-only title when chart title is absent', () => {
13
+ const result = handleChartAriaLabels({
14
+ visualizationType: 'Bar'
15
+ })
16
+ expect(result).toBe('Bar chart')
17
+ })
18
+
19
+ it('returns fallback when visualizationType is missing', () => {
20
+ const result = handleChartAriaLabels({})
21
+ expect(result).toBe('Data visualization container')
22
+ })
23
+
24
+ describe('description', () => {
25
+ it('concatenates static description after title', () => {
26
+ const result = handleChartAriaLabels({
27
+ visualizationType: 'Line',
28
+ title: 'Trends',
29
+ altText: { type: 'static', value: 'Cases rose steadily over 8 weeks.' }
30
+ })
31
+ expect(result).toBe('Line chart with the title: Trends. Cases rose steadily over 8 weeks.')
32
+ })
33
+
34
+ it('concatenates description from metadata after title', () => {
35
+ const result = handleChartAriaLabels({
36
+ visualizationType: 'Line',
37
+ title: 'Trends',
38
+ altText: { type: 'metadata', metadataKey: 'altDescription' },
39
+ dataMetadata: { altDescription: 'Positivity increased from 4% to 12%.' }
40
+ })
41
+ expect(result).toBe('Line chart with the title: Trends. Positivity increased from 4% to 12%.')
42
+ })
43
+
44
+ it('returns title only when metadata key is missing', () => {
45
+ const result = handleChartAriaLabels({
46
+ visualizationType: 'Line',
47
+ title: 'Trends',
48
+ altText: { type: 'metadata', metadataKey: 'missing' },
49
+ dataMetadata: { other: 'value' }
50
+ })
51
+ expect(result).toBe('Line chart with the title: Trends')
52
+ })
53
+
54
+ it('returns title only when not configured', () => {
55
+ const result = handleChartAriaLabels({
56
+ visualizationType: 'Line',
57
+ title: 'Trends'
58
+ })
59
+ expect(result).toBe('Line chart with the title: Trends')
60
+ })
61
+
62
+ it('returns title only when altText is empty object', () => {
63
+ const result = handleChartAriaLabels({
64
+ visualizationType: 'Line',
65
+ title: 'Trends',
66
+ altText: {}
67
+ })
68
+ expect(result).toBe('Line chart with the title: Trends')
69
+ })
70
+ })
71
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { hasSpacedInlineLabel } from '../hasSpacedInlineLabel'
3
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
+
5
+ describe('hasSpacedInlineLabel', () => {
6
+ it('returns true for multi-word inline labels', () => {
7
+ expect(
8
+ hasSpacedInlineLabel(
9
+ createMockConfig({
10
+ yAxis: {
11
+ ...createMockConfig().yAxis,
12
+ titlePlacement: 'side',
13
+ inlineLabel: 'per 100k'
14
+ }
15
+ })
16
+ )
17
+ ).toBe(true)
18
+ })
19
+
20
+ it('returns true for inline labels with leading space', () => {
21
+ expect(
22
+ hasSpacedInlineLabel(
23
+ createMockConfig({
24
+ yAxis: {
25
+ ...createMockConfig().yAxis,
26
+ titlePlacement: 'side',
27
+ inlineLabel: ' cases'
28
+ }
29
+ })
30
+ )
31
+ ).toBe(true)
32
+ })
33
+
34
+ it('returns false for top y-axis titles with empty inline labels', () => {
35
+ expect(
36
+ hasSpacedInlineLabel(
37
+ createMockConfig({
38
+ yAxis: {
39
+ ...createMockConfig().yAxis,
40
+ titlePlacement: 'top',
41
+ inlineLabel: ''
42
+ }
43
+ })
44
+ )
45
+ ).toBe(false)
46
+ })
47
+
48
+ it('returns false when the inline label has no spaces', () => {
49
+ expect(
50
+ hasSpacedInlineLabel(
51
+ createMockConfig({
52
+ yAxis: {
53
+ ...createMockConfig().yAxis,
54
+ titlePlacement: 'side',
55
+ inlineLabel: 'Rate'
56
+ }
57
+ })
58
+ )
59
+ ).toBe(false)
60
+ })
61
+
62
+ it('returns true for spaced inline labels regardless of title placement', () => {
63
+ expect(
64
+ hasSpacedInlineLabel(
65
+ createMockConfig({
66
+ yAxis: {
67
+ ...createMockConfig().yAxis,
68
+ titlePlacement: 'top',
69
+ inlineLabel: 'per 100k'
70
+ }
71
+ })
72
+ )
73
+ ).toBe(true)
74
+ })
75
+ })
@@ -65,19 +65,33 @@ describe('seriesColumnSettings', () => {
65
65
  it('only returns explicit formatting overrides and preserves falsey values', () => {
66
66
  expect(
67
67
  getSeriesColumnFormattingParams({
68
- prefix: '',
68
+ prefix: '$',
69
69
  suffix: ' units',
70
70
  roundToPlace: 0,
71
71
  commas: false
72
72
  })
73
73
  ).toEqual({
74
- addColPrefix: '',
74
+ addColPrefix: '$',
75
75
  addColSuffix: ' units',
76
76
  addColRoundTo: 0,
77
77
  addColCommas: false
78
78
  })
79
79
  })
80
80
 
81
+ it('ignores blank prefix and suffix overrides so global formatting can apply', () => {
82
+ expect(
83
+ getSeriesColumnFormattingParams({
84
+ prefix: '',
85
+ suffix: '',
86
+ roundToPlace: 0,
87
+ commas: false
88
+ })
89
+ ).toEqual({
90
+ addColRoundTo: 0,
91
+ addColCommas: false
92
+ })
93
+ })
94
+
81
95
  it('returns undefined when no formatting overrides were explicitly configured', () => {
82
96
  expect(getSeriesColumnFormattingParams({ label: 'Cases' })).toBeUndefined()
83
97
  })
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getLegendHighlightKey, shouldResetSeriesHighlight } from '../seriesHighlight'
3
+
4
+ describe('seriesHighlight helpers', () => {
5
+ const seriesLabels = {
6
+ nonfatal_rate_alldrugs: 'All drugs',
7
+ opioid_nonfatal_rate: 'All opioids',
8
+ stimulant_nonfatal_rate: 'All stimulants'
9
+ }
10
+
11
+ it('normalizes a legend label to the stored series highlight key', () => {
12
+ expect(getLegendHighlightKey(seriesLabels, 'All opioids')).toBe('opioid_nonfatal_rate')
13
+ expect(getLegendHighlightKey(seriesLabels, 'unmapped_series')).toBe('unmapped_series')
14
+ })
15
+
16
+ it('accepts legend label objects and legacy raw string callers', () => {
17
+ expect(
18
+ getLegendHighlightKey(seriesLabels, {
19
+ datum: 'All stimulants',
20
+ index: 2,
21
+ text: 'All stimulants',
22
+ value: '#075290'
23
+ })
24
+ ).toBe('stimulant_nonfatal_rate')
25
+
26
+ expect(getLegendHighlightKey(seriesLabels, 'Highlighted bar')).toBe('Highlighted bar')
27
+ })
28
+
29
+ it('does not reset when removing one selected item from a three-series legend', () => {
30
+ expect(
31
+ shouldResetSeriesHighlight(
32
+ ['nonfatal_rate_alldrugs', 'opioid_nonfatal_rate'],
33
+ Object.keys(seriesLabels),
34
+ 'opioid_nonfatal_rate',
35
+ 'Line'
36
+ )
37
+ ).toBe(false)
38
+ })
39
+
40
+ it('resets when selecting the only remaining unselected item', () => {
41
+ expect(
42
+ shouldResetSeriesHighlight(
43
+ ['nonfatal_rate_alldrugs', 'opioid_nonfatal_rate'],
44
+ Object.keys(seriesLabels),
45
+ 'stimulant_nonfatal_rate',
46
+ 'Line'
47
+ )
48
+ ).toBe(true)
49
+ })
50
+
51
+ it('does not reset when the clicked item is not a known series key', () => {
52
+ expect(
53
+ shouldResetSeriesHighlight(
54
+ ['nonfatal_rate_alldrugs', 'opioid_nonfatal_rate'],
55
+ Object.keys(seriesLabels),
56
+ 'suppressed-data-note',
57
+ 'Line'
58
+ )
59
+ ).toBe(false)
60
+ })
61
+ })
@@ -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,127 @@
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('builds the right-axis scale from the supplied domain rows', () => {
31
+ const config = createComboConfig()
32
+ const renderedRows = [{ Cases: 5, Rate: 12 }]
33
+ const stableDomainRows = [
34
+ { Cases: 5, Rate: 12 },
35
+ { Cases: 10, Rate: 90 }
36
+ ]
37
+
38
+ const renderedResult = renderHook(() => useRightAxis({ config, yMax: 100, data: renderedRows }))
39
+ const stableResult = renderHook(() => useRightAxis({ config, yMax: 100, data: stableDomainRows }))
40
+
41
+ expect(renderedResult.result.current.yScaleRight.domain()).toEqual([0, 12])
42
+ expect(stableResult.result.current.yScaleRight.domain()).toEqual([0, 90])
43
+ })
44
+
45
+ it('uses clean-top-tick for the automatic right-axis max when shared auto max strategy is enabled', () => {
46
+ const config = {
47
+ ...createComboConfig(),
48
+ yAxis: {
49
+ ...createComboConfig().yAxis,
50
+ autoMaxStrategy: 'clean-top-tick' as const
51
+ }
52
+ }
53
+
54
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 101 }] }))
55
+
56
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 120])
57
+ expect(result.result.current.rightTickValues).toBeUndefined()
58
+ })
59
+
60
+ it('does not finalize right-axis ticks from the inherited mixed Combo min', () => {
61
+ const config = {
62
+ ...createComboConfig(),
63
+ yAxis: {
64
+ ...createComboConfig().yAxis,
65
+ autoMaxStrategy: 'clean-top-tick' as const
66
+ }
67
+ }
68
+
69
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: -100, Rate: 6 }] }))
70
+
71
+ expect(result.result.current.yScaleRight.domain()).toEqual([-100, 6])
72
+ expect(result.result.current.rightTickValues).toBeUndefined()
73
+ })
74
+
75
+ it('does not round the right-axis max when rightMax is explicit', () => {
76
+ const config = {
77
+ ...createComboConfig(),
78
+ yAxis: {
79
+ ...createComboConfig().yAxis,
80
+ autoMaxStrategy: 'clean-top-tick' as const,
81
+ rightMax: '100'
82
+ }
83
+ }
84
+
85
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 25 }] }))
86
+
87
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 100])
88
+ expect(result.result.current.rightTickValues).toBeUndefined()
89
+ })
90
+
91
+ it('uses clean-top-tick when rightMax is invalid', () => {
92
+ const createConfigWithRightMax = (rightMax: string) => ({
93
+ ...createComboConfig(),
94
+ yAxis: {
95
+ ...createComboConfig().yAxis,
96
+ autoMaxStrategy: 'clean-top-tick' as const,
97
+ rightMax
98
+ }
99
+ })
100
+
101
+ const lowerThanData = renderHook(() =>
102
+ useRightAxis({ config: createConfigWithRightMax('50'), yMax: 100, data: [{ Cases: 5, Rate: 101 }] })
103
+ )
104
+ const nonNumeric = renderHook(() =>
105
+ useRightAxis({ config: createConfigWithRightMax('not-a-number'), yMax: 100, data: [{ Cases: 5, Rate: 101 }] })
106
+ )
107
+
108
+ expect(lowerThanData.result.current.yScaleRight.domain()).toEqual([0, 120])
109
+ expect(lowerThanData.result.current.rightTickValues).toBeUndefined()
110
+ expect(nonNumeric.result.current.yScaleRight.domain()).toEqual([0, 120])
111
+ expect(nonNumeric.result.current.rightTickValues).toBeUndefined()
112
+ })
113
+
114
+ it('ignores a non-numeric rightMin value', () => {
115
+ const config = {
116
+ ...createComboConfig(),
117
+ yAxis: {
118
+ ...createComboConfig().yAxis,
119
+ rightMin: 'not-a-number'
120
+ }
121
+ }
122
+
123
+ const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: 5, Rate: 25 }] }))
124
+
125
+ expect(result.result.current.yScaleRight.domain()).toEqual([0, 25])
126
+ })
127
+ })