@cdc/chart 4.26.5 → 4.26.7

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