@cdc/chart 4.26.6 → 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.
- package/CONFIG.md +9 -7
- package/dist/cdcchart.js +32200 -31292
- package/examples/private/flueview.json +22051 -0
- package/package.json +3 -11
- package/src/CdcChart.tsx +2 -2
- package/src/CdcChartComponent.tsx +40 -10
- package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
- package/src/_stories/Chart.smoke.stories.tsx +14 -1
- package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
- package/src/components/BarChart/helpers/getBarData.ts +28 -28
- package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
- package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
- package/src/components/BarChart/helpers/useBarChart.ts +4 -5
- package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
- package/src/components/BoxPlot/helpers/index.ts +4 -3
- package/src/components/Brush/BrushSelector.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +90 -29
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +32 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +5 -1
- package/src/components/Forecasting/Forecasting.tsx +1 -1
- package/src/components/Legend/Legend.Component.tsx +54 -59
- package/src/components/Legend/Legend.Suppression.tsx +3 -3
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
- package/src/components/LineChart/helpers.ts +0 -2
- package/src/components/LineChart/index.tsx +2 -3
- package/src/components/LinearChart.tsx +4 -3
- package/src/components/PieChart/PieChart.tsx +117 -10
- package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
- package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -10
- package/src/data/initial-state.js +2 -0
- package/src/helpers/categoryOrder.ts +60 -0
- package/src/helpers/getBoxPlotConfig.ts +19 -12
- package/src/helpers/getComboChartConfig.ts +0 -1
- package/src/helpers/getExcludedData.ts +0 -1
- package/src/helpers/getHasBoundarySuppression.ts +4 -3
- package/src/helpers/getMinMax.ts +2 -1
- package/src/helpers/getNewRuntime.ts +0 -2
- package/src/helpers/getPiePercent.ts +3 -3
- package/src/helpers/isConvertLineToBarGraph.ts +2 -2
- package/src/helpers/seriesColumnSettings.ts +21 -3
- package/src/helpers/sizeHelpers.ts +0 -2
- package/src/helpers/tests/categoryOrder.test.ts +57 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +60 -0
- package/src/hooks/tests/useRightAxis.test.tsx +143 -2
- package/src/hooks/tests/useScales.test.tsx +3 -6
- package/src/hooks/tests/useTooltip.test.tsx +132 -0
- package/src/hooks/useRightAxis.ts +19 -26
- package/src/hooks/useTooltip.tsx +14 -10
- package/src/store/chart.reducer.ts +0 -1
- package/src/test/CdcChart.dataTable.test.tsx +94 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import toNumber from 'lodash/toNumber'
|
|
2
2
|
|
|
3
3
|
export const getPiePercent = (data: Record<string, any>[], seriesKey: string): Record<string, any>[] => {
|
|
4
4
|
// getonly the numeric values for each seriesKey
|
|
5
|
-
const numericValues = data.map(row =>
|
|
5
|
+
const numericValues = data.map(row => toNumber(row[seriesKey])).filter(v => !Number.isNaN(v))
|
|
6
6
|
|
|
7
7
|
const total = numericValues.reduce((sum, v) => sum + v, 0)
|
|
8
8
|
|
|
9
9
|
return data.map(row => {
|
|
10
|
-
const raw =
|
|
10
|
+
const raw = toNumber(row[seriesKey])
|
|
11
11
|
if (Number.isNaN(raw)) {
|
|
12
12
|
// skip non-numeric / undefined
|
|
13
13
|
return row
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import uniq from 'lodash/uniq'
|
|
2
2
|
|
|
3
3
|
export const isConvertLineToBarGraph = (configObj, filteredData) => {
|
|
4
4
|
const { allowLineToBarGraph, series, visualizationType, xAxis } = configObj
|
|
@@ -6,6 +6,6 @@ export const isConvertLineToBarGraph = (configObj, filteredData) => {
|
|
|
6
6
|
const lineWithLessThanThreePoints = visualizationType === 'Line' && filteredData?.length < 3
|
|
7
7
|
const isDynamicSeries = series?.some(series => series.dynamicCategory)
|
|
8
8
|
const isDynamicWithLessThanThreePoints =
|
|
9
|
-
isDynamicSeries &&
|
|
9
|
+
isDynamicSeries && uniq(filteredData?.map(data => data[xAxis.dataKey])).length <= 2
|
|
10
10
|
return lineWithLessThanThreePoints || isDynamicWithLessThanThreePoints
|
|
11
11
|
}
|
|
@@ -12,13 +12,22 @@ type ColumnFormattingParams = {
|
|
|
12
12
|
|
|
13
13
|
const hasOwn = (object: object, key: keyof Column) => Object.prototype.hasOwnProperty.call(object, key)
|
|
14
14
|
const isNonEmptyString = (value: unknown) => typeof value === 'string' && value !== ''
|
|
15
|
+
const getNumericRoundToPlace = (value: Column['roundToPlace']): number | undefined => {
|
|
16
|
+
if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const numericValue = Number(value)
|
|
21
|
+
if (!Number.isFinite(numericValue)) return undefined
|
|
22
|
+
|
|
23
|
+
return Math.max(0, Math.min(20, Math.round(numericValue)))
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
export const createDefaultSeriesColumnConfig = (columnName: string): Column => ({
|
|
17
27
|
name: columnName,
|
|
18
28
|
label: columnName,
|
|
19
29
|
prefix: '',
|
|
20
30
|
suffix: '',
|
|
21
|
-
roundToPlace: 0,
|
|
22
31
|
commas: false,
|
|
23
32
|
dataTable: true,
|
|
24
33
|
order: undefined,
|
|
@@ -70,12 +79,18 @@ export const upsertSeriesColumnConfig = (
|
|
|
70
79
|
): ChartColumns => {
|
|
71
80
|
const existingEntry = findColumnConfigByName(columns, seriesKey)
|
|
72
81
|
const columnKey = existingEntry?.columnKey || seriesKey
|
|
73
|
-
const nextColumnConfig = {
|
|
82
|
+
const nextColumnConfig: Partial<Column> = {
|
|
74
83
|
...(existingEntry?.columnConfig || {}),
|
|
75
84
|
...updates,
|
|
76
85
|
name: seriesKey
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
delete nextColumnConfig[key as keyof Column]
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
79
94
|
if (
|
|
80
95
|
nextColumnConfig.label === undefined &&
|
|
81
96
|
!hasOwn(existingEntry?.columnConfig || {}, 'label') &&
|
|
@@ -104,7 +119,10 @@ export const getSeriesColumnFormattingParams = (columnConfig?: Partial<Column>):
|
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
if (hasOwn(columnConfig, 'roundToPlace')) {
|
|
107
|
-
|
|
122
|
+
const numericRoundToPlace = getNumericRoundToPlace(columnConfig.roundToPlace)
|
|
123
|
+
if (numericRoundToPlace !== undefined) {
|
|
124
|
+
formattingParams.addColRoundTo = numericRoundToPlace
|
|
125
|
+
}
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
if (hasOwn(columnConfig, 'commas')) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getOrderedCategoryValues, isCustomCategoryOrderActive, sortByCategoryOrder } from '../categoryOrder'
|
|
3
|
+
|
|
4
|
+
describe('categoryOrder helpers', () => {
|
|
5
|
+
const data = [
|
|
6
|
+
{ category: 'A', value: 1 },
|
|
7
|
+
{ category: 'D', value: 4 },
|
|
8
|
+
{ category: 'C', value: 3 },
|
|
9
|
+
{ category: 'D', value: 5 },
|
|
10
|
+
{ category: 'L', value: 12 }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
it('detects custom category order only for categorical axes with configured values', () => {
|
|
14
|
+
expect(
|
|
15
|
+
isCustomCategoryOrderActive({
|
|
16
|
+
xAxis: { type: 'categorical', dataKey: 'category', categoryOrderType: 'custom', categoryOrder: ['C'] } as any
|
|
17
|
+
})
|
|
18
|
+
).toBe(true)
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
isCustomCategoryOrderActive({
|
|
22
|
+
xAxis: { type: 'date', dataKey: 'category', categoryOrderType: 'custom', categoryOrder: ['C'] } as any
|
|
23
|
+
})
|
|
24
|
+
).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('sorts rows by configured category order and keeps duplicate rows stable', () => {
|
|
28
|
+
const sorted = sortByCategoryOrder(data, {
|
|
29
|
+
xAxis: {
|
|
30
|
+
type: 'categorical',
|
|
31
|
+
dataKey: 'category',
|
|
32
|
+
categoryOrderType: 'custom',
|
|
33
|
+
categoryOrder: ['A', 'C', 'D', 'L']
|
|
34
|
+
} as any
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(sorted.map(row => row.category)).toEqual(['A', 'C', 'D', 'D', 'L'])
|
|
38
|
+
expect(sorted.map(row => row.value)).toEqual([1, 3, 4, 5, 12])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('appends values missing from custom order in source order', () => {
|
|
42
|
+
const sorted = sortByCategoryOrder(data, {
|
|
43
|
+
xAxis: {
|
|
44
|
+
type: 'categorical',
|
|
45
|
+
dataKey: 'category',
|
|
46
|
+
categoryOrderType: 'custom',
|
|
47
|
+
categoryOrder: ['C']
|
|
48
|
+
} as any
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect(sorted.map(row => row.category)).toEqual(['C', 'A', 'D', 'D', 'L'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('merges configured category order with newly discovered values', () => {
|
|
55
|
+
expect(getOrderedCategoryValues(data, 'category', ['C', 'A'])).toEqual(['C', 'A', 'D', 'L'])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -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,6 +79,15 @@ 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({
|
|
@@ -92,6 +118,40 @@ describe('seriesColumnSettings', () => {
|
|
|
92
118
|
})
|
|
93
119
|
})
|
|
94
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
|
+
|
|
95
155
|
it('returns undefined when no formatting overrides were explicitly configured', () => {
|
|
96
156
|
expect(getSeriesColumnFormattingParams({ label: 'Cases' })).toBeUndefined()
|
|
97
157
|
})
|
|
@@ -27,6 +27,29 @@ const createComboConfig = () =>
|
|
|
27
27
|
})
|
|
28
28
|
|
|
29
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
|
+
|
|
30
53
|
it('builds the right-axis scale from the supplied domain rows', () => {
|
|
31
54
|
const config = createComboConfig()
|
|
32
55
|
const renderedRows = [{ Cases: 5, Rate: 12 }]
|
|
@@ -57,7 +80,22 @@ describe('useRightAxis', () => {
|
|
|
57
80
|
expect(result.result.current.rightTickValues).toBeUndefined()
|
|
58
81
|
})
|
|
59
82
|
|
|
60
|
-
it('
|
|
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', () => {
|
|
61
99
|
const config = {
|
|
62
100
|
...createComboConfig(),
|
|
63
101
|
yAxis: {
|
|
@@ -68,10 +106,113 @@ describe('useRightAxis', () => {
|
|
|
68
106
|
|
|
69
107
|
const result = renderHook(() => useRightAxis({ config, yMax: 100, data: [{ Cases: -100, Rate: 6 }] }))
|
|
70
108
|
|
|
71
|
-
expect(result.result.current.yScaleRight.domain()).toEqual([
|
|
109
|
+
expect(result.result.current.yScaleRight.domain()).toEqual([0, 6])
|
|
72
110
|
expect(result.result.current.rightTickValues).toBeUndefined()
|
|
73
111
|
})
|
|
74
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
|
+
|
|
75
216
|
it('does not round the right-axis max when rightMax is explicit', () => {
|
|
76
217
|
const config = {
|
|
77
218
|
...createComboConfig(),
|
|
@@ -106,12 +106,9 @@ describe('useScales', () => {
|
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
renderHook(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
wrapper: createWrapper()
|
|
113
|
-
}
|
|
114
|
-
)
|
|
109
|
+
renderHook(() => useScales({ ...baseProps, config: configWithManualPadding, hasSpacedInlineLabel: true }), {
|
|
110
|
+
wrapper: createWrapper()
|
|
111
|
+
})
|
|
115
112
|
|
|
116
113
|
expect(mockGetMinMax.mock.calls[0][0].config.yAxis).toMatchObject({
|
|
117
114
|
enablePadding: false,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { act, renderHook } from '@testing-library/react'
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import ConfigContext from '../../ConfigContext'
|
|
5
|
+
import { createMockChartContext, createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
|
|
6
|
+
import { useTooltip } from '../useTooltip'
|
|
7
|
+
|
|
8
|
+
vi.mock('@visx/event', () => ({
|
|
9
|
+
localPoint: () => ({ x: 5, y: 5 })
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
describe('useTooltip', () => {
|
|
13
|
+
const showTooltip = vi.fn()
|
|
14
|
+
const hideTooltip = vi.fn()
|
|
15
|
+
const row = {
|
|
16
|
+
'Cause of death': 'Hypertensive disorders of pregnancy',
|
|
17
|
+
Percentage: 22,
|
|
18
|
+
Count: 146
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const baseConfig = createMockConfig()
|
|
22
|
+
const config = createMockConfig({
|
|
23
|
+
visualizationType: 'Bar',
|
|
24
|
+
orientation: 'vertical',
|
|
25
|
+
xAxis: {
|
|
26
|
+
...baseConfig.xAxis,
|
|
27
|
+
type: 'categorical',
|
|
28
|
+
dataKey: 'Cause of death'
|
|
29
|
+
},
|
|
30
|
+
yAxis: {
|
|
31
|
+
...baseConfig.yAxis,
|
|
32
|
+
inlineLabel: ''
|
|
33
|
+
},
|
|
34
|
+
general: {
|
|
35
|
+
showMissingDataLabel: true,
|
|
36
|
+
hideNullValue: true
|
|
37
|
+
} as any,
|
|
38
|
+
series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }] as any,
|
|
39
|
+
columns: {
|
|
40
|
+
Count: {
|
|
41
|
+
name: 'Count',
|
|
42
|
+
label: 'Count',
|
|
43
|
+
prefix: '',
|
|
44
|
+
suffix: '',
|
|
45
|
+
roundToPlace: 0,
|
|
46
|
+
commas: true,
|
|
47
|
+
dataTable: true,
|
|
48
|
+
tooltips: true
|
|
49
|
+
}
|
|
50
|
+
} as any,
|
|
51
|
+
dataFormat: {
|
|
52
|
+
abbreviated: false,
|
|
53
|
+
bottomAbbreviated: false,
|
|
54
|
+
bottomPrefix: '',
|
|
55
|
+
bottomRoundTo: 0,
|
|
56
|
+
bottomSuffix: '',
|
|
57
|
+
bottomCommas: false,
|
|
58
|
+
commas: false,
|
|
59
|
+
prefix: '',
|
|
60
|
+
preserveOriginalDecimals: false,
|
|
61
|
+
rightPrefix: '',
|
|
62
|
+
rightRoundTo: 0,
|
|
63
|
+
rightSuffix: '',
|
|
64
|
+
roundTo: 1,
|
|
65
|
+
suffix: '%'
|
|
66
|
+
} as any,
|
|
67
|
+
runtime: {
|
|
68
|
+
...baseConfig.runtime,
|
|
69
|
+
xAxis: {
|
|
70
|
+
...baseConfig.runtime.xAxis,
|
|
71
|
+
type: 'categorical',
|
|
72
|
+
dataKey: 'Cause of death'
|
|
73
|
+
},
|
|
74
|
+
yAxis: {
|
|
75
|
+
...baseConfig.runtime.yAxis,
|
|
76
|
+
dataKey: 'Percentage'
|
|
77
|
+
},
|
|
78
|
+
series: [{ dataKey: 'Percentage', type: 'Bar', axis: 'Left', tooltip: true }],
|
|
79
|
+
seriesKeys: ['Percentage'],
|
|
80
|
+
seriesLabelsAll: ['Percentage']
|
|
81
|
+
} as any
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const xScale = Object.assign(
|
|
85
|
+
vi.fn(() => 0),
|
|
86
|
+
{
|
|
87
|
+
domain: () => ['Hypertensive disorders of pregnancy'],
|
|
88
|
+
bandwidth: () => 10
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
const yScale = Object.assign(
|
|
92
|
+
vi.fn(() => 0),
|
|
93
|
+
{
|
|
94
|
+
domain: () => ['Hypertensive disorders of pregnancy']
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const wrapper = ({ children }: React.PropsWithChildren) => (
|
|
99
|
+
<ConfigContext.Provider value={createMockChartContext(config, { tableData: [row] })}>
|
|
100
|
+
{children}
|
|
101
|
+
</ConfigContext.Provider>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
showTooltip.mockClear()
|
|
106
|
+
hideTooltip.mockClear()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('does not apply the left-axis suffix to an additional tooltip column', () => {
|
|
110
|
+
const { result } = renderHook(
|
|
111
|
+
() =>
|
|
112
|
+
useTooltip({
|
|
113
|
+
xScale,
|
|
114
|
+
yScale,
|
|
115
|
+
showTooltip,
|
|
116
|
+
hideTooltip,
|
|
117
|
+
yAxisWidth: 0
|
|
118
|
+
}),
|
|
119
|
+
{ wrapper }
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
act(() => result.current.handleTooltipMouseOver({}))
|
|
123
|
+
|
|
124
|
+
expect(showTooltip).toHaveBeenCalledOnce()
|
|
125
|
+
expect(showTooltip.mock.calls[0][0].tooltipData.data).toEqual(
|
|
126
|
+
expect.arrayContaining([
|
|
127
|
+
expect.objectContaining({ key: 'Percentage', value: '22.0%', kind: 'series' }),
|
|
128
|
+
expect.objectContaining({ key: 'Count', value: '146', kind: 'extra' })
|
|
129
|
+
])
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { scaleLinear } from '@visx/scale'
|
|
2
|
-
import useReduceData from './useReduceData'
|
|
3
2
|
import { TOP_PADDING } from './useScales'
|
|
4
3
|
import { getCleanTopTickMax } from '../helpers/getCleanTopTickMax'
|
|
5
4
|
import { getAxisMaxOverride } from '../helpers/getAxisMaxOverride'
|
|
6
5
|
|
|
6
|
+
const cleanRightAxisValue = value => (typeof value === 'string' ? value.replace(/[,$]/g, '').trim() : value)
|
|
7
|
+
|
|
7
8
|
export default function useRightAxis({ config, yMax = 0, data = [] }) {
|
|
8
|
-
const hasRightAxis = config.visualizationType === 'Combo' && config.orientation === 'vertical'
|
|
9
9
|
const rightSeriesKeys =
|
|
10
10
|
config.series && config.series.filter(series => series.axis === 'Right').map(key => key.dataKey)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const allRightAxisData = rightSeriesKeys => {
|
|
14
|
-
if (!rightSeriesKeys) return [0]
|
|
15
|
-
let rightAxisData = []
|
|
16
|
-
rightSeriesKeys.map((key, index) => {
|
|
17
|
-
return (rightAxisData = [...rightAxisData, ...data.map(item => Number(item[key]))])
|
|
18
|
-
})
|
|
19
|
-
return rightAxisData
|
|
20
|
-
}
|
|
11
|
+
const hasRightAxis =
|
|
12
|
+
config.visualizationType === 'Combo' && config.orientation === 'vertical' && Boolean(rightSeriesKeys?.length)
|
|
21
13
|
|
|
22
|
-
const rightAxisValues =
|
|
14
|
+
const rightAxisValues = (rightSeriesKeys ?? [])
|
|
15
|
+
.flatMap(key => data.map(item => item[key]))
|
|
16
|
+
.filter(value => value !== null && value !== undefined && String(value).trim() !== '')
|
|
17
|
+
.map(cleanRightAxisValue)
|
|
18
|
+
.filter(value => String(value).trim() !== '')
|
|
19
|
+
.map(Number)
|
|
20
|
+
.filter(Number.isFinite)
|
|
21
|
+
const rawRightMin = rightAxisValues.length ? Math.min(...rightAxisValues) : 0
|
|
23
22
|
const rawRightMax = rightAxisValues.length ? Math.max(...rightAxisValues) : 0
|
|
24
23
|
let max = rawRightMax
|
|
25
24
|
const { hasValidMax: hasValidExplicitRightMax, maxNumber: rightMaxNumber } = getAxisMaxOverride({
|
|
@@ -28,24 +27,18 @@ export default function useRightAxis({ config, yMax = 0, data = [] }) {
|
|
|
28
27
|
})
|
|
29
28
|
const rightMinRaw = config.yAxis.rightMin
|
|
30
29
|
const rightMinNumber = Number(rightMinRaw)
|
|
31
|
-
const
|
|
32
|
-
rightMinRaw !== undefined &&
|
|
30
|
+
const hasValidExplicitRightMin =
|
|
31
|
+
rightMinRaw !== undefined &&
|
|
32
|
+
rightMinRaw !== null &&
|
|
33
|
+
rightMinRaw !== '' &&
|
|
34
|
+
Number.isFinite(rightMinNumber) &&
|
|
35
|
+
rightMinNumber <= rawRightMin
|
|
33
36
|
|
|
34
37
|
if (hasValidExplicitRightMax && rightMaxNumber > max) {
|
|
35
38
|
max = rightMaxNumber
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
if ((hasBarSeries || hasLineSeries) && minValue > 0) {
|
|
47
|
-
minValue = 0
|
|
48
|
-
}
|
|
41
|
+
const minValue = hasValidExplicitRightMin ? rightMinNumber : rawRightMin > 0 ? 0 : rawRightMin
|
|
49
42
|
|
|
50
43
|
if (config.yAxis.autoMaxStrategy === 'clean-top-tick' && !hasValidExplicitRightMax) {
|
|
51
44
|
max = getCleanTopTickMax(max)
|