@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.
Files changed (60) hide show
  1. package/CONFIG.md +9 -7
  2. package/dist/cdcchart.js +32200 -31292
  3. package/examples/private/flueview.json +22051 -0
  4. package/package.json +3 -11
  5. package/src/CdcChart.tsx +2 -2
  6. package/src/CdcChartComponent.tsx +40 -10
  7. package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
  8. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  9. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  10. package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
  11. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  12. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
  13. package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
  14. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  15. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  16. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
  17. package/src/components/BarChart/helpers/useBarChart.ts +4 -5
  18. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  19. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  20. package/src/components/BoxPlot/helpers/index.ts +4 -3
  21. package/src/components/Brush/BrushSelector.tsx +1 -1
  22. package/src/components/EditorPanel/EditorPanel.tsx +90 -29
  23. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
  24. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
  25. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
  26. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
  27. package/src/components/EditorPanel/useEditorPermissions.test.tsx +32 -0
  28. package/src/components/EditorPanel/useEditorPermissions.ts +5 -1
  29. package/src/components/Forecasting/Forecasting.tsx +1 -1
  30. package/src/components/Legend/Legend.Component.tsx +54 -59
  31. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  32. package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
  33. package/src/components/LineChart/helpers.ts +0 -2
  34. package/src/components/LineChart/index.tsx +2 -3
  35. package/src/components/LinearChart.tsx +4 -3
  36. package/src/components/PieChart/PieChart.tsx +117 -10
  37. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  38. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  39. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -10
  40. package/src/data/initial-state.js +2 -0
  41. package/src/helpers/categoryOrder.ts +60 -0
  42. package/src/helpers/getBoxPlotConfig.ts +19 -12
  43. package/src/helpers/getComboChartConfig.ts +0 -1
  44. package/src/helpers/getExcludedData.ts +0 -1
  45. package/src/helpers/getHasBoundarySuppression.ts +4 -3
  46. package/src/helpers/getMinMax.ts +2 -1
  47. package/src/helpers/getNewRuntime.ts +0 -2
  48. package/src/helpers/getPiePercent.ts +3 -3
  49. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  50. package/src/helpers/seriesColumnSettings.ts +21 -3
  51. package/src/helpers/sizeHelpers.ts +0 -2
  52. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  53. package/src/helpers/tests/seriesColumnSettings.test.ts +60 -0
  54. package/src/hooks/tests/useRightAxis.test.tsx +143 -2
  55. package/src/hooks/tests/useScales.test.tsx +3 -6
  56. package/src/hooks/tests/useTooltip.test.tsx +132 -0
  57. package/src/hooks/useRightAxis.ts +19 -26
  58. package/src/hooks/useTooltip.tsx +14 -10
  59. package/src/store/chart.reducer.ts +0 -1
  60. package/src/test/CdcChart.dataTable.test.tsx +94 -0
@@ -1,13 +1,13 @@
1
- import _ from 'lodash'
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 => _.toNumber(row[seriesKey])).filter(v => !Number.isNaN(v))
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 = _.toNumber(row[seriesKey])
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 _ from 'lodash'
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 && _.uniq(filteredData?.map(data => data[xAxis.dataKey])).length <= 2
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
- formattingParams.addColRoundTo = columnConfig.roundToPlace ?? 0
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')) {
@@ -1,5 +1,3 @@
1
- import { clamp } from 'lodash'
2
-
3
1
  import { isMobileHeightViewport } from '@cdc/core/helpers/viewports'
4
2
  import { ChartConfig, ViewportSize } from '../types/ChartConfig'
5
3
  import { EDITOR_WIDTH } from '@cdc/core/helpers/constants'
@@ -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('does not finalize right-axis ticks from the inherited mixed Combo min', () => {
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([-100, 6])
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
- () => useScales({ ...baseProps, config: configWithManualPadding, hasSpacedInlineLabel: true }),
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
- let { minValue } = useReduceData(config, data)
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 = allRightAxisData(rightSeriesKeys).map(Number).filter(Number.isFinite)
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 hasExplicitRightMin =
32
- rightMinRaw !== undefined && rightMinRaw !== null && rightMinRaw !== '' && Number.isFinite(rightMinNumber)
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
- 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
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)