@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type ChartConfig, type VisualizationType } from '../types/ChartConfig'
|
|
2
|
+
|
|
3
|
+
export type SeriesColorAssignment = {
|
|
4
|
+
key: string
|
|
5
|
+
color: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type SeriesColorAssignmentItem = {
|
|
9
|
+
key: string
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SERIES_COLOR_ASSIGNMENT_TYPES: VisualizationType[] = ['Line', 'Bar', 'Combo', 'Area Chart', 'Scatter Plot']
|
|
14
|
+
|
|
15
|
+
export const hasDynamicCategorySeries = (config: ChartConfig): boolean =>
|
|
16
|
+
Array.isArray(config.series) && config.series.some(series => Boolean(series?.dynamicCategory))
|
|
17
|
+
|
|
18
|
+
export const isColorByCategoryBarChart = (config: ChartConfig): boolean =>
|
|
19
|
+
config.visualizationType === 'Bar' &&
|
|
20
|
+
config.visualizationSubType === 'regular' &&
|
|
21
|
+
Boolean(config.legend?.colorCode) &&
|
|
22
|
+
(config.series?.length || 0) === 1
|
|
23
|
+
|
|
24
|
+
export const supportsSeriesColorAssignments = (config: ChartConfig): boolean =>
|
|
25
|
+
SERIES_COLOR_ASSIGNMENT_TYPES.includes(config.visualizationType) &&
|
|
26
|
+
!hasDynamicCategorySeries(config) &&
|
|
27
|
+
!isColorByCategoryBarChart(config)
|
|
28
|
+
|
|
29
|
+
export const isSeriesColorAssignmentModeByValue = (config: ChartConfig): boolean =>
|
|
30
|
+
config.general?.palette?.colorAssignmentMode === 'by-value'
|
|
31
|
+
|
|
32
|
+
export const getSeriesColorAssignments = (config: ChartConfig): SeriesColorAssignment[] =>
|
|
33
|
+
Array.isArray(config.general?.palette?.colorAssignments) ? config.general.palette.colorAssignments : []
|
|
34
|
+
|
|
35
|
+
export const hasSeriesColorAssignmentOverrides = (config: ChartConfig): boolean => {
|
|
36
|
+
if (!supportsSeriesColorAssignments(config) || !isSeriesColorAssignmentModeByValue(config)) return false
|
|
37
|
+
|
|
38
|
+
const currentSeriesKeys = new Set(getSeriesColorAssignmentItems(config).map(({ key }) => key))
|
|
39
|
+
|
|
40
|
+
return getSeriesColorAssignments(config).some(assignment => {
|
|
41
|
+
if (!assignment?.key || !assignment?.color) return false
|
|
42
|
+
|
|
43
|
+
return currentSeriesKeys.has(assignment.key)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const getSeriesDisplayLabel = (config: ChartConfig, key: string): string => {
|
|
48
|
+
const series = config.series?.find(item => item?.dataKey === key)
|
|
49
|
+
return String(config.runtime?.seriesLabels?.[key] || series?.name || (series as any)?.label || key)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const getSeriesColorAssignmentItems = (config: ChartConfig): SeriesColorAssignmentItem[] => {
|
|
53
|
+
if (!supportsSeriesColorAssignments(config) || !Array.isArray(config.series)) return []
|
|
54
|
+
|
|
55
|
+
return config.series
|
|
56
|
+
.filter(series => series?.dataKey && !series.dynamicCategory)
|
|
57
|
+
.map(series => ({
|
|
58
|
+
key: series.dataKey,
|
|
59
|
+
label: getSeriesDisplayLabel(config, series.dataKey)
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const getSeriesKeyByColorScaleLabel = (config: ChartConfig): Map<string, string> => {
|
|
64
|
+
const labelToKey = new Map<string, string>()
|
|
65
|
+
const runtimeKeys = Array.isArray(config.runtime?.seriesKeys) ? config.runtime.seriesKeys : []
|
|
66
|
+
|
|
67
|
+
runtimeKeys.forEach(key => {
|
|
68
|
+
labelToKey.set(String(key), key)
|
|
69
|
+
labelToKey.set(getSeriesDisplayLabel(config, key), key)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
getSeriesColorAssignmentItems(config).forEach(({ key, label }) => {
|
|
73
|
+
labelToKey.set(String(key), key)
|
|
74
|
+
labelToKey.set(label, key)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return labelToKey
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const applySeriesColorAssignmentsToRange = (
|
|
81
|
+
config: ChartConfig,
|
|
82
|
+
domain: string[] = [],
|
|
83
|
+
range: string[] = []
|
|
84
|
+
): string[] => {
|
|
85
|
+
if (!supportsSeriesColorAssignments(config) || !isSeriesColorAssignmentModeByValue(config)) return range
|
|
86
|
+
|
|
87
|
+
const assignmentMap = new Map(
|
|
88
|
+
getSeriesColorAssignments(config)
|
|
89
|
+
.filter(assignment => assignment?.key && assignment?.color)
|
|
90
|
+
.map(assignment => [assignment.key, assignment.color])
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (!assignmentMap.size || !range.length) return range
|
|
94
|
+
|
|
95
|
+
const labelToKey = getSeriesKeyByColorScaleLabel(config)
|
|
96
|
+
|
|
97
|
+
return domain.map((label, index) => {
|
|
98
|
+
const key = labelToKey.get(String(label))
|
|
99
|
+
const assignedColor = key ? assignmentMap.get(key) : undefined
|
|
100
|
+
return assignedColor || range[index % range.length]
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { Series } from '@cdc/core/types/Series'
|
|
2
|
+
import isNumber from '@cdc/core/helpers/isNumber'
|
|
2
3
|
import { ChartConfig } from '../types/ChartConfig'
|
|
3
4
|
|
|
5
|
+
// For legend suppression (when `legend.unified` is false), treat a series as having
|
|
6
|
+
// data only if the active (filtered) dataset contains at least one numeric, non-zero
|
|
7
|
+
// value for that series. Placeholder strings (e.g. "Not applicable", "<1.0", ""),
|
|
8
|
+
// nullish values, and zeros are treated as "no visible data" for legend purposes.
|
|
9
|
+
const hasGraphableValue = (raw: unknown): boolean => {
|
|
10
|
+
const sanitized = typeof raw === 'string' ? raw.replace(/[,$]/g, '').trim() : raw
|
|
11
|
+
return isNumber(sanitized) && Number(sanitized) !== 0
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
export const getSeriesWithData = (config: ChartConfig) => {
|
|
5
15
|
const { filters, data, runtime, legend } = config
|
|
6
16
|
const { colorCode } = legend
|
|
@@ -21,7 +31,7 @@ export const getSeriesWithData = (config: ChartConfig) => {
|
|
|
21
31
|
.filter(d => !s.colorCodeSeries || d[colorCode] === s.colorCodeSeries)
|
|
22
32
|
.filter(d => {
|
|
23
33
|
const key = s.dynamicCategory ? s.originalDataKey : s.dataKey
|
|
24
|
-
return d[key]
|
|
34
|
+
return hasGraphableValue(d[key])
|
|
25
35
|
})
|
|
26
36
|
}))
|
|
27
37
|
.filter(s => s.data.length)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type AxisMaxOverrideProps = {
|
|
2
|
+
minimumValidMax: number
|
|
3
|
+
value: unknown
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const getAxisMaxOverride = ({ minimumValidMax, value }: AxisMaxOverrideProps) => {
|
|
7
|
+
const maxNumber = Number(value)
|
|
8
|
+
const hasEnteredMax = value !== undefined && value !== null && value !== ''
|
|
9
|
+
const hasValidMax = hasEnteredMax && Number.isFinite(maxNumber) && maxNumber >= minimumValidMax
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
hasEnteredMax,
|
|
13
|
+
hasValidMax,
|
|
14
|
+
maxNumber
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import capitalize from 'lodash/capitalize'
|
|
2
|
+
import chain from 'lodash/chain'
|
|
3
|
+
import filter from 'lodash/filter'
|
|
4
|
+
import flatMap from 'lodash/flatMap'
|
|
5
|
+
import map from 'lodash/map'
|
|
6
|
+
import min from 'lodash/min'
|
|
7
|
+
import round from 'lodash/round'
|
|
8
|
+
import uniq from 'lodash/uniq'
|
|
2
9
|
import { ChartConfig } from '../types/ChartConfig'
|
|
3
10
|
import * as d3 from 'd3-array'
|
|
4
11
|
|
|
5
12
|
export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
|
|
6
13
|
const combinedData = data
|
|
7
|
-
const groups =
|
|
8
|
-
const seriesKeys =
|
|
14
|
+
const groups = uniq(map(combinedData, newConfig.xAxis.dataKey))
|
|
15
|
+
const seriesKeys = map(newConfig.series, 'dataKey')
|
|
9
16
|
const plots: any[] = []
|
|
10
17
|
|
|
11
18
|
groups.forEach(g => {
|
|
@@ -14,13 +21,13 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
|
|
|
14
21
|
if (!g) throw new Error('No groups resolved in box plots')
|
|
15
22
|
|
|
16
23
|
// Start handle operations on combinedData
|
|
17
|
-
const { count, sortedData } =
|
|
24
|
+
const { count, sortedData } = chain(combinedData)
|
|
18
25
|
// Filter by xAxis data key
|
|
19
26
|
.filter(item => item[newConfig.xAxis.dataKey] === g)
|
|
20
27
|
// perform multiple operations on the filtered data
|
|
21
28
|
.thru(filteredData => ({
|
|
22
29
|
count: filteredData.length,
|
|
23
|
-
sortedData:
|
|
30
|
+
sortedData: map(filteredData, item => Number(item[seriesKey])).sort()
|
|
24
31
|
}))
|
|
25
32
|
// get the results from the chain
|
|
26
33
|
.value()
|
|
@@ -38,19 +45,19 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
|
|
|
38
45
|
plots.push({
|
|
39
46
|
columnCategory: g,
|
|
40
47
|
columnMax: d3.max(nonOutliers),
|
|
41
|
-
columnThirdQuartile:
|
|
48
|
+
columnThirdQuartile: round(q3, newConfig.dataFormat.roundTo),
|
|
42
49
|
columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo),
|
|
43
|
-
columnFirstQuartile:
|
|
44
|
-
columnMin:
|
|
50
|
+
columnFirstQuartile: round(q1, newConfig.dataFormat.roundTo),
|
|
51
|
+
columnMin: min(nonOutliers),
|
|
45
52
|
columnCount: count,
|
|
46
53
|
columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo),
|
|
47
54
|
columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo),
|
|
48
|
-
columnIqr:
|
|
55
|
+
columnIqr: round(iqr, newConfig.dataFormat.roundTo),
|
|
49
56
|
values: sortedData,
|
|
50
57
|
columnLowerBounds: lowerBounds,
|
|
51
58
|
columnUpperBounds: upperBounds,
|
|
52
|
-
columnOutliers:
|
|
53
|
-
columnNonOutliers:
|
|
59
|
+
columnOutliers: filter(sortedData, value => value < lowerBounds || value > upperBounds),
|
|
60
|
+
columnNonOutliers: filter(sortedData, value => value >= lowerBounds && value <= upperBounds)
|
|
54
61
|
})
|
|
55
62
|
} catch (e) {
|
|
56
63
|
console.error('COVE: ', e.message) // eslint-disable-line
|
|
@@ -61,7 +68,7 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
|
|
|
61
68
|
// Generate a flat list of categories based on seriesKeys and groups
|
|
62
69
|
const categories =
|
|
63
70
|
seriesKeys.length > 1
|
|
64
|
-
?
|
|
71
|
+
? flatMap(groups, value => map(seriesKeys, key => `${capitalize(key)} - ${capitalize(value)}`))
|
|
65
72
|
: groups
|
|
66
73
|
|
|
67
74
|
return [plots, categories]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const NICE_MANTISSAS = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] as const
|
|
2
|
+
|
|
3
|
+
type FinalizeTopTickMaxProps = {
|
|
4
|
+
currentMax: number
|
|
5
|
+
rawMax: number
|
|
6
|
+
ticks: number[]
|
|
7
|
+
shouldUseCleanTopTick: boolean
|
|
8
|
+
headroomMode?: 'none' | 'inline-label'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const getTickComparisonEpsilon = (tickGap: number) => Math.abs(tickGap) * 1e-10
|
|
12
|
+
|
|
13
|
+
export const getCleanTopTickMax = (value: number) => {
|
|
14
|
+
if (!Number.isFinite(value) || value <= 0) return value
|
|
15
|
+
|
|
16
|
+
const powerOfTenStep = Math.pow(10, Math.floor(Math.log10(value)))
|
|
17
|
+
const normalizedValue = value / powerOfTenStep
|
|
18
|
+
const mantissa = NICE_MANTISSAS.find(candidate => normalizedValue <= candidate)
|
|
19
|
+
|
|
20
|
+
return (mantissa ?? 10) * powerOfTenStep
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getCleanTopTickMaxCandidates = () => [...NICE_MANTISSAS]
|
|
24
|
+
|
|
25
|
+
export const getFinalTopTickMax = ({
|
|
26
|
+
currentMax,
|
|
27
|
+
rawMax,
|
|
28
|
+
ticks,
|
|
29
|
+
shouldUseCleanTopTick,
|
|
30
|
+
headroomMode = 'none'
|
|
31
|
+
}: FinalizeTopTickMaxProps) => {
|
|
32
|
+
if (!shouldUseCleanTopTick || !Number.isFinite(currentMax)) return { max: currentMax }
|
|
33
|
+
if (!Array.isArray(ticks) || ticks.length === 0) return { max: currentMax }
|
|
34
|
+
|
|
35
|
+
const sortedTicks = Array.from(new Set(ticks.filter(Number.isFinite))).sort((a, b) => a - b)
|
|
36
|
+
if (sortedTicks.length === 0) return { max: currentMax }
|
|
37
|
+
|
|
38
|
+
const tickGap = sortedTicks.slice(1).reduce((smallestGap, tick, index) => {
|
|
39
|
+
const gap = tick - sortedTicks[index]
|
|
40
|
+
return gap > 0 ? Math.min(smallestGap, gap) : smallestGap
|
|
41
|
+
}, Infinity)
|
|
42
|
+
|
|
43
|
+
if (!Number.isFinite(tickGap) || tickGap <= 0) return { max: currentMax }
|
|
44
|
+
|
|
45
|
+
const targetMax = Math.max(currentMax, Number.isFinite(rawMax) ? rawMax : currentMax)
|
|
46
|
+
const tickComparisonEpsilon = getTickComparisonEpsilon(tickGap)
|
|
47
|
+
let finalMax = sortedTicks[sortedTicks.length - 1]
|
|
48
|
+
|
|
49
|
+
while (finalMax + tickComparisonEpsilon < targetMax) {
|
|
50
|
+
finalMax = Number((finalMax + tickGap).toPrecision(15))
|
|
51
|
+
sortedTicks.push(finalMax)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
headroomMode === 'inline-label' &&
|
|
56
|
+
Number.isFinite(rawMax) &&
|
|
57
|
+
finalMax - rawMax <= tickGap * 0.25 + tickComparisonEpsilon
|
|
58
|
+
) {
|
|
59
|
+
finalMax = Number((finalMax + tickGap).toPrecision(15))
|
|
60
|
+
sortedTicks.push(finalMax)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { max: finalMax, tickValues: sortedTicks }
|
|
64
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
divergentColorDistribution,
|
|
11
11
|
colorblindColorDistribution
|
|
12
12
|
} from '@cdc/core/helpers/palettes/colorDistributions'
|
|
13
|
+
import { applySeriesColorAssignmentsToRange } from './colorAssignmentHelpers'
|
|
13
14
|
|
|
14
15
|
export const getColorScale = (config: ChartConfig): ((value: string) => string) => {
|
|
15
16
|
const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
|
|
@@ -32,18 +33,24 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
|
|
|
32
33
|
// Migrate old palette name if needed
|
|
33
34
|
const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
|
|
34
35
|
|
|
36
|
+
const domain = config.runtime.seriesLabelsAll
|
|
37
|
+
|
|
38
|
+
const customColorsOrdered = config.general?.palette?.customColorsOrdered
|
|
39
|
+
|
|
35
40
|
// Check for customColorsOrdered first (direct 1-to-1 mapping, no distribution needed)
|
|
36
|
-
if (
|
|
37
|
-
const
|
|
41
|
+
if (Array.isArray(customColorsOrdered) && customColorsOrdered.length > 0) {
|
|
42
|
+
const range = applySeriesColorAssignmentsToRange(config, domain, customColorsOrdered)
|
|
38
43
|
return scaleOrdinal({
|
|
39
|
-
domain
|
|
40
|
-
range
|
|
44
|
+
domain,
|
|
45
|
+
range,
|
|
41
46
|
unknown: null
|
|
42
47
|
})
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
const customColors = config.general?.palette?.customColors
|
|
51
|
+
const isUsingCustomColors = Array.isArray(customColors) && customColors.length > 0
|
|
45
52
|
let palette =
|
|
46
|
-
|
|
53
|
+
(isUsingCustomColors ? customColors : undefined) ||
|
|
47
54
|
palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
|
|
48
55
|
palettesSource[configPalette]
|
|
49
56
|
|
|
@@ -58,8 +65,8 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
|
|
|
58
65
|
// Apply enhanced color distribution (same logic as pie charts)
|
|
59
66
|
const paletteVersion = getColorPaletteVersion(config)
|
|
60
67
|
|
|
61
|
-
// Skip enhanced distribution if not v2, too many keys, or wrong palette length
|
|
62
|
-
if (paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
|
|
68
|
+
// Skip enhanced distribution if using custom colors, not v2, too many keys, or wrong palette length
|
|
69
|
+
if (isUsingCustomColors || paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
|
|
63
70
|
// Use existing logic for v1 palettes and other cases
|
|
64
71
|
while (numberOfKeys > palette.length) {
|
|
65
72
|
palette = palette.concat(palette)
|
|
@@ -90,9 +97,11 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
|
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
const range = applySeriesColorAssignmentsToRange(config, domain, palette)
|
|
101
|
+
|
|
93
102
|
return scaleOrdinal({
|
|
94
|
-
domain
|
|
95
|
-
range
|
|
103
|
+
domain,
|
|
104
|
+
range,
|
|
96
105
|
unknown: null
|
|
97
106
|
})
|
|
98
107
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pick from 'lodash/pick'
|
|
2
|
+
import values from 'lodash/values'
|
|
3
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
4
|
+
|
|
5
|
+
type GetHasBoundarySuppressionProps = {
|
|
6
|
+
rows?: Object[]
|
|
7
|
+
config: ChartConfig
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const getHasBoundarySuppression = ({ rows, config }: GetHasBoundarySuppressionProps) => {
|
|
11
|
+
if (!Array.isArray(rows) || rows.length === 0) return false
|
|
12
|
+
const suppressionRules = config.preliminaryData?.filter(({ type, style }) => type === 'suppression' && style) || []
|
|
13
|
+
if (!suppressionRules.length) return false
|
|
14
|
+
|
|
15
|
+
const boundaryRows = [rows[0], rows[rows.length - 1]].filter(Boolean)
|
|
16
|
+
|
|
17
|
+
return boundaryRows.some(item =>
|
|
18
|
+
suppressionRules.some(({ column, value }) => {
|
|
19
|
+
const seriesValues = values(pick(item, config.runtime?.seriesKeys))
|
|
20
|
+
const dynamicCategory = config.series[0]?.dynamicCategory
|
|
21
|
+
const match = column ? item[column] === value : seriesValues.includes(value)
|
|
22
|
+
const dynamic = dynamicCategory && (item[dynamicCategory] === column || !column)
|
|
23
|
+
|
|
24
|
+
return Boolean(match || dynamic)
|
|
25
|
+
})
|
|
26
|
+
)
|
|
27
|
+
}
|
package/src/helpers/getMinMax.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
-
import
|
|
2
|
+
import { getCleanTopTickMax } from './getCleanTopTickMax'
|
|
3
|
+
import { getAxisMaxOverride } from './getAxisMaxOverride'
|
|
3
4
|
|
|
4
5
|
type GetMinMaxProps = {
|
|
5
6
|
/** config - standard chart config */
|
|
@@ -18,6 +19,8 @@ type GetMinMaxProps = {
|
|
|
18
19
|
isAllLine: boolean
|
|
19
20
|
/** convertLineToBarGraph - whether line charts should be rendered as bar graphs */
|
|
20
21
|
convertLineToBarGraph?: boolean
|
|
22
|
+
/** whether selected-domain boundary rows include suppression tokens */
|
|
23
|
+
hasBoundarySuppression?: boolean
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
const getMinMax = ({
|
|
@@ -28,7 +31,8 @@ const getMinMax = ({
|
|
|
28
31
|
data,
|
|
29
32
|
isAllLine,
|
|
30
33
|
tableData,
|
|
31
|
-
convertLineToBarGraph
|
|
34
|
+
convertLineToBarGraph,
|
|
35
|
+
hasBoundarySuppression = false
|
|
32
36
|
}: GetMinMaxProps) => {
|
|
33
37
|
let min = 0
|
|
34
38
|
let max = 0
|
|
@@ -45,14 +49,18 @@ const getMinMax = ({
|
|
|
45
49
|
const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
|
|
46
50
|
const paddingAddedToAxis = config.yAxis.enablePadding ? 1 + config.yAxis.scalePadding / 100 : 1
|
|
47
51
|
const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
|
|
48
|
-
//
|
|
49
|
-
const
|
|
52
|
+
// Validate before applying chart-specific domain rules.
|
|
53
|
+
const { hasValidMax: hasValidExplicitLeftMax, maxNumber: enteredMaxNumber } = getAxisMaxOverride({
|
|
54
|
+
value: enteredMaxValue,
|
|
55
|
+
minimumValidMax: existPositiveValue ? maxValue : 0
|
|
56
|
+
})
|
|
57
|
+
const shouldCleanAutoLeftMax = config.yAxis.autoMaxStrategy === 'clean-top-tick' && !hasValidExplicitLeftMax
|
|
50
58
|
const isMinValid = isLogarithmicAxis
|
|
51
59
|
? Number(enteredMinValue) >= 0
|
|
52
60
|
: (Number(enteredMinValue) <= 0 && minValue >= 0) || (Number(enteredMinValue) <= minValue && minValue < 0)
|
|
53
61
|
|
|
54
62
|
min = enteredMinValue && isMinValid ? Number(enteredMinValue) : minValue
|
|
55
|
-
max =
|
|
63
|
+
max = hasValidExplicitLeftMax ? enteredMaxNumber : Number.MIN_VALUE
|
|
56
64
|
const { lower, upper } = config?.confidenceKeys || {}
|
|
57
65
|
|
|
58
66
|
const useBrushFullRange = config.xAxis.brushActive && !config.xAxis.brushDynamicYAxis
|
|
@@ -135,8 +143,8 @@ const getMinMax = ({
|
|
|
135
143
|
leftMax = findMaxFromSeriesKeys(dataForMinMax, leftAxisSeriesItems, leftMax, 'left')
|
|
136
144
|
rightMax = findMaxFromSeriesKeys(dataForMinMax, rightAxisSeriesItems, rightMax, 'right')
|
|
137
145
|
|
|
138
|
-
if (leftMax <
|
|
139
|
-
leftMax =
|
|
146
|
+
if (hasValidExplicitLeftMax && leftMax < enteredMaxNumber) {
|
|
147
|
+
leftMax = enteredMaxNumber
|
|
140
148
|
}
|
|
141
149
|
} catch (e) {
|
|
142
150
|
console.error(e.message)
|
|
@@ -180,23 +188,10 @@ const getMinMax = ({
|
|
|
180
188
|
const numEnteredMin = Number(enteredMinValue)
|
|
181
189
|
const isMinValid = isLogarithmicAxis ? numEnteredMin >= 0 && numEnteredMin < minValue : numEnteredMin < minValue
|
|
182
190
|
|
|
183
|
-
const suppressedMinValue = tableData?.some((item, i, arr) =>
|
|
184
|
-
config.preliminaryData?.some(({ type, style, column, value }) => {
|
|
185
|
-
if (type !== 'suppression' || !style) return false
|
|
186
|
-
|
|
187
|
-
const values = _.values(_.pick(item, config.runtime?.seriesKeys))
|
|
188
|
-
const dynamicCategory = config.series[0].dynamicCategory
|
|
189
|
-
|
|
190
|
-
const match = column ? item[column] === value : values.includes(value)
|
|
191
|
-
const dynamic = dynamicCategory && (item[dynamicCategory] === column || !column)
|
|
192
|
-
|
|
193
|
-
return (match || dynamic) && (i === 0 || i === arr.length - 1)
|
|
194
|
-
})
|
|
195
|
-
)
|
|
196
|
-
|
|
197
191
|
const isCategorical = config.yAxis.type === 'categorical'
|
|
198
192
|
|
|
199
|
-
min =
|
|
193
|
+
min =
|
|
194
|
+
enteredMinValue !== '' && isMinValid ? numEnteredMin : hasBoundarySuppression ? 0 : isCategorical ? 0 : minValue
|
|
200
195
|
}
|
|
201
196
|
|
|
202
197
|
//If data value max wasn't provided, calculate it
|
|
@@ -205,6 +200,14 @@ const getMinMax = ({
|
|
|
205
200
|
max = existPositiveValue ? maxValue : 0
|
|
206
201
|
}
|
|
207
202
|
|
|
203
|
+
if (shouldCleanAutoLeftMax) {
|
|
204
|
+
if (config.visualizationType === 'Combo') {
|
|
205
|
+
leftMax = getCleanTopTickMax(leftMax)
|
|
206
|
+
} else {
|
|
207
|
+
max = getCleanTopTickMax(max)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
208
211
|
//Adds Y Axis data padding if applicable
|
|
209
212
|
if (config.runtime.yAxis.paddingPercent) {
|
|
210
213
|
let paddingValue = (max - min) * config.runtime.yAxis.paddingPercent
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import toNumber from 'lodash/toNumber'
|
|
2
2
|
|
|
3
3
|
export const getPiePercent = (data: Record<string, any>[], seriesKey: string): Record<string, any>[] => {
|
|
4
4
|
// getonly the numeric values for each seriesKey
|
|
5
|
-
const numericValues = data.map(row =>
|
|
5
|
+
const numericValues = data.map(row => toNumber(row[seriesKey])).filter(v => !Number.isNaN(v))
|
|
6
6
|
|
|
7
7
|
const total = numericValues.reduce((sum, v) => sum + v, 0)
|
|
8
8
|
|
|
9
9
|
return data.map(row => {
|
|
10
|
-
const raw =
|
|
10
|
+
const raw = toNumber(row[seriesKey])
|
|
11
11
|
if (Number.isNaN(raw)) {
|
|
12
12
|
// skip non-numeric / undefined
|
|
13
13
|
return row
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
+
import { AxisFilterDomainBehavior } from '@cdc/core/types/Axis'
|
|
3
|
+
|
|
4
|
+
const hasRows = (data?: Object[]) => Array.isArray(data) && data.length > 0
|
|
5
|
+
|
|
6
|
+
export const getYAxisFilterDomainBehavior = (config: ChartConfig): AxisFilterDomainBehavior => {
|
|
7
|
+
return config.yAxis?.filterDomainBehavior === 'stable' ? 'stable' : 'dynamic'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const getYAxisDomainData = ({
|
|
11
|
+
config,
|
|
12
|
+
data,
|
|
13
|
+
tableData,
|
|
14
|
+
fullEligibleDomainData
|
|
15
|
+
}: {
|
|
16
|
+
config: ChartConfig
|
|
17
|
+
data: Object[]
|
|
18
|
+
tableData?: Object[]
|
|
19
|
+
fullEligibleDomainData?: Object[]
|
|
20
|
+
}): Object[] => {
|
|
21
|
+
const brushActive = Boolean(config.xAxis?.brushActive)
|
|
22
|
+
const brushDynamicYAxis = Boolean(config.xAxis?.brushDynamicYAxis)
|
|
23
|
+
|
|
24
|
+
if (brushActive && brushDynamicYAxis) {
|
|
25
|
+
return data
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const behavior = getYAxisFilterDomainBehavior(config)
|
|
29
|
+
if (behavior === 'stable' && hasRows(fullEligibleDomainData)) {
|
|
30
|
+
return fullEligibleDomainData
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (brushActive && !brushDynamicYAxis && hasRows(tableData)) {
|
|
34
|
+
return tableData
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return data
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ChartConfig } from '../types/ChartConfig'
|
|
2
|
+
|
|
3
|
+
type YAxisFinalizationEligibilityProps = {
|
|
4
|
+
config: ChartConfig
|
|
5
|
+
hasSpacedInlineLabel: boolean
|
|
6
|
+
hasValidExplicitLeftMax: boolean
|
|
7
|
+
isHorizontal: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const chartUsesStandardPrimaryYScale = (config: ChartConfig) => {
|
|
11
|
+
return !['Box Plot', 'Bump Chart', 'Forest Plot'].includes(config.visualizationType)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isLinearOrLegacyYAxis = (config: ChartConfig) => {
|
|
15
|
+
const yAxisType = config.yAxis.type
|
|
16
|
+
|
|
17
|
+
return yAxisType === undefined || yAxisType === '' || yAxisType === 'linear'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getYAxisFinalizationEligibility = ({
|
|
21
|
+
config,
|
|
22
|
+
hasSpacedInlineLabel,
|
|
23
|
+
hasValidExplicitLeftMax,
|
|
24
|
+
isHorizontal
|
|
25
|
+
}: YAxisFinalizationEligibilityProps) => {
|
|
26
|
+
const hasStandardLinearPrimaryYAxis =
|
|
27
|
+
!isHorizontal && isLinearOrLegacyYAxis(config) && chartUsesStandardPrimaryYScale(config)
|
|
28
|
+
const supportsInlineLabelHeadroom = hasStandardLinearPrimaryYAxis && config.visualizationType !== 'Combo'
|
|
29
|
+
const shouldUseInlineLabelHeadroom = hasSpacedInlineLabel && supportsInlineLabelHeadroom
|
|
30
|
+
const shouldUseCleanTopTick =
|
|
31
|
+
!hasValidExplicitLeftMax && (config.yAxis.autoMaxStrategy === 'clean-top-tick' || shouldUseInlineLabelHeadroom)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
hasStandardLinearPrimaryYAxis,
|
|
35
|
+
supportsInlineLabelHeadroom,
|
|
36
|
+
shouldUseCleanTopTick,
|
|
37
|
+
shouldUseInlineLabelHeadroom,
|
|
38
|
+
shouldFinalizePrimaryYAxis: shouldUseCleanTopTick && hasStandardLinearPrimaryYAxis
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
2
|
+
|
|
3
|
+
export const hasSpacedInlineLabel = (config: ChartConfig): boolean => {
|
|
4
|
+
const inlineLabel = config.yAxis?.inlineLabel
|
|
5
|
+
if (inlineLabel && inlineLabel.includes(' ')) {
|
|
6
|
+
return true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import uniq from 'lodash/uniq'
|
|
2
2
|
|
|
3
3
|
export const isConvertLineToBarGraph = (configObj, filteredData) => {
|
|
4
4
|
const { allowLineToBarGraph, series, visualizationType, xAxis } = configObj
|
|
@@ -6,6 +6,6 @@ export const isConvertLineToBarGraph = (configObj, filteredData) => {
|
|
|
6
6
|
const lineWithLessThanThreePoints = visualizationType === 'Line' && filteredData?.length < 3
|
|
7
7
|
const isDynamicSeries = series?.some(series => series.dynamicCategory)
|
|
8
8
|
const isDynamicWithLessThanThreePoints =
|
|
9
|
-
isDynamicSeries &&
|
|
9
|
+
isDynamicSeries && uniq(filteredData?.map(data => data[xAxis.dataKey])).length <= 2
|
|
10
10
|
return lineWithLessThanThreePoints || isDynamicWithLessThanThreePoints
|
|
11
11
|
}
|