@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.
- package/CONFIG.md +20 -14
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +36554 -34320
- package/examples/feature/heatmap/average-age-categorical.json +1 -1
- package/examples/feature/heatmap/calendar-heatmap.json +1 -1
- package/examples/feature/heatmap/sparse-aggregation.json +1 -1
- package/examples/private/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/examples/private/flueview.json +22051 -0
- package/package.json +3 -11
- package/src/CdcChart.tsx +7 -6
- package/src/CdcChartComponent.tsx +71 -18
- package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
- package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/Chart.smoke.stories.tsx +14 -1
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
- package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
- package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
- package/src/_stories/_mock/heatmap.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +3 -0
- package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- package/src/components/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 +457 -165
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
- 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/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +288 -11
- package/src/components/EditorPanel/useEditorPermissions.ts +46 -6
- package/src/components/Forecasting/Forecasting.tsx +1 -1
- package/src/components/HeatMap/components/HeatMap.tsx +22 -17
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
- package/src/components/HeatMap/heatmap.css +15 -1
- package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
- package/src/components/Legend/Legend.Component.tsx +56 -61
- package/src/components/Legend/Legend.Suppression.tsx +3 -3
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/helpers.test.ts +230 -1
- package/src/components/LineChart/helpers.ts +25 -14
- package/src/components/LineChart/index.tsx +23 -7
- package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
- package/src/components/LinearChart.tsx +51 -15
- 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 +5 -13
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
- package/src/data/initial-state.js +3 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
- package/src/helpers/categoryOrder.ts +60 -0
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getBoxPlotConfig.ts +19 -12
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getComboChartConfig.ts +0 -1
- package/src/helpers/getExcludedData.ts +0 -1
- package/src/helpers/getHasBoundarySuppression.ts +27 -0
- package/src/helpers/getMinMax.ts +25 -22
- package/src/helpers/getNewRuntime.ts +0 -2
- package/src/helpers/getPiePercent.ts +3 -3
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/isConvertLineToBarGraph.ts +2 -2
- package/src/helpers/seriesColumnSettings.ts +26 -7
- package/src/helpers/sizeHelpers.ts +0 -2
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/categoryOrder.test.ts +57 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
- package/src/helpers/tests/seriesColumnSettings.test.ts +76 -2
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +268 -0
- package/src/hooks/tests/useScales.test.tsx +302 -27
- package/src/hooks/tests/useTooltip.test.tsx +132 -0
- package/src/hooks/useRightAxis.ts +35 -25
- package/src/hooks/useScales.ts +45 -28
- package/src/hooks/useTooltip.tsx +17 -11
- package/src/store/chart.reducer.ts +0 -1
- package/src/test/CdcChart.dataTable.test.tsx +216 -0
- package/src/types/ChartConfig.ts +6 -1
- package/src/types/ChartContext.ts +3 -0
- package/src/helpers/getYAxisAutoPadding.ts +0 -55
- package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
- package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
|
@@ -11,13 +11,23 @@ type ColumnFormattingParams = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const hasOwn = (object: object, key: keyof Column) => Object.prototype.hasOwnProperty.call(object, key)
|
|
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
|
+
}
|
|
14
25
|
|
|
15
26
|
export const createDefaultSeriesColumnConfig = (columnName: string): Column => ({
|
|
16
27
|
name: columnName,
|
|
17
28
|
label: columnName,
|
|
18
29
|
prefix: '',
|
|
19
30
|
suffix: '',
|
|
20
|
-
roundToPlace: 0,
|
|
21
31
|
commas: false,
|
|
22
32
|
dataTable: true,
|
|
23
33
|
order: undefined,
|
|
@@ -69,12 +79,18 @@ export const upsertSeriesColumnConfig = (
|
|
|
69
79
|
): ChartColumns => {
|
|
70
80
|
const existingEntry = findColumnConfigByName(columns, seriesKey)
|
|
71
81
|
const columnKey = existingEntry?.columnKey || seriesKey
|
|
72
|
-
const nextColumnConfig = {
|
|
82
|
+
const nextColumnConfig: Partial<Column> = {
|
|
73
83
|
...(existingEntry?.columnConfig || {}),
|
|
74
84
|
...updates,
|
|
75
85
|
name: seriesKey
|
|
76
86
|
}
|
|
77
87
|
|
|
88
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
delete nextColumnConfig[key as keyof Column]
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
78
94
|
if (
|
|
79
95
|
nextColumnConfig.label === undefined &&
|
|
80
96
|
!hasOwn(existingEntry?.columnConfig || {}, 'label') &&
|
|
@@ -94,16 +110,19 @@ export const getSeriesColumnFormattingParams = (columnConfig?: Partial<Column>):
|
|
|
94
110
|
|
|
95
111
|
const formattingParams: ColumnFormattingParams = {}
|
|
96
112
|
|
|
97
|
-
if (hasOwn(columnConfig, 'prefix')) {
|
|
98
|
-
formattingParams.addColPrefix = columnConfig.prefix
|
|
113
|
+
if (hasOwn(columnConfig, 'prefix') && isNonEmptyString(columnConfig.prefix)) {
|
|
114
|
+
formattingParams.addColPrefix = columnConfig.prefix
|
|
99
115
|
}
|
|
100
116
|
|
|
101
|
-
if (hasOwn(columnConfig, 'suffix')) {
|
|
102
|
-
formattingParams.addColSuffix = columnConfig.suffix
|
|
117
|
+
if (hasOwn(columnConfig, 'suffix') && isNonEmptyString(columnConfig.suffix)) {
|
|
118
|
+
formattingParams.addColSuffix = columnConfig.suffix
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
if (hasOwn(columnConfig, 'roundToPlace')) {
|
|
106
|
-
|
|
122
|
+
const numericRoundToPlace = getNumericRoundToPlace(columnConfig.roundToPlace)
|
|
123
|
+
if (numericRoundToPlace !== undefined) {
|
|
124
|
+
formattingParams.addColRoundTo = numericRoundToPlace
|
|
125
|
+
}
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
if (hasOwn(columnConfig, 'commas')) {
|
|
@@ -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,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,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
|
+
})
|
|
@@ -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,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
|
+
})
|