@cdc/chart 4.26.5 → 4.26.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONFIG.md +12 -8
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +34093 -32767
- package/examples/feature/heatmap/average-age-categorical.json +1 -1
- package/examples/feature/heatmap/calendar-heatmap.json +1 -1
- package/examples/feature/heatmap/sparse-aggregation.json +1 -1
- package/examples/private/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/package.json +3 -3
- package/src/CdcChart.tsx +5 -4
- package/src/CdcChartComponent.tsx +32 -9
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
- package/src/_stories/_mock/heatmap.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +3 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- package/src/components/EditorPanel/EditorPanel.tsx +367 -136
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +256 -11
- package/src/components/EditorPanel/useEditorPermissions.ts +41 -5
- package/src/components/HeatMap/components/HeatMap.tsx +22 -17
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
- package/src/components/HeatMap/heatmap.css +15 -1
- package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
- package/src/components/Legend/Legend.Component.tsx +2 -2
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/helpers.test.ts +230 -1
- package/src/components/LineChart/helpers.ts +26 -13
- package/src/components/LineChart/index.tsx +21 -4
- package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
- package/src/components/LinearChart.tsx +50 -15
- package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
- package/src/data/initial-state.js +1 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getHasBoundarySuppression.ts +26 -0
- package/src/helpers/getMinMax.ts +24 -22
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/seriesColumnSettings.ts +5 -4
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
- package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +127 -0
- package/src/hooks/tests/useScales.test.tsx +307 -29
- package/src/hooks/useRightAxis.ts +30 -13
- package/src/hooks/useScales.ts +45 -28
- package/src/hooks/useTooltip.tsx +3 -1
- package/src/test/CdcChart.dataTable.test.tsx +122 -0
- package/src/types/ChartConfig.ts +6 -1
- package/src/types/ChartContext.ts +3 -0
- package/src/helpers/getYAxisAutoPadding.ts +0 -55
- package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
- package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
|
@@ -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
|
+
})
|
|
@@ -12,16 +12,10 @@ const mockGetMinMax = vi.fn(() => ({
|
|
|
12
12
|
rightMax: 0
|
|
13
13
|
}))
|
|
14
14
|
|
|
15
|
-
const mockGetYAxisAutoPadding = vi.fn(() => 10)
|
|
16
|
-
|
|
17
15
|
vi.mock('../../helpers/getMinMax', () => ({
|
|
18
16
|
default: (...args: any[]) => mockGetMinMax(...args)
|
|
19
17
|
}))
|
|
20
18
|
|
|
21
|
-
vi.mock('../../helpers/getYAxisAutoPadding', () => ({
|
|
22
|
-
getYAxisAutoPadding: (...args: any[]) => mockGetYAxisAutoPadding(...args)
|
|
23
|
-
}))
|
|
24
|
-
|
|
25
19
|
const config = createMockConfig({
|
|
26
20
|
runtime: {
|
|
27
21
|
...createMockConfig().runtime,
|
|
@@ -56,27 +50,54 @@ const baseProps = {
|
|
|
56
50
|
|
|
57
51
|
describe('useScales', () => {
|
|
58
52
|
beforeEach(() => {
|
|
59
|
-
mockGetMinMax.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
wrapper: createWrapper()
|
|
53
|
+
mockGetMinMax.mockReset()
|
|
54
|
+
mockGetMinMax.mockReturnValue({
|
|
55
|
+
min: 0,
|
|
56
|
+
max: 100,
|
|
57
|
+
leftMax: 100,
|
|
58
|
+
rightMax: 0
|
|
66
59
|
})
|
|
67
|
-
|
|
68
|
-
expect(mockGetYAxisAutoPadding).toHaveBeenCalledTimes(3)
|
|
69
60
|
})
|
|
70
61
|
|
|
71
|
-
it('
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
it('uses spaced inline labels as an effective clean-top-tick trigger', () => {
|
|
63
|
+
mockGetMinMax.mockReturnValue({
|
|
64
|
+
min: 0,
|
|
65
|
+
max: 11.25,
|
|
66
|
+
leftMax: 0,
|
|
67
|
+
rightMax: 0
|
|
74
68
|
})
|
|
75
69
|
|
|
76
|
-
|
|
70
|
+
const result = renderHook(
|
|
71
|
+
() =>
|
|
72
|
+
useScales({
|
|
73
|
+
...baseProps,
|
|
74
|
+
maxValue: 11.25,
|
|
75
|
+
config: createMockConfig({
|
|
76
|
+
yAxis: {
|
|
77
|
+
...createMockConfig().yAxis,
|
|
78
|
+
autoMaxStrategy: 'default',
|
|
79
|
+
inlineLabel: ' percent'
|
|
80
|
+
},
|
|
81
|
+
runtime: {
|
|
82
|
+
...createMockConfig().runtime,
|
|
83
|
+
yAxis: {
|
|
84
|
+
...createMockConfig().runtime.yAxis,
|
|
85
|
+
numTicks: 3
|
|
86
|
+
}
|
|
87
|
+
} as any
|
|
88
|
+
}),
|
|
89
|
+
hasSpacedInlineLabel: true
|
|
90
|
+
}),
|
|
91
|
+
{
|
|
92
|
+
wrapper: createWrapper()
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
expect(result.result.current.max).toBe(15)
|
|
97
|
+
expect(result.result.current.yTickValues).toEqual([0, 5, 10, 15])
|
|
77
98
|
})
|
|
78
99
|
|
|
79
|
-
it('ignores manual y-axis padding while calculating
|
|
100
|
+
it('ignores manual y-axis padding while calculating inline-label headroom', () => {
|
|
80
101
|
const configWithManualPadding = createMockConfig({
|
|
81
102
|
yAxis: {
|
|
82
103
|
...createMockConfig().yAxis,
|
|
@@ -85,21 +106,278 @@ describe('useScales', () => {
|
|
|
85
106
|
}
|
|
86
107
|
})
|
|
87
108
|
|
|
88
|
-
renderHook(
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
renderHook(
|
|
110
|
+
() => useScales({ ...baseProps, config: configWithManualPadding, hasSpacedInlineLabel: true }),
|
|
111
|
+
{
|
|
112
|
+
wrapper: createWrapper()
|
|
113
|
+
}
|
|
114
|
+
)
|
|
91
115
|
|
|
92
116
|
expect(mockGetMinMax.mock.calls[0][0].config.yAxis).toMatchObject({
|
|
93
117
|
enablePadding: false,
|
|
94
118
|
scalePadding: 0
|
|
95
119
|
})
|
|
96
|
-
expect(
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
expect(mockGetMinMax).toHaveBeenCalledTimes(1)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('does not finalize default strategy domains for single-word inline labels', () => {
|
|
124
|
+
mockGetMinMax.mockReturnValue({
|
|
125
|
+
min: 0,
|
|
126
|
+
max: 11.25,
|
|
127
|
+
leftMax: 0,
|
|
128
|
+
rightMax: 0
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const result = renderHook(
|
|
132
|
+
() =>
|
|
133
|
+
useScales({
|
|
134
|
+
...baseProps,
|
|
135
|
+
config: createMockConfig({
|
|
136
|
+
yAxis: {
|
|
137
|
+
...createMockConfig().yAxis,
|
|
138
|
+
autoMaxStrategy: 'default',
|
|
139
|
+
inlineLabel: '%'
|
|
140
|
+
},
|
|
141
|
+
runtime: {
|
|
142
|
+
...createMockConfig().runtime,
|
|
143
|
+
yAxis: {
|
|
144
|
+
...createMockConfig().runtime.yAxis,
|
|
145
|
+
numTicks: 3
|
|
146
|
+
}
|
|
147
|
+
} as any
|
|
148
|
+
}),
|
|
149
|
+
hasSpacedInlineLabel: false
|
|
150
|
+
}),
|
|
151
|
+
{
|
|
152
|
+
wrapper: createWrapper()
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
expect(result.result.current.max).toBe(11.25)
|
|
157
|
+
expect(result.result.current.yTickValues).toBeUndefined()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('keeps shared small-multiple Y domains on the finalized top tick instead of percent padding', () => {
|
|
161
|
+
mockGetMinMax.mockReturnValue({
|
|
162
|
+
min: 0,
|
|
163
|
+
max: 11.25,
|
|
164
|
+
leftMax: 0,
|
|
165
|
+
rightMax: 0
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const result = renderHook(
|
|
169
|
+
() =>
|
|
170
|
+
useScales({
|
|
171
|
+
...baseProps,
|
|
172
|
+
config: createMockConfig({
|
|
173
|
+
smallMultiples: {
|
|
174
|
+
mode: 'by-column',
|
|
175
|
+
tileColumn: 'group',
|
|
176
|
+
independentYAxis: false
|
|
177
|
+
},
|
|
178
|
+
yAxis: {
|
|
179
|
+
...createMockConfig().yAxis,
|
|
180
|
+
autoMaxStrategy: 'default',
|
|
181
|
+
inlineLabel: ' percent'
|
|
182
|
+
},
|
|
183
|
+
runtime: {
|
|
184
|
+
...createMockConfig().runtime,
|
|
185
|
+
yAxis: {
|
|
186
|
+
...createMockConfig().runtime.yAxis,
|
|
187
|
+
numTicks: 3
|
|
188
|
+
}
|
|
189
|
+
} as any
|
|
190
|
+
}),
|
|
191
|
+
maxValue: 11.25,
|
|
192
|
+
hasSpacedInlineLabel: true
|
|
193
|
+
}),
|
|
194
|
+
{
|
|
195
|
+
wrapper: createWrapper()
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
expect(result.result.current.max).toBe(15)
|
|
200
|
+
expect(result.result.current.yTickValues).toEqual([0, 5, 10, 15])
|
|
201
|
+
expect(result.result.current.yScale.ticks(3).at(-1)).toBe(15)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('does not return explicit tick values when the primary Y-axis max is explicit', () => {
|
|
205
|
+
mockGetMinMax.mockReturnValue({
|
|
206
|
+
min: 0,
|
|
207
|
+
max: 20,
|
|
208
|
+
leftMax: 0,
|
|
209
|
+
rightMax: 0
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const result = renderHook(
|
|
213
|
+
() =>
|
|
214
|
+
useScales({
|
|
215
|
+
...baseProps,
|
|
216
|
+
maxValue: 11.25,
|
|
217
|
+
config: createMockConfig({
|
|
218
|
+
yAxis: {
|
|
219
|
+
...createMockConfig().yAxis,
|
|
220
|
+
autoMaxStrategy: 'clean-top-tick',
|
|
221
|
+
inlineLabel: ' percent'
|
|
222
|
+
},
|
|
223
|
+
runtime: {
|
|
224
|
+
...createMockConfig().runtime,
|
|
225
|
+
yAxis: {
|
|
226
|
+
...createMockConfig().runtime.yAxis,
|
|
227
|
+
max: '20',
|
|
228
|
+
numTicks: 3
|
|
229
|
+
}
|
|
230
|
+
} as any
|
|
231
|
+
}),
|
|
232
|
+
hasSpacedInlineLabel: true
|
|
233
|
+
}),
|
|
234
|
+
{
|
|
235
|
+
wrapper: createWrapper()
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
expect(result.result.current.max).toBe(20)
|
|
240
|
+
expect(result.result.current.yTickValues).toBeUndefined()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('does not use spaced inline labels as an effective clean-top-tick trigger for Combo charts', () => {
|
|
244
|
+
mockGetMinMax.mockReturnValue({
|
|
245
|
+
min: 0,
|
|
246
|
+
max: 100,
|
|
247
|
+
leftMax: 11.25,
|
|
248
|
+
rightMax: 0
|
|
99
249
|
})
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
250
|
+
|
|
251
|
+
const result = renderHook(
|
|
252
|
+
() =>
|
|
253
|
+
useScales({
|
|
254
|
+
...baseProps,
|
|
255
|
+
config: createMockConfig({
|
|
256
|
+
visualizationType: 'Combo',
|
|
257
|
+
yAxis: {
|
|
258
|
+
...createMockConfig().yAxis,
|
|
259
|
+
autoMaxStrategy: 'default',
|
|
260
|
+
inlineLabel: ' percent'
|
|
261
|
+
},
|
|
262
|
+
runtime: {
|
|
263
|
+
...createMockConfig().runtime,
|
|
264
|
+
yAxis: {
|
|
265
|
+
...createMockConfig().runtime.yAxis,
|
|
266
|
+
numTicks: 3
|
|
267
|
+
}
|
|
268
|
+
} as any
|
|
269
|
+
}),
|
|
270
|
+
maxValue: 101,
|
|
271
|
+
hasSpacedInlineLabel: true
|
|
272
|
+
}),
|
|
273
|
+
{
|
|
274
|
+
wrapper: createWrapper()
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(result.result.current.leftMax).toBe(11.25)
|
|
279
|
+
expect(result.result.current.yTickValues).toBeUndefined()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('finalizes Combo left-axis ticks when clean-top-tick is explicitly enabled', () => {
|
|
283
|
+
mockGetMinMax.mockReturnValue({
|
|
284
|
+
min: 0,
|
|
285
|
+
max: 100,
|
|
286
|
+
leftMax: 12,
|
|
287
|
+
rightMax: 0
|
|
103
288
|
})
|
|
289
|
+
|
|
290
|
+
const result = renderHook(
|
|
291
|
+
() =>
|
|
292
|
+
useScales({
|
|
293
|
+
...baseProps,
|
|
294
|
+
config: createMockConfig({
|
|
295
|
+
visualizationType: 'Combo',
|
|
296
|
+
yAxis: {
|
|
297
|
+
...createMockConfig().yAxis,
|
|
298
|
+
autoMaxStrategy: 'clean-top-tick'
|
|
299
|
+
},
|
|
300
|
+
runtime: {
|
|
301
|
+
...createMockConfig().runtime,
|
|
302
|
+
yAxis: {
|
|
303
|
+
...createMockConfig().runtime.yAxis,
|
|
304
|
+
numTicks: 3
|
|
305
|
+
}
|
|
306
|
+
} as any
|
|
307
|
+
}),
|
|
308
|
+
maxValue: 101,
|
|
309
|
+
hasSpacedInlineLabel: false
|
|
310
|
+
}),
|
|
311
|
+
{
|
|
312
|
+
wrapper: createWrapper()
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
expect(result.result.current.leftMax).toBe(15)
|
|
317
|
+
expect(result.result.current.yTickValues).toEqual([0, 5, 10, 15])
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('uses the selected Y-axis domain rows for min/max calculation', () => {
|
|
321
|
+
const renderedRows = [{ Date: '2024-01-01', value: 3 }]
|
|
322
|
+
const domainRows = [
|
|
323
|
+
{ Date: '2024-01-01', value: 3 },
|
|
324
|
+
{ Date: '2024-01-02', value: 12 }
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
renderHook(
|
|
328
|
+
() =>
|
|
329
|
+
useScales({
|
|
330
|
+
...baseProps,
|
|
331
|
+
data: renderedRows,
|
|
332
|
+
tableData: domainRows,
|
|
333
|
+
yAxisDomainData: domainRows
|
|
334
|
+
}),
|
|
335
|
+
{
|
|
336
|
+
wrapper: createWrapper()
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
expect(mockGetMinMax.mock.calls[0][0].data).toBe(domainRows)
|
|
341
|
+
expect(mockGetMinMax.mock.calls[0][0].tableData).toBe(domainRows)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('passes hasBoundarySuppression through to getMinMax', () => {
|
|
345
|
+
renderHook(
|
|
346
|
+
() =>
|
|
347
|
+
useScales({
|
|
348
|
+
...baseProps,
|
|
349
|
+
config: createMockConfig({ visualizationType: 'Line' }),
|
|
350
|
+
data: [{ Cases: 10 }],
|
|
351
|
+
tableData: [{ Cases: 10 }],
|
|
352
|
+
hasBoundarySuppression: true
|
|
353
|
+
}),
|
|
354
|
+
{
|
|
355
|
+
wrapper: createWrapper()
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
expect(mockGetMinMax.mock.calls[0][0].hasBoundarySuppression).toBe(true)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('passes hasBoundarySuppression=false when not set', () => {
|
|
363
|
+
const renderedRows = [{ Cases: 50 }]
|
|
364
|
+
const stableDomainRows = [{ Cases: 50 }, { Cases: 60 }]
|
|
365
|
+
|
|
366
|
+
renderHook(
|
|
367
|
+
() =>
|
|
368
|
+
useScales({
|
|
369
|
+
...baseProps,
|
|
370
|
+
config: createMockConfig({ visualizationType: 'Line' }),
|
|
371
|
+
data: renderedRows,
|
|
372
|
+
tableData: renderedRows,
|
|
373
|
+
yAxisDomainData: stableDomainRows
|
|
374
|
+
}),
|
|
375
|
+
{
|
|
376
|
+
wrapper: createWrapper()
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
expect(mockGetMinMax.mock.calls[0][0].data).toBe(stableDomainRows)
|
|
381
|
+
expect(mockGetMinMax.mock.calls[0][0].hasBoundarySuppression).toBe(false)
|
|
104
382
|
})
|
|
105
383
|
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { scaleLinear } from '@visx/scale'
|
|
2
2
|
import useReduceData from './useReduceData'
|
|
3
3
|
import { TOP_PADDING } from './useScales'
|
|
4
|
+
import { getCleanTopTickMax } from '../helpers/getCleanTopTickMax'
|
|
5
|
+
import { getAxisMaxOverride } from '../helpers/getAxisMaxOverride'
|
|
4
6
|
|
|
5
7
|
export default function useRightAxis({ config, yMax = 0, data = [] }) {
|
|
6
8
|
const hasRightAxis = config.visualizationType === 'Combo' && config.orientation === 'vertical'
|
|
@@ -17,14 +19,36 @@ export default function useRightAxis({ config, yMax = 0, data = [] }) {
|
|
|
17
19
|
return rightAxisData
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const rightAxisValues = allRightAxisData(rightSeriesKeys).map(Number).filter(Number.isFinite)
|
|
23
|
+
const rawRightMax = rightAxisValues.length ? Math.max(...rightAxisValues) : 0
|
|
24
|
+
let max = rawRightMax
|
|
25
|
+
const { hasValidMax: hasValidExplicitRightMax, maxNumber: rightMaxNumber } = getAxisMaxOverride({
|
|
26
|
+
value: config.yAxis.rightMax,
|
|
27
|
+
minimumValidMax: max
|
|
28
|
+
})
|
|
29
|
+
const rightMinRaw = config.yAxis.rightMin
|
|
30
|
+
const rightMinNumber = Number(rightMinRaw)
|
|
31
|
+
const hasExplicitRightMin =
|
|
32
|
+
rightMinRaw !== undefined && rightMinRaw !== null && rightMinRaw !== '' && Number.isFinite(rightMinNumber)
|
|
33
|
+
|
|
34
|
+
if (hasValidExplicitRightMax && rightMaxNumber > max) {
|
|
35
|
+
max = rightMaxNumber
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (hasExplicitRightMin && rightMinNumber < minValue) {
|
|
39
|
+
minValue = rightMinNumber
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// if there is a bar series & the right axis doesn't include a negative number, default to zero
|
|
43
|
+
const hasBarSeries = config.runtime?.barSeriesKeys?.length > 0
|
|
44
|
+
const hasLineSeries = config.runtime?.lineSeriesKeys?.length > 0
|
|
21
45
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
46
|
+
if ((hasBarSeries || hasLineSeries) && minValue > 0) {
|
|
47
|
+
minValue = 0
|
|
24
48
|
}
|
|
25
49
|
|
|
26
|
-
if (config.yAxis.
|
|
27
|
-
|
|
50
|
+
if (config.yAxis.autoMaxStrategy === 'clean-top-tick' && !hasValidExplicitRightMax) {
|
|
51
|
+
max = getCleanTopTickMax(max)
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
// Enforce smallest right axis max so small-data charts don't show misleading decimal ticks
|
|
@@ -41,18 +65,11 @@ export default function useRightAxis({ config, yMax = 0, data = [] }) {
|
|
|
41
65
|
if (smallestRightAxisMax !== null && max < smallestRightAxisMax) {
|
|
42
66
|
max = smallestRightAxisMax
|
|
43
67
|
}
|
|
44
|
-
// if there is a bar series & the right axis doesn't include a negative number, default to zero
|
|
45
|
-
const hasBarSeries = config.runtime?.barSeriesKeys?.length > 0
|
|
46
|
-
const hasLineSeries = config.runtime?.lineSeriesKeys?.length > 0
|
|
47
|
-
|
|
48
|
-
if ((hasBarSeries || hasLineSeries) && minValue > 0) {
|
|
49
|
-
minValue = 0
|
|
50
|
-
}
|
|
51
68
|
|
|
52
69
|
const yScaleRight = scaleLinear({
|
|
53
70
|
domain: [minValue, max],
|
|
54
71
|
range: [yMax, TOP_PADDING]
|
|
55
72
|
})
|
|
56
73
|
|
|
57
|
-
return { yScaleRight, hasRightAxis }
|
|
74
|
+
return { yScaleRight, hasRightAxis, rightTickValues: undefined }
|
|
58
75
|
}
|