@cdc/chart 4.26.4 → 4.26.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -1,6 +1,7 @@
1
1
  import { scaleOrdinal } from '@visx/scale'
2
2
  import { getColorScale } from './getColorScale'
3
3
  import { ColorScale } from '../types/ChartContext'
4
+ import { hasSeriesColorAssignmentOverrides } from './colorAssignmentHelpers'
4
5
 
5
6
  /**
6
7
  * Get filtered data for a specific tile based on its mode
@@ -206,6 +207,10 @@ export const getFullColorPalette = (config, numberOfTiles) => {
206
207
  export const createTileColorScale = (tileItem, config, originalColorScale, tileIndex, numberOfTiles) => {
207
208
  const colorMode = config.smallMultiples?.colorMode || 'same'
208
209
 
210
+ if (tileItem.mode === 'by-series' && hasSeriesColorAssignmentOverrides(config)) {
211
+ return originalColorScale
212
+ }
213
+
209
214
  if (colorMode === 'same') {
210
215
  if (tileItem.mode === 'by-series') {
211
216
  // Same mode + by-series: All tiles use the same color
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { AXIS_LABEL_FONT_SIZE, AXIS_LABEL_FONT_SIZE_SMALL, getAxisLabelFontSize } from '../axisLabelFontSize'
3
+
4
+ describe('axisLabelFontSize', () => {
5
+ it('returns the desktop axis label size for non-mobile visualization viewports', () => {
6
+ expect(getAxisLabelFontSize('sm')).toBe(AXIS_LABEL_FONT_SIZE)
7
+ expect(getAxisLabelFontSize('md')).toBe(AXIS_LABEL_FONT_SIZE)
8
+ expect(getAxisLabelFontSize('lg')).toBe(AXIS_LABEL_FONT_SIZE)
9
+ })
10
+
11
+ it('returns the compact axis label size for mobile visualization viewports', () => {
12
+ expect(getAxisLabelFontSize('xxs')).toBe(AXIS_LABEL_FONT_SIZE_SMALL)
13
+ expect(getAxisLabelFontSize('xs')).toBe(AXIS_LABEL_FONT_SIZE_SMALL)
14
+ })
15
+ })
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildBumpChartTooltipHtml } from '../bumpChartTooltip'
3
+
4
+ const buildConfig = overrides => ({
5
+ xAxis: {
6
+ dataKey: 'Timestamp',
7
+ type: 'date-time'
8
+ },
9
+ tooltips: {
10
+ dateDisplayFormat: '%Y'
11
+ },
12
+ columns: {
13
+ Timestamp: {
14
+ name: 'Timestamp',
15
+ label: 'Year',
16
+ tooltips: true
17
+ },
18
+ Frequency: {
19
+ name: 'Frequency',
20
+ label: 'Frequency',
21
+ tooltips: true
22
+ }
23
+ },
24
+ legend: {
25
+ hide: false,
26
+ style: 'circles',
27
+ groupBy: ''
28
+ },
29
+ runtime: {
30
+ xAxis: {
31
+ label: 'Year'
32
+ },
33
+ seriesLabels: {
34
+ Rank: 'Rank'
35
+ },
36
+ seriesLabelsAll: ['Rank', 'Other'],
37
+ seriesKeys: ['Rank', 'Other']
38
+ },
39
+ visualizationType: 'Bump Chart',
40
+ series: [{ dataKey: 'Rank' }, { dataKey: 'Other' }],
41
+ ...overrides
42
+ })
43
+
44
+ const helpers = {
45
+ parseDate: (value: string) => new Date(value),
46
+ formatDate: (date: Date) => String(date.getUTCFullYear()),
47
+ formatTooltipsDate: (date: Date) => `tooltip-${date.getUTCFullYear()}`
48
+ }
49
+
50
+ describe('buildBumpChartTooltipHtml', () => {
51
+ it('uses the shared x-axis formatter in the tooltip heading', () => {
52
+ const html = buildBumpChartTooltipHtml({
53
+ config: buildConfig(),
54
+ colorScale: undefined,
55
+ dataRow: { Timestamp: '2014-01-01T00:00:00.000Z', Rank: '1', Frequency: '149' },
56
+ series: { dataKey: 'Rank' },
57
+ helpers
58
+ })
59
+
60
+ expect(html).toContain('Year: tooltip-2014')
61
+ })
62
+
63
+ it('does not render the raw x-axis value as an extra tooltip row', () => {
64
+ const html = buildBumpChartTooltipHtml({
65
+ config: buildConfig(),
66
+ colorScale: undefined,
67
+ dataRow: { Timestamp: '2014-01-01T00:00:00.000Z', Rank: '1', Frequency: '149' },
68
+ series: { dataKey: 'Rank' },
69
+ helpers
70
+ })
71
+
72
+ expect(html).not.toContain('2014-01-01T00:00:00.000Z')
73
+ expect(html).toContain('Frequency: 149')
74
+ })
75
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
3
+
4
+ vi.mock('@cdc/core/helpers/getTextWidth', () => ({
5
+ getTextWidth: (text: string) => text.length
6
+ }))
7
+
8
+ import { calculateLeftYAxisWidth } from '../calculateLeftYAxisWidth'
9
+
10
+ describe('calculateLeftYAxisWidth', () => {
11
+ it('prefers supplied tick values when measuring labels', () => {
12
+ const handleLeftTickFormatting = vi.fn(tick => String(tick))
13
+ const yScale = {
14
+ ticks: vi.fn(() => [0, 5000, 10000]),
15
+ domain: vi.fn(() => [0, 10000])
16
+ }
17
+
18
+ calculateLeftYAxisWidth({
19
+ config: createMockConfig({
20
+ yAxis: {
21
+ ...createMockConfig().yAxis,
22
+ titlePlacement: 'side'
23
+ }
24
+ }),
25
+ data: [],
26
+ yScale,
27
+ numTicks: 3,
28
+ tickValues: [0, 5, 10],
29
+ parentWidth: 400,
30
+ tickLabelFont: 'normal 16px sans-serif',
31
+ axisLabelFontSize: 16,
32
+ handleLeftTickFormatting
33
+ })
34
+
35
+ expect(yScale.ticks).not.toHaveBeenCalled()
36
+ expect(handleLeftTickFormatting.mock.calls.map(call => call[0])).toEqual([0, 5, 10])
37
+ })
38
+ })
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getSeriesWithData } from '../dataHelpers'
3
+ import { ChartConfig } from '../../types/ChartConfig'
4
+
5
+ type SeriesInput = {
6
+ dataKey: string
7
+ name?: string
8
+ type?: string
9
+ dynamicCategory?: string
10
+ originalDataKey?: string
11
+ }
12
+
13
+ const buildConfig = (
14
+ data: Record<string, unknown>[],
15
+ series: SeriesInput[],
16
+ overrides: Partial<ChartConfig> = {}
17
+ ): ChartConfig =>
18
+ ({
19
+ data,
20
+ filters: [],
21
+ legend: { colorCode: '' },
22
+ runtime: { series },
23
+ ...overrides
24
+ } as unknown as ChartConfig)
25
+
26
+ describe('getSeriesWithData', () => {
27
+ it('includes a series that has at least one numeric non-zero value', () => {
28
+ const config = buildConfig(
29
+ [{ percent_mild: '3.8' }, { percent_mild: '0.0' }],
30
+ [{ dataKey: 'percent_mild', name: 'Mild', type: 'Bar' }]
31
+ )
32
+ expect(getSeriesWithData(config)).toEqual(['Mild'])
33
+ })
34
+
35
+ it('excludes a series whose values are all zero (0 and "0.0")', () => {
36
+ const config = buildConfig(
37
+ [{ percent_fever_notemp: '0.0' }, { percent_fever_notemp: 0 }],
38
+ [{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }]
39
+ )
40
+ expect(getSeriesWithData(config)).toEqual([])
41
+ })
42
+
43
+ it('keeps a series that has a single non-zero value among zeros', () => {
44
+ const config = buildConfig(
45
+ [{ percent_mild: '0.0' }, { percent_mild: '0.0' }, { percent_mild: '1.2' }],
46
+ [{ dataKey: 'percent_mild', name: 'Mild', type: 'Bar' }]
47
+ )
48
+ expect(getSeriesWithData(config)).toEqual(['Mild'])
49
+ })
50
+
51
+ it('treats non-numeric placeholder strings as no data', () => {
52
+ const config = buildConfig(
53
+ [{ percent_fever_notemp: 'Not applicable' }, { percent_fever_notemp: '<1.0' }, { percent_fever_notemp: '' }],
54
+ [{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }]
55
+ )
56
+ expect(getSeriesWithData(config)).toEqual([])
57
+ })
58
+
59
+ it('treats null and undefined values as no data', () => {
60
+ const config = buildConfig(
61
+ [{ percent_fever_notemp: null }, { percent_fever_notemp: undefined }],
62
+ [{ dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }]
63
+ )
64
+ expect(getSeriesWithData(config)).toEqual([])
65
+ })
66
+
67
+ it('counts comma- and dollar-formatted numeric strings as data', () => {
68
+ const config = buildConfig(
69
+ [{ revenue: '$1,000' }, { revenue: '0.0' }],
70
+ [{ dataKey: 'revenue', name: 'Revenue', type: 'Bar' }]
71
+ )
72
+ expect(getSeriesWithData(config)).toEqual(['Revenue'])
73
+ })
74
+
75
+ it('counts negative non-zero values as data', () => {
76
+ const config = buildConfig([{ delta: '-5' }, { delta: '0' }], [{ dataKey: 'delta', name: 'Delta', type: 'Bar' }])
77
+ expect(getSeriesWithData(config)).toEqual(['Delta'])
78
+ })
79
+
80
+ it('applies active filters before evaluating series data', () => {
81
+ const config = buildConfig(
82
+ [
83
+ { reaction: 'Headache', percent_fever_notemp: '0.0', percent_mild: '3.8' },
84
+ { reaction: 'Fever or feverish', percent_fever_notemp: '0.9', percent_mild: '0.0' }
85
+ ],
86
+ [
87
+ { dataKey: 'percent_mild', name: 'Mild', type: 'Bar' },
88
+ { dataKey: 'percent_fever_notemp', name: 'Reported fever', type: 'Bar' }
89
+ ],
90
+ { filters: [{ columnName: 'reaction', active: 'Headache' }] as ChartConfig['filters'] }
91
+ )
92
+ // Under "Headache" the fever column is all zeros, so it drops out of the legend.
93
+ expect(getSeriesWithData(config)).toEqual(['Mild'])
94
+ })
95
+
96
+ it('uses originalDataKey for dynamicCategory series', () => {
97
+ const config = buildConfig(
98
+ [
99
+ { category: 'a', value: '4.2', label: 'a-value' },
100
+ { category: 'b', value: '0.0', label: 'b-value' }
101
+ ],
102
+ [
103
+ {
104
+ dataKey: 'a',
105
+ name: 'A',
106
+ type: 'Bar',
107
+ dynamicCategory: 'category',
108
+ originalDataKey: 'value'
109
+ },
110
+ {
111
+ dataKey: 'b',
112
+ name: 'B',
113
+ type: 'Bar',
114
+ dynamicCategory: 'category',
115
+ originalDataKey: 'value'
116
+ }
117
+ ]
118
+ )
119
+ // Series "a" has a non-zero value; series "b" is zero only.
120
+ expect(getSeriesWithData(config)).toEqual(['A'])
121
+ })
122
+
123
+ it('splits color-coded Bar series by colorCode values', () => {
124
+ const config = buildConfig(
125
+ [
126
+ { group: 'North', amount: '5' },
127
+ { group: 'South', amount: '0.0' }
128
+ ],
129
+ [{ dataKey: 'amount', name: 'Amount', type: 'Bar' }],
130
+ { legend: { colorCode: 'group' } as ChartConfig['legend'] }
131
+ )
132
+ // Only the "North" rows carry a non-zero amount.
133
+ expect(getSeriesWithData(config)).toEqual(['North'])
134
+ })
135
+ })
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ensureSpecialChartAxisTypes } from '../ensureSpecialChartAxisTypes'
3
+
4
+ describe('ensureSpecialChartAxisTypes', () => {
5
+ it('forces bump charts to use a date-time x-axis', () => {
6
+ const config = {
7
+ visualizationType: 'Bump Chart',
8
+ xAxis: {
9
+ type: 'categorical',
10
+ dataKey: 'Timestamp',
11
+ dateParseFormat: '%m/%d/%Y',
12
+ dateDisplayFormat: '%Y'
13
+ }
14
+ }
15
+
16
+ expect(ensureSpecialChartAxisTypes(config as any).xAxis.type).toBe('date-time')
17
+ })
18
+
19
+ it('leaves non-bump chart axis types unchanged', () => {
20
+ const config = {
21
+ visualizationType: 'Line',
22
+ xAxis: {
23
+ type: 'categorical',
24
+ dataKey: 'Category'
25
+ }
26
+ }
27
+
28
+ expect(ensureSpecialChartAxisTypes(config as any).xAxis.type).toBe('categorical')
29
+ })
30
+ })
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getAxisMaxOverride } from '../getAxisMaxOverride'
3
+
4
+ describe('getAxisMaxOverride', () => {
5
+ it('accepts finite entered values that meet the required max', () => {
6
+ expect(getAxisMaxOverride({ value: '25', minimumValidMax: 20 })).toEqual({
7
+ hasEnteredMax: true,
8
+ hasValidMax: true,
9
+ maxNumber: 25
10
+ })
11
+ })
12
+
13
+ it('rejects missing or empty entered values', () => {
14
+ expect(getAxisMaxOverride({ value: undefined, minimumValidMax: 0 }).hasValidMax).toBe(false)
15
+ expect(getAxisMaxOverride({ value: null, minimumValidMax: 0 }).hasValidMax).toBe(false)
16
+ expect(getAxisMaxOverride({ value: '', minimumValidMax: 0 }).hasValidMax).toBe(false)
17
+ })
18
+
19
+ it('rejects non-finite entered values', () => {
20
+ expect(getAxisMaxOverride({ value: 'not-a-number', minimumValidMax: 0 }).hasValidMax).toBe(false)
21
+ expect(getAxisMaxOverride({ value: Infinity, minimumValidMax: 0 }).hasValidMax).toBe(false)
22
+ })
23
+
24
+ it('rejects entered values below the required max', () => {
25
+ expect(getAxisMaxOverride({ value: '19', minimumValidMax: 20 })).toMatchObject({
26
+ hasEnteredMax: true,
27
+ hasValidMax: false,
28
+ maxNumber: 19
29
+ })
30
+ })
31
+
32
+ it('supports the non-positive primary-axis rule by passing zero as the required max', () => {
33
+ expect(getAxisMaxOverride({ value: '0', minimumValidMax: 0 }).hasValidMax).toBe(true)
34
+ expect(getAxisMaxOverride({ value: '-1', minimumValidMax: 0 }).hasValidMax).toBe(false)
35
+ })
36
+ })
@@ -0,0 +1,165 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getCleanTopTickMax, getCleanTopTickMaxCandidates, getFinalTopTickMax } from '../getCleanTopTickMax'
3
+
4
+ describe('getCleanTopTickMax', () => {
5
+ it('rounds automatic max values with the clean-top-tick mantissa ladder', () => {
6
+ expect(getCleanTopTickMaxCandidates()).toEqual([1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10])
7
+
8
+ expect([
9
+ [0.0049, getCleanTopTickMax(0.0049)],
10
+ [0.011, getCleanTopTickMax(0.011)],
11
+ [0.012, getCleanTopTickMax(0.012)],
12
+ [1.1, getCleanTopTickMax(1.1)],
13
+ [1.2, getCleanTopTickMax(1.2)],
14
+ [3, getCleanTopTickMax(3)],
15
+ [5, getCleanTopTickMax(5)],
16
+ [5.1, getCleanTopTickMax(5.1)],
17
+ [7, getCleanTopTickMax(7)],
18
+ [7.2, getCleanTopTickMax(7.2)],
19
+ [8, getCleanTopTickMax(8)],
20
+ [9, getCleanTopTickMax(9)],
21
+ [25, getCleanTopTickMax(25)],
22
+ [89, getCleanTopTickMax(89)],
23
+ [101, getCleanTopTickMax(101)],
24
+ [1434, getCleanTopTickMax(1434)],
25
+ [1470, getCleanTopTickMax(1470)],
26
+ [1490, getCleanTopTickMax(1490)],
27
+ [1675, getCleanTopTickMax(1675)],
28
+ [2340, getCleanTopTickMax(2340)],
29
+ [5678, getCleanTopTickMax(5678)],
30
+ [8999, getCleanTopTickMax(8999)]
31
+ ]).toEqual([
32
+ [0.0049, 0.005],
33
+ [0.011, 0.012],
34
+ [0.012, 0.012],
35
+ [1.1, 1.2],
36
+ [1.2, 1.2],
37
+ [3, 3],
38
+ [5, 5],
39
+ [5.1, 6],
40
+ [7, 8],
41
+ [7.2, 8],
42
+ [8, 8],
43
+ [9, 10],
44
+ [25, 25],
45
+ [89, 100],
46
+ [101, 120],
47
+ [1434, 1500],
48
+ [1470, 1500],
49
+ [1490, 1500],
50
+ [1675, 2000],
51
+ [2340, 2500],
52
+ [5678, 6000],
53
+ [8999, 10000]
54
+ ])
55
+ })
56
+
57
+ it('leaves non-finite values unchanged', () => {
58
+ expect(getCleanTopTickMax(Infinity)).toBe(Infinity)
59
+ expect(Number.isNaN(getCleanTopTickMax(NaN))).toBe(true)
60
+ })
61
+
62
+ it('leaves non-positive values unchanged', () => {
63
+ expect(getCleanTopTickMax(0)).toBe(0)
64
+ expect(getCleanTopTickMax(-1)).toBe(-1)
65
+ })
66
+ })
67
+
68
+ describe('getFinalTopTickMax', () => {
69
+ it('advances clean max values to the next generated tick interval when the top tick is below the max', () => {
70
+ expect(
71
+ getFinalTopTickMax({
72
+ currentMax: 12,
73
+ rawMax: 12,
74
+ ticks: [0, 5, 10],
75
+ shouldUseCleanTopTick: true
76
+ })
77
+ ).toEqual({ max: 15, tickValues: [0, 5, 10, 15] })
78
+
79
+ expect(
80
+ getFinalTopTickMax({
81
+ currentMax: 25,
82
+ rawMax: 25,
83
+ ticks: [0, 10, 20],
84
+ shouldUseCleanTopTick: true
85
+ })
86
+ ).toEqual({ max: 30, tickValues: [0, 10, 20, 30] })
87
+ })
88
+
89
+ it('keeps a clean max when the generated top tick already reaches it', () => {
90
+ expect(
91
+ getFinalTopTickMax({
92
+ currentMax: 12,
93
+ rawMax: 12,
94
+ ticks: [0, 2, 4, 6, 8, 10, 12],
95
+ shouldUseCleanTopTick: true
96
+ })
97
+ ).toEqual({ max: 12, tickValues: [0, 2, 4, 6, 8, 10, 12] })
98
+ })
99
+
100
+ it('does not advance for floating-point dust above the generated top tick', () => {
101
+ expect(
102
+ getFinalTopTickMax({
103
+ currentMax: 0.6000000000000001,
104
+ rawMax: 0.6,
105
+ ticks: [0, 0.2, 0.4, 0.6],
106
+ shouldUseCleanTopTick: true
107
+ })
108
+ ).toEqual({ max: 0.6, tickValues: [0, 0.2, 0.4, 0.6] })
109
+ })
110
+
111
+ it('adds one more interval for spaced inline labels when the raw max is too close to the top tick', () => {
112
+ expect(
113
+ getFinalTopTickMax({
114
+ currentMax: 14.4,
115
+ rawMax: 14.4,
116
+ ticks: [0, 5, 10, 15],
117
+ shouldUseCleanTopTick: true,
118
+ headroomMode: 'inline-label'
119
+ })
120
+ ).toEqual({ max: 20, tickValues: [0, 5, 10, 15, 20] })
121
+
122
+ expect(
123
+ getFinalTopTickMax({
124
+ currentMax: 11.25,
125
+ rawMax: 11.25,
126
+ ticks: [0, 5, 10, 15],
127
+ shouldUseCleanTopTick: true,
128
+ headroomMode: 'inline-label'
129
+ })
130
+ ).toEqual({ max: 15, tickValues: [0, 5, 10, 15] })
131
+ })
132
+
133
+ it('finalizes tiny spaced inline-label domains and includes the top tick', () => {
134
+ expect(
135
+ getFinalTopTickMax({
136
+ currentMax: 0.015,
137
+ rawMax: 0.015,
138
+ ticks: [0, 0.005, 0.01, 0.015],
139
+ shouldUseCleanTopTick: true,
140
+ headroomMode: 'inline-label'
141
+ })
142
+ ).toEqual({ max: 0.02, tickValues: [0, 0.005, 0.01, 0.015, 0.02] })
143
+
144
+ expect(
145
+ getFinalTopTickMax({
146
+ currentMax: 0.019,
147
+ rawMax: 0.019,
148
+ ticks: [0, 0.005, 0.01, 0.015],
149
+ shouldUseCleanTopTick: true,
150
+ headroomMode: 'inline-label'
151
+ })
152
+ ).toEqual({ max: 0.025, tickValues: [0, 0.005, 0.01, 0.015, 0.02, 0.025] })
153
+ })
154
+
155
+ it('does not change the max when effective clean-top-tick is disabled', () => {
156
+ expect(
157
+ getFinalTopTickMax({
158
+ currentMax: 12,
159
+ rawMax: 12,
160
+ ticks: [0, 5, 10],
161
+ shouldUseCleanTopTick: false
162
+ })
163
+ ).toEqual({ max: 12 })
164
+ })
165
+ })