@cdc/chart 4.25.8 → 4.25.10
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/.claude/settings.local.json +9 -0
- package/dist/cdcchart.js +37524 -35243
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/d.json +382 -0
- package/examples/private/example-2.json +49784 -0
- package/examples/private/f2.json +1 -0
- package/examples/private/f4.json +1577 -0
- package/examples/private/forecast.json +1180 -0
- package/examples/private/lollipop.json +468 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +10 -22
- package/package.json +25 -7
- package/src/CdcChart.tsx +1 -2
- package/src/CdcChartComponent.tsx +174 -32
- package/src/_stories/Chart.Anchors.stories.tsx +2 -2
- package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
- package/src/_stories/Chart.CI.stories.tsx +1 -1
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
- package/src/_stories/Chart.Filters.stories.tsx +2 -2
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +19 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +1 -1
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartEditor.stories.tsx +60 -60
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
- package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
- package/src/_stories/_mock/stacked-pattern-test.json +520 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +159 -20
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
- package/src/components/BarChart/components/BarChart.Vertical.tsx +153 -21
- package/src/components/BarChart/helpers/index.ts +43 -4
- package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
- package/src/components/BarChart/helpers/useBarChart.ts +25 -3
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +364 -39
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +414 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +28 -20
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +115 -120
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/Forecasting/Forecasting.tsx +36 -6
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +106 -13
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/index.tsx +2 -2
- package/src/components/LinearChart.tsx +22 -5
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- package/src/data/initial-state.js +315 -293
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +109 -0
- package/src/helpers/getColorScale.ts +72 -8
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useTooltip.tsx +57 -15
- package/src/index.jsx +0 -2
- package/src/scss/main.scss +12 -0
- package/src/store/chart.reducer.ts +1 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +30 -6
- package/src/types/ChartContext.ts +1 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- package/src/hooks/useColorPalette.js +0 -76
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a palette lookup map with backward-compatibility mappings for forecast charts
|
|
3
|
+
*
|
|
4
|
+
* This function creates multiple naming format mappings to support:
|
|
5
|
+
* - V1 configs using old naming like "Sequential Blue" (Title Case with spaces)
|
|
6
|
+
* - V2 configs using new naming like "sequential-blue" (lowercase with hyphens)
|
|
7
|
+
* - MPX legacy aliases (v1 only)
|
|
8
|
+
*
|
|
9
|
+
* @param processedPalettes - The palette data processed through updatePaletteNames
|
|
10
|
+
* @param paletteVersion - The palette version (1 or 2) from the config
|
|
11
|
+
* @returns A palette map with keys in various formats pointing to color arrays
|
|
12
|
+
*/
|
|
13
|
+
export const buildForecastPaletteMappings = (
|
|
14
|
+
processedPalettes: Record<string, string[]>,
|
|
15
|
+
paletteVersion: number
|
|
16
|
+
): Record<string, string[]> => {
|
|
17
|
+
const paletteMap: Record<string, string[]> = {}
|
|
18
|
+
|
|
19
|
+
// Create base mappings with multiple naming formats for backward compatibility:
|
|
20
|
+
// - sequential-blue (hyphenated)
|
|
21
|
+
// - sequential_blue (underscore)
|
|
22
|
+
// - Sequential Blue (with spaces)
|
|
23
|
+
Object.keys(processedPalettes).forEach(key => {
|
|
24
|
+
const value = processedPalettes[key]
|
|
25
|
+
// Original key
|
|
26
|
+
paletteMap[key] = value
|
|
27
|
+
// Lowercase with hyphens
|
|
28
|
+
paletteMap[key.toLowerCase().replace(/ /g, '-')] = value
|
|
29
|
+
// Lowercase with underscores
|
|
30
|
+
paletteMap[key.toLowerCase().replace(/ /g, '_')] = value
|
|
31
|
+
// Original key variations
|
|
32
|
+
paletteMap[key.replace(/_/g, '-')] = value
|
|
33
|
+
paletteMap[key.toLowerCase()] = value
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (paletteVersion === 1) {
|
|
37
|
+
// V1: Add MPX legacy aliases
|
|
38
|
+
// Note: Sequential Blue Two was aliased as "Sequential Blue 2 (MPX)" and Sequential Orange as "Sequential Orange (MPX)"
|
|
39
|
+
const MPX_ALIASES: Record<string, string[]> = {
|
|
40
|
+
'sequential-blue-two': ['sequential-blue-2-(mpx)', 'sequential-blue-2-(MPX)'],
|
|
41
|
+
'sequential-blue-tworeverse': ['sequential-blue-2-(mpx)reverse', 'sequential-blue-2-(MPX)reverse'],
|
|
42
|
+
'sequential-orange': ['sequential-orange-(mpx)', 'sequential-orange-(MPX)'],
|
|
43
|
+
'sequential-orangereverse': ['sequential-orange-(mpx)reverse', 'sequential-orange-(MPX)reverse']
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Object.entries(MPX_ALIASES).forEach(([canonical, aliases]) => {
|
|
47
|
+
const palette = paletteMap[canonical]
|
|
48
|
+
if (palette) {
|
|
49
|
+
aliases.forEach(alias => {
|
|
50
|
+
paletteMap[alias] = palette
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
// V2: Add backward compatibility mappings for migrated configs
|
|
56
|
+
// Map old v1 sequential palette names (Title Case with spaces) to v2 palettes
|
|
57
|
+
// Also includes MPX aliases since users may have selected those in v1
|
|
58
|
+
const V2_MIGRATION_ALIASES: Record<string, string[]> = {
|
|
59
|
+
'sequential-blue': [
|
|
60
|
+
'Sequential Blue',
|
|
61
|
+
'sequential blue',
|
|
62
|
+
'Sequential Blue Two',
|
|
63
|
+
'Sequential Blue Three',
|
|
64
|
+
'sequential-blue-2-(mpx)',
|
|
65
|
+
'sequential-blue-2-(MPX)',
|
|
66
|
+
'Sequential Blue 2 (MPX)'
|
|
67
|
+
],
|
|
68
|
+
'sequential-bluereverse': [
|
|
69
|
+
'Sequential Blue Reverse',
|
|
70
|
+
'sequential bluereverse',
|
|
71
|
+
'Sequential Blue Two Reverse',
|
|
72
|
+
'Sequential Blue Three Reverse',
|
|
73
|
+
'sequential-blue-2-(mpx)reverse',
|
|
74
|
+
'sequential-blue-2-(MPX)reverse',
|
|
75
|
+
'Sequential Blue 2 (MPX) Reverse'
|
|
76
|
+
],
|
|
77
|
+
'sequential-green': ['Sequential Green', 'sequential green'],
|
|
78
|
+
'sequential-greenreverse': ['Sequential Green Reverse', 'sequential greenreverse'],
|
|
79
|
+
'sequential-orange': [
|
|
80
|
+
'Sequential Orange',
|
|
81
|
+
'sequential orange',
|
|
82
|
+
'Sequential Orange Two',
|
|
83
|
+
'sequential-orange-(mpx)',
|
|
84
|
+
'sequential-orange-(MPX)',
|
|
85
|
+
'Sequential Orange (MPX)'
|
|
86
|
+
],
|
|
87
|
+
'sequential-orangereverse': [
|
|
88
|
+
'Sequential Orange Reverse',
|
|
89
|
+
'sequential orangereverse',
|
|
90
|
+
'Sequential Orange Two Reverse',
|
|
91
|
+
'sequential-orange-(mpx)reverse',
|
|
92
|
+
'sequential-orange-(MPX)reverse',
|
|
93
|
+
'Sequential Orange (MPX) Reverse'
|
|
94
|
+
],
|
|
95
|
+
'sequential-purple': ['Sequential Purple', 'sequential purple'],
|
|
96
|
+
'sequential-purplereverse': ['Sequential Purple Reverse', 'sequential purplereverse'],
|
|
97
|
+
'sequential-teal': ['Sequential Teal', 'sequential teal'],
|
|
98
|
+
'sequential-tealreverse': ['Sequential Teal Reverse', 'sequential tealreverse']
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Object.entries(V2_MIGRATION_ALIASES).forEach(([canonical, aliases]) => {
|
|
102
|
+
const palette = paletteMap[canonical]
|
|
103
|
+
if (palette) {
|
|
104
|
+
aliases.forEach(alias => {
|
|
105
|
+
paletteMap[alias] = palette
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return paletteMap
|
|
112
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds user-friendly palette options for forecast color dropdowns
|
|
3
|
+
*
|
|
4
|
+
* This function takes processed palette data and transforms it into a format
|
|
5
|
+
* suitable for dropdown UI with clean, readable display names.
|
|
6
|
+
*
|
|
7
|
+
* @param processedPalettes - The palette data processed through updatePaletteNames
|
|
8
|
+
* @param paletteVersion - The palette version (1 or 2) from the config
|
|
9
|
+
* @returns An object with kebab-case keys and user-friendly display names as values
|
|
10
|
+
*/
|
|
11
|
+
export const buildForecastPaletteOptions = (
|
|
12
|
+
processedPalettes: Record<string, string[]>,
|
|
13
|
+
paletteVersion: number
|
|
14
|
+
): Record<string, string> => {
|
|
15
|
+
const paletteOptions: Record<string, string> = {}
|
|
16
|
+
|
|
17
|
+
// Create user-friendly options with clean, readable names
|
|
18
|
+
Object.keys(processedPalettes).forEach(key => {
|
|
19
|
+
// Clean up the display name:
|
|
20
|
+
// 1. Replace underscores with spaces
|
|
21
|
+
// 2. Add space before "reverse" if it's concatenated
|
|
22
|
+
// 3. Capitalize first letter of each word
|
|
23
|
+
const cleanName = key
|
|
24
|
+
.replace(/_/g, ' ') // Replace underscores with spaces
|
|
25
|
+
.replace(/reverse$/i, ' Reverse') // Add space before reverse
|
|
26
|
+
.replace(/\b\w/g, char => char.toUpperCase()) // Capitalize first letter of each word
|
|
27
|
+
|
|
28
|
+
// Use lowercase with hyphens as the key for consistency with existing saved values
|
|
29
|
+
// Convert both underscores and spaces to hyphens
|
|
30
|
+
const displayKey = key.replace(/_/g, '-').replace(/ /g, '-').toLowerCase()
|
|
31
|
+
paletteOptions[displayKey] = cleanName
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Add MPX aliases for v1 backward compatibility (these were historical names)
|
|
35
|
+
if (paletteVersion === 1) {
|
|
36
|
+
// Map "Sequential Blue Two" to MPX alias
|
|
37
|
+
if (paletteOptions['sequential-blue-two']) {
|
|
38
|
+
paletteOptions['sequential-blue-2-(mpx)'] = 'Sequential Blue 2 (MPX)'
|
|
39
|
+
}
|
|
40
|
+
if (paletteOptions['sequential-blue-tworeverse']) {
|
|
41
|
+
paletteOptions['sequential-blue-2-(mpx)reverse'] = 'Sequential Blue 2 (MPX) Reverse'
|
|
42
|
+
}
|
|
43
|
+
// Map "Sequential Orange" to MPX alias
|
|
44
|
+
if (paletteOptions['sequential-orange']) {
|
|
45
|
+
paletteOptions['sequential-orange-(mpx)'] = 'Sequential Orange (MPX)'
|
|
46
|
+
}
|
|
47
|
+
if (paletteOptions['sequential-orangereverse']) {
|
|
48
|
+
paletteOptions['sequential-orange-(mpx)reverse'] = 'Sequential Orange (MPX) Reverse'
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// V2 backward compatibility: add options for old v1 sequential palette names
|
|
52
|
+
// These are for migrated configs that still have v1-style names like "Sequential Blue"
|
|
53
|
+
if (paletteOptions['sequential-blue']) {
|
|
54
|
+
paletteOptions['sequential-blue'] = 'Sequential Blue'
|
|
55
|
+
}
|
|
56
|
+
if (paletteOptions['sequential-green']) {
|
|
57
|
+
paletteOptions['sequential-green'] = 'Sequential Green'
|
|
58
|
+
}
|
|
59
|
+
if (paletteOptions['sequential-orange']) {
|
|
60
|
+
paletteOptions['sequential-orange'] = 'Sequential Orange'
|
|
61
|
+
}
|
|
62
|
+
if (paletteOptions['sequential-purple']) {
|
|
63
|
+
paletteOptions['sequential-purple'] = 'Sequential Purple'
|
|
64
|
+
}
|
|
65
|
+
if (paletteOptions['sequential-teal']) {
|
|
66
|
+
paletteOptions['sequential-teal'] = 'Sequential Teal'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return paletteOptions
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalizes a palette value to match the standardized hyphenated format
|
|
75
|
+
* and migrates v1 palette names to v2 equivalents
|
|
76
|
+
*
|
|
77
|
+
* @param value - The palette name to normalize
|
|
78
|
+
* @param paletteVersion - The palette version (1 or 2) from the config
|
|
79
|
+
* @returns The normalized palette name in lowercase with hyphens, or 'Select' if empty
|
|
80
|
+
*/
|
|
81
|
+
export const normalizePaletteValue = (value: string | undefined, paletteVersion: number = 1): string => {
|
|
82
|
+
if (!value) return 'Select'
|
|
83
|
+
|
|
84
|
+
// Convert to lowercase with hyphens for consistent matching
|
|
85
|
+
const normalized = value.toLowerCase().replace(/ /g, '-').replace(/_/g, '-')
|
|
86
|
+
|
|
87
|
+
// If v2, migrate v1-only palette names to their v2 equivalents
|
|
88
|
+
if (paletteVersion === 2) {
|
|
89
|
+
const V1_TO_V2_MIGRATION: Record<string, string> = {
|
|
90
|
+
// Sequential Blue variants → sequential-blue
|
|
91
|
+
'sequential-blue-two': 'sequential-blue',
|
|
92
|
+
'sequential-blue-three': 'sequential-blue',
|
|
93
|
+
'sequential-blue-2-(mpx)': 'sequential-blue',
|
|
94
|
+
'sequential-blue-tworeverse': 'sequential-bluereverse',
|
|
95
|
+
'sequential-blue-threereverse': 'sequential-bluereverse',
|
|
96
|
+
'sequential-blue-2-(mpx)reverse': 'sequential-bluereverse',
|
|
97
|
+
|
|
98
|
+
// Sequential Orange variants → sequential-orange
|
|
99
|
+
'sequential-orange-two': 'sequential-orange',
|
|
100
|
+
'sequential-orange-(mpx)': 'sequential-orange',
|
|
101
|
+
'sequential-orange-tworeverse': 'sequential-orangereverse',
|
|
102
|
+
'sequential-orange-(mpx)reverse': 'sequential-orangereverse'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return V1_TO_V2_MIGRATION[normalized] || normalized
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalized
|
|
109
|
+
}
|
|
@@ -1,20 +1,84 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { twoColorPalette } from '@cdc/core/data/colorPalettes'
|
|
2
|
+
import { filterChartColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
|
|
3
|
+
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
2
4
|
import { scaleOrdinal } from '@visx/scale'
|
|
3
5
|
import { ChartConfig } from '../types/ChartConfig'
|
|
6
|
+
import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
|
|
7
|
+
import { getFallbackColorPalette, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
|
|
8
|
+
import {
|
|
9
|
+
v2ColorDistribution,
|
|
10
|
+
divergentColorDistribution,
|
|
11
|
+
colorblindColorDistribution
|
|
12
|
+
} from '@cdc/core/helpers/palettes/colorDistributions'
|
|
4
13
|
|
|
5
14
|
export const getColorScale = (config: ChartConfig): ((value: string) => string) => {
|
|
6
15
|
const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
|
|
7
16
|
? config.twoColor.palette
|
|
8
|
-
: config.palette
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
: config.general?.palette?.name
|
|
18
|
+
const colorPalettes = filterChartColorPalettes(config)
|
|
19
|
+
|
|
20
|
+
// Get the correct version of two-color palettes
|
|
21
|
+
const version = getColorPaletteVersion(config)
|
|
22
|
+
const versionKey = `v${version}`
|
|
23
|
+
const versionedTwoColorPalette = twoColorPalette[versionKey] || twoColorPalette.v2
|
|
24
|
+
|
|
25
|
+
// For paired/deviation bars, only use two-color palettes
|
|
26
|
+
const palettesSource = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
|
|
27
|
+
? versionedTwoColorPalette
|
|
28
|
+
: colorPalettes
|
|
29
|
+
|
|
30
|
+
const allPalettes: Record<string, string[]> = { ...versionedTwoColorPalette, ...colorPalettes }
|
|
31
|
+
|
|
32
|
+
// Migrate old palette name if needed
|
|
33
|
+
const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
|
|
34
|
+
|
|
35
|
+
let palette =
|
|
36
|
+
config.general?.palette?.customColors ||
|
|
37
|
+
palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
|
|
38
|
+
palettesSource[configPalette]
|
|
12
39
|
|
|
13
|
-
|
|
14
|
-
|
|
40
|
+
// Fallback to a default palette if none found
|
|
41
|
+
if (!palette) {
|
|
42
|
+
console.warn(`Palette "${configPalette}" not found, falling back to default`)
|
|
43
|
+
palette = Object.values(allPalettes)[0] || ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
|
|
15
44
|
}
|
|
16
45
|
|
|
17
|
-
|
|
46
|
+
let numberOfKeys = config.runtime.seriesKeys.length
|
|
47
|
+
|
|
48
|
+
// Apply enhanced color distribution (same logic as pie charts)
|
|
49
|
+
const paletteVersion = getColorPaletteVersion(config)
|
|
50
|
+
|
|
51
|
+
// Skip enhanced distribution if not v2, too many keys, or wrong palette length
|
|
52
|
+
if (paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
|
|
53
|
+
// Use existing logic for v1 palettes and other cases
|
|
54
|
+
while (numberOfKeys > palette.length) {
|
|
55
|
+
palette = palette.concat(palette)
|
|
56
|
+
}
|
|
57
|
+
palette = palette.slice(0, numberOfKeys)
|
|
58
|
+
} else {
|
|
59
|
+
// Apply enhanced distribution for v2 palettes
|
|
60
|
+
const isSequential = configPalette && configPalette.includes('sequential')
|
|
61
|
+
const isDivergent = configPalette && configPalette.includes('divergent')
|
|
62
|
+
const isColorblindSafe =
|
|
63
|
+
configPalette && (configPalette.includes('colorblindsafe') || configPalette.includes('qualitative_standard'))
|
|
64
|
+
|
|
65
|
+
// Determine which distribution to use based on palette type
|
|
66
|
+
let distributionMap = null
|
|
67
|
+
if (isDivergent) {
|
|
68
|
+
distributionMap = divergentColorDistribution
|
|
69
|
+
} else if (isColorblindSafe) {
|
|
70
|
+
distributionMap = colorblindColorDistribution
|
|
71
|
+
} else if (isSequential) {
|
|
72
|
+
distributionMap = v2ColorDistribution
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (distributionMap && distributionMap[numberOfKeys]) {
|
|
76
|
+
const distributionIndices = distributionMap[numberOfKeys]
|
|
77
|
+
palette = distributionIndices.map((index: number) => palette[index])
|
|
78
|
+
} else {
|
|
79
|
+
palette = palette.slice(0, numberOfKeys)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
18
82
|
|
|
19
83
|
return scaleOrdinal({
|
|
20
84
|
domain: config.runtime.seriesLabelsAll,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
|
|
3
3
|
export const getNewRuntime = (visualizationConfig, newFilteredData) => {
|
|
4
|
-
const runtime =
|
|
4
|
+
const runtime = visualizationConfig.runtime ? { ...visualizationConfig.runtime } : {}
|
|
5
5
|
runtime.series = []
|
|
6
6
|
runtime.seriesLabels = {}
|
|
7
7
|
runtime.seriesLabelsAll = []
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
3
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
4
|
+
|
|
5
|
+
type UseChartHoverAnalyticsParams = {
|
|
6
|
+
config: any
|
|
7
|
+
interactionLabel?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to track analytics when user enters the chart area
|
|
12
|
+
* Fires once per chart entry, not on every hover interaction
|
|
13
|
+
*/
|
|
14
|
+
export const useChartHoverAnalytics = ({ config, interactionLabel = '' }: UseChartHoverAnalyticsParams) => {
|
|
15
|
+
const hasTrackedRef = useRef(false)
|
|
16
|
+
|
|
17
|
+
const handleChartMouseEnter = () => {
|
|
18
|
+
// Only track if we have an interaction label and haven't tracked yet
|
|
19
|
+
if (!interactionLabel || hasTrackedRef.current) return
|
|
20
|
+
|
|
21
|
+
// Publish the analytics event
|
|
22
|
+
publishAnalyticsEvent({
|
|
23
|
+
vizType: config?.type,
|
|
24
|
+
vizSubType: getVizSubType(config),
|
|
25
|
+
eventType: 'chart_hover',
|
|
26
|
+
eventAction: 'hover',
|
|
27
|
+
eventLabel: interactionLabel,
|
|
28
|
+
vizTitle: getVizTitle(config)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Mark as tracked so we don't fire again
|
|
32
|
+
hasTrackedRef.current = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleChartMouseLeave = () => {
|
|
36
|
+
// Reset tracking when mouse leaves so next entry will track
|
|
37
|
+
hasTrackedRef.current = false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
handleChartMouseEnter,
|
|
42
|
+
handleChartMouseLeave
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,37 +1,74 @@
|
|
|
1
1
|
import isNumber from '@cdc/core/helpers/isNumber'
|
|
2
|
+
import { useMemo } from 'react'
|
|
2
3
|
|
|
3
4
|
function useReduceData(config, data) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
return useMemo(() => {
|
|
6
|
+
if (!data || !config?.runtime?.seriesKeys) {
|
|
7
|
+
return { minValue: 0, maxValue: 0, existPositiveValue: false, isAllLine: false }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const isBar = config.series.every(({ type }) => type === 'Bar')
|
|
11
|
+
const isAllLine = config.series.every(({ type }) => ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg'].includes(type))
|
|
12
|
+
|
|
13
|
+
const cleanChars = value => {
|
|
14
|
+
if (value === null || value === '') {
|
|
15
|
+
return ''
|
|
16
|
+
}
|
|
17
|
+
return typeof value === 'string' ? value.replace(/[,$]/g, '') : value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getSeriesKey = seriesKey => {
|
|
21
|
+
const series = config.runtime.series.find(item => item.dataKey === seriesKey)
|
|
22
|
+
return series?.dynamicCategory ? series.originalDataKey : seriesKey
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const seriesKeysMap = new Map()
|
|
26
|
+
config.runtime.seriesKeys.forEach(key => {
|
|
27
|
+
seriesKeysMap.set(key, getSeriesKey(key))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
let minValue = Infinity
|
|
31
|
+
let maxValue = -Infinity
|
|
32
|
+
let existPositiveValue = false
|
|
33
|
+
const stackedTotals = []
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < data.length; i++) {
|
|
36
|
+
const row = data[i]
|
|
37
|
+
let rowMax = -Infinity
|
|
38
|
+
let rowMin = Infinity
|
|
39
|
+
let stackedSum = 0
|
|
40
|
+
|
|
41
|
+
for (const key of config.runtime.seriesKeys) {
|
|
42
|
+
const seriesKey = seriesKeysMap.get(key)
|
|
43
|
+
const cleanValue = cleanChars(row[seriesKey])
|
|
44
|
+
|
|
45
|
+
if (isNumber(cleanValue)) {
|
|
46
|
+
const numValue = Number(cleanValue)
|
|
47
|
+
|
|
48
|
+
if (numValue > rowMax) rowMax = numValue
|
|
49
|
+
if (numValue < rowMin) rowMin = numValue
|
|
50
|
+
|
|
51
|
+
if (numValue >= 0) existPositiveValue = true
|
|
52
|
+
|
|
53
|
+
if (!isNaN(numValue)) stackedSum += numValue
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (rowMax > maxValue) maxValue = rowMax
|
|
58
|
+
if (rowMin < minValue) minValue = rowMin
|
|
59
|
+
|
|
60
|
+
if (!isNaN(stackedSum)) stackedTotals.push(stackedSum)
|
|
61
|
+
}
|
|
23
62
|
|
|
24
63
|
if (
|
|
25
64
|
(config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && isBar)) &&
|
|
26
65
|
config.visualizationSubType === 'stacked'
|
|
27
66
|
) {
|
|
28
|
-
|
|
29
|
-
max = Math.max(...yTotals)
|
|
67
|
+
maxValue = Math.max(...stackedTotals)
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
if (config.visualizationSubType === 'stacked' && config.visualizationType === 'Area Chart') {
|
|
33
|
-
|
|
34
|
-
max = Math.max(...yTotals)
|
|
71
|
+
maxValue = Math.max(...stackedTotals)
|
|
35
72
|
}
|
|
36
73
|
|
|
37
74
|
if (
|
|
@@ -39,62 +76,60 @@ function useReduceData(config, data) {
|
|
|
39
76
|
config.series &&
|
|
40
77
|
config.series.dataKey
|
|
41
78
|
) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
let specialMax = -Infinity
|
|
80
|
+
for (const row of data) {
|
|
81
|
+
const cleanValue = cleanChars(row[config.series.dataKey])
|
|
82
|
+
if (isNumber(cleanValue)) {
|
|
83
|
+
const numValue = Number(cleanValue)
|
|
84
|
+
if (numValue > specialMax) specialMax = numValue
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
maxValue = specialMax
|
|
45
88
|
}
|
|
46
89
|
|
|
47
90
|
if (config.visualizationType === 'Combo' && config.visualizationSubType === 'stacked' && !isBar) {
|
|
48
91
|
if (config.runtime.barSeriesKeys && config.runtime.lineSeriesKeys) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
let barMax = -Infinity
|
|
93
|
+
let lineMax = -Infinity
|
|
94
|
+
|
|
95
|
+
for (const row of data) {
|
|
96
|
+
let barSum = 0
|
|
97
|
+
let rowLineMax = -Infinity
|
|
98
|
+
|
|
99
|
+
for (const key of config.runtime.barSeriesKeys) {
|
|
100
|
+
const cleanValue = cleanChars(row[key])
|
|
101
|
+
if (isNumber(cleanValue)) {
|
|
102
|
+
const numValue = Number(cleanValue)
|
|
103
|
+
if (!isNaN(numValue)) barSum += numValue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const key of config.runtime.lineSeriesKeys) {
|
|
108
|
+
const cleanValue = cleanChars(row[key])
|
|
109
|
+
if (isNumber(cleanValue)) {
|
|
110
|
+
const numValue = Number(cleanValue)
|
|
111
|
+
if (numValue > rowLineMax) rowLineMax = numValue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (barSum > barMax) barMax = barSum
|
|
116
|
+
if (rowLineMax > lineMax) lineMax = rowLineMax
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
maxValue = Math.max(barMax, lineMax)
|
|
57
120
|
}
|
|
58
121
|
}
|
|
59
122
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const getMinValueFromData = () => {
|
|
64
|
-
const minNumberFromData = Math.min(
|
|
65
|
-
...data.map(d =>
|
|
66
|
-
Math.min(
|
|
67
|
-
...config.runtime.seriesKeys.map(key => {
|
|
68
|
-
const seriesKey = getSeriesKey(key)
|
|
69
|
-
return isNumber(d[seriesKey]) ? Number(cleanChars(d[seriesKey])) : Infinity
|
|
70
|
-
})
|
|
71
|
-
)
|
|
72
|
-
)
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
return String(minNumberFromData)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const findPositiveNum = () => {
|
|
79
|
-
if (!config.runtime.seriesKeys) {
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
return config.runtime.seriesKeys.some(key => data.some(d => d[getSeriesKey(key)] >= 0))
|
|
83
|
-
}
|
|
123
|
+
if (minValue === Infinity) minValue = 0
|
|
124
|
+
if (maxValue === -Infinity) maxValue = 0
|
|
84
125
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
return {
|
|
127
|
+
minValue: Number(minValue),
|
|
128
|
+
maxValue: Number(maxValue),
|
|
129
|
+
existPositiveValue,
|
|
130
|
+
isAllLine
|
|
88
131
|
}
|
|
89
|
-
|
|
90
|
-
return typeof value === 'string' ? value.replace(/[,$]/g, '') : value
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const maxValue = Number(getMaxValueFromData())
|
|
94
|
-
const minValue = Number(getMinValueFromData())
|
|
95
|
-
const existPositiveValue = findPositiveNum()
|
|
96
|
-
|
|
97
|
-
return { minValue, maxValue, existPositiveValue, isAllLine }
|
|
132
|
+
}, [config, data])
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
export default useReduceData
|