Skip to content

Commit c263ecf

Browse files
authored
Fix: forecast chart palettes migrations (#2347)
* fix: forecast palette selections * Add new helpers * Add new helpers * Fix: forecast chart migrations * add missing files
1 parent 4d3691f commit c263ecf

File tree

9 files changed

+699
-194
lines changed

9 files changed

+699
-194
lines changed

packages/chart/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@
4848
<!-- GENERIC CHART TYPES -->
4949
<!-- <div class="react-container" data-config="/examples/private/ari-other-conditions-1.json"></div> -->
5050
<!-- <div class="react-container" data-config="/examples/private/Viral-Respiratory-Deaths-Age.json"></div> -->
51-
<div class="react-container" data-config="/examples/grouped-bar-test.json"></div>
51+
<!-- <div class="react-container" data-config="/examples/private/f3.json"></div> -->
5252
<!-- <div class="react-container" data-config="/examples/test.json"></div> -->
5353
<!-- <div class="react-container" data-config="/examples/feature/line/line-chart.json"></div> -->
5454
<!-- <div class="react-container" data-config="/examples/dev-8332.json"></div> -->
55-
<!-- <div class="react-container" data-config="/examples/feature/annotations/index.json"></div> -->
55+
<div class="react-container" data-config="/examples/private/f4.json"></div>
5656
<!-- <div class="react-container" data-config="/examples/feature/filters/url-filter.json"></div> -->
5757
<!-- <div class="react-container" data-config="/examples/feature/bar/additional-column-tooltip.json"></div> -->
5858
<!-- <div class="react-container" data-config="https://cdc.gov/poxvirus/mpox/modules/data-viz/mpx-trends_1.json"></div> -->
@@ -160,4 +160,4 @@
160160
<script type="module" src="./src/index.jsx"></script>
161161
</body>
162162

163-
</html>
163+
</html>

packages/chart/src/components/EditorPanel/EditorPanel.tsx

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,9 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
834834
const [pendingPaletteSelection, setPendingPaletteSelection] = useState<{
835835
palette: string
836836
action: () => void
837+
seriesIndex?: number
838+
stageIndex?: number
839+
type?: 'general' | 'twoColor' | 'forecast'
837840
} | null>(null)
838841

839842
const setLollipopShape = shape => {
@@ -1137,7 +1140,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
11371140
}
11381141

11391142
if (isV1PaletteConfig) {
1140-
setPendingPaletteSelection({ palette, action: executeSelection })
1143+
setPendingPaletteSelection({ palette, action: executeSelection, type: 'general' })
11411144
setShowConversionModal(true)
11421145
} else {
11431146
executeSelection()
@@ -1208,7 +1211,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
12081211
}
12091212

12101213
if (isV1PaletteConfig) {
1211-
setPendingPaletteSelection({ palette, action: executeSelection })
1214+
setPendingPaletteSelection({ palette, action: executeSelection, type: 'twoColor' })
12121215
setShowConversionModal(true)
12131216
} else {
12141217
executeSelection()
@@ -1218,6 +1221,111 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
12181221
}
12191222
}
12201223

1224+
// Forecast palette selection - includes v1/v2 migration modal logic
1225+
const handleForecastPaletteSelection = (palette: string, seriesIndex: number, stageIndex: number) => {
1226+
try {
1227+
if (!config) {
1228+
console.error('COVE: Config is undefined in handleForecastPaletteSelection')
1229+
return
1230+
}
1231+
1232+
// Check if it's a v1 palette configuration
1233+
const isV1PaletteConfig = isV1Palette(config)
1234+
1235+
const executeSelection = () => {
1236+
const copyOfSeries = [...config.series]
1237+
const copyOfStages = [...(copyOfSeries[seriesIndex].stages || [])]
1238+
copyOfStages[stageIndex] = { ...copyOfStages[stageIndex], color: palette }
1239+
copyOfSeries[seriesIndex] = { ...copyOfSeries[seriesIndex], stages: copyOfStages }
1240+
1241+
const _newConfig = cloneConfig(config)
1242+
_newConfig.series = copyOfSeries
1243+
1244+
// If this is the first v2 palette selection, upgrade to v2
1245+
if (isV1PaletteConfig && USE_V2_MIGRATION) {
1246+
if (!_newConfig.general) {
1247+
_newConfig.general = {}
1248+
}
1249+
if (!_newConfig.general.palette) {
1250+
_newConfig.general.palette = {}
1251+
}
1252+
_newConfig.general.palette.version = '2.0'
1253+
1254+
// Forecast-specific migration map for v1 → v2 palette names (all lowercase-hyphen format)
1255+
const forecastPaletteMigrationMap: Record<string, string> = {
1256+
// Sequential Blue variants → sequential-blue
1257+
'sequential-blue': 'sequential-blue',
1258+
'sequential-blue-two': 'sequential-blue',
1259+
'sequential-blue-three': 'sequential-blue',
1260+
'sequential-blue-2-(mpx)': 'sequential-blue',
1261+
'sequential-blue-2-mpx': 'sequential-blue',
1262+
// Sequential Orange variants → sequential-orange
1263+
'sequential-orange': 'sequential-orange',
1264+
'sequential-orange-two': 'sequential-orange',
1265+
'sequential-orange-(mpx)': 'sequential-orange',
1266+
'sequential-orange-mpx': 'sequential-orange',
1267+
// Other sequential palettes (no variants, just normalize)
1268+
'sequential-green': 'sequential-green',
1269+
'sequential-purple': 'sequential-purple',
1270+
'sequential-teal': 'sequential-teal',
1271+
// Reverse variants - Sequential Blue
1272+
'sequential-bluereverse': 'sequential-bluereverse',
1273+
'sequential-blue-reverse': 'sequential-bluereverse',
1274+
'sequential-blue-tworeverse': 'sequential-bluereverse',
1275+
'sequential-blue-two-reverse': 'sequential-bluereverse',
1276+
'sequential-blue-threereverse': 'sequential-bluereverse',
1277+
'sequential-blue-three-reverse': 'sequential-bluereverse',
1278+
'sequential-blue-2-(mpx)reverse': 'sequential-bluereverse',
1279+
'sequential-blue-2-(mpx)-reverse': 'sequential-bluereverse',
1280+
'sequential-blue-2-mpxreverse': 'sequential-bluereverse',
1281+
'sequential-blue-2-mpx-reverse': 'sequential-bluereverse',
1282+
// Reverse variants - Sequential Orange
1283+
'sequential-orangereverse': 'sequential-orangereverse',
1284+
'sequential-orange-reverse': 'sequential-orangereverse',
1285+
'sequential-orange-tworeverse': 'sequential-orangereverse',
1286+
'sequential-orange-two-reverse': 'sequential-orangereverse',
1287+
'sequential-orange-(mpx)reverse': 'sequential-orangereverse',
1288+
'sequential-orange-(mpx)-reverse': 'sequential-orangereverse',
1289+
'sequential-orange-mpxreverse': 'sequential-orangereverse',
1290+
'sequential-orange-mpx-reverse': 'sequential-orangereverse',
1291+
// Reverse variants - Other sequential palettes
1292+
'sequential-greenreverse': 'sequential-greenreverse',
1293+
'sequential-green-reverse': 'sequential-greenreverse',
1294+
'sequential-purplereverse': 'sequential-purplereverse',
1295+
'sequential-purple-reverse': 'sequential-purplereverse',
1296+
'sequential-tealreverse': 'sequential-tealreverse',
1297+
'sequential-teal-reverse': 'sequential-tealreverse'
1298+
}
1299+
1300+
// Migrate and normalize all forecast stage colors to v2 format
1301+
_newConfig.series.forEach((series: any) => {
1302+
if (series.type === 'Forecasting' && series.stages) {
1303+
series.stages.forEach((stage: any) => {
1304+
if (stage.color) {
1305+
// First, try to migrate using the map
1306+
const migrated = forecastPaletteMigrationMap[stage.color] || stage.color
1307+
// Then normalize to lowercase with hyphens
1308+
stage.color = migrated.toLowerCase().replace(/ /g, '-').replace(/_/g, '-')
1309+
}
1310+
})
1311+
}
1312+
})
1313+
}
1314+
1315+
updateConfig(_newConfig)
1316+
}
1317+
1318+
if (isV1PaletteConfig) {
1319+
setPendingPaletteSelection({ palette, action: executeSelection, type: 'forecast', seriesIndex, stageIndex })
1320+
setShowConversionModal(true)
1321+
} else {
1322+
executeSelection()
1323+
}
1324+
} catch (error) {
1325+
console.error('COVE: Error in handleForecastPaletteSelection:', error)
1326+
}
1327+
}
1328+
12211329
// Modal handlers
12221330
const handleConversionConfirm = () => {
12231331
if (pendingPaletteSelection) {
@@ -1228,18 +1336,53 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
12281336
}
12291337

12301338
const handleConversionCancel = () => {
1339+
// Don't update config - just close modal and discard pending selection
12311340
setShowConversionModal(false)
12321341
setPendingPaletteSelection(null)
12331342
}
12341343

12351344
const handleReturnToV1 = () => {
12361345
if (pendingPaletteSelection) {
12371346
const _newConfig = cloneConfig(config)
1347+
const { palette, type } = pendingPaletteSelection
1348+
1349+
// Handle based on palette type
1350+
if (type === 'forecast') {
1351+
// Forecast palette selection
1352+
const { seriesIndex, stageIndex } = pendingPaletteSelection
1353+
if (seriesIndex !== undefined && stageIndex !== undefined) {
1354+
const copyOfSeries = [..._newConfig.series]
1355+
const copyOfStages = [...copyOfSeries[seriesIndex].stages]
1356+
copyOfStages[stageIndex] = { ...copyOfStages[stageIndex], color: palette }
1357+
copyOfSeries[seriesIndex] = { ...copyOfSeries[seriesIndex], stages: copyOfStages }
1358+
_newConfig.series = copyOfSeries
1359+
}
1360+
} else if (type === 'twoColor') {
1361+
// Two-color palette selection
1362+
if (!_newConfig.twoColor) {
1363+
_newConfig.twoColor = { palette: '', isPaletteReversed: false }
1364+
}
1365+
_newConfig.twoColor.palette = palette
1366+
} else {
1367+
// General palette selection (type === 'general' or undefined for backwards compatibility)
1368+
if (!_newConfig.general) {
1369+
_newConfig.general = {}
1370+
}
1371+
if (!_newConfig.general.palette) {
1372+
_newConfig.general.palette = {}
1373+
}
1374+
_newConfig.general.palette.name = palette
1375+
}
1376+
1377+
// Set version to V1
1378+
if (!_newConfig.general) {
1379+
_newConfig.general = {}
1380+
}
12381381
if (!_newConfig.general.palette) {
12391382
_newConfig.general.palette = {}
12401383
}
1241-
_newConfig.general.palette.name = pendingPaletteSelection.palette
12421384
_newConfig.general.palette.version = '1.0'
1385+
12431386
updateConfig(_newConfig)
12441387
}
12451388
setShowConversionModal(false)
@@ -1514,7 +1657,8 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
15141657
handleUpdateHighlightedBarColor,
15151658
setLollipopShape,
15161659
handlePaletteSelection,
1517-
handleTwoColorPaletteSelection
1660+
handleTwoColorPaletteSelection,
1661+
handleForecastPaletteSelection
15181662
}
15191663
if (isLoading) {
15201664
return <></>
@@ -1609,7 +1753,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
16091753
options={getColumns()}
16101754
/>
16111755
{config.series && config.series.length !== 0 && (
1612-
<Panels.Series.Wrapper getColumns={getColumns}>
1756+
<Panels.Series.Wrapper getColumns={getColumns} handleForecastPaletteSelection={handleForecastPaletteSelection}>
16131757
<fieldset>
16141758
<legend className='edit-label float-left'>Displaying</legend>
16151759
<Tooltip style={{ textTransform: 'none' }}>

packages/chart/src/components/EditorPanel/EditorPanelContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type EditorPanelContext = {
2525
setLollipopShape?: Function
2626
handlePaletteSelection?: (palette: string) => void
2727
handleTwoColorPaletteSelection?: (palette: string) => void
28+
handleForecastPaletteSelection?: (palette: string, seriesIndex: number, stageIndex: number) => void
2829
}
2930

3031
const EditorPanelContext = createContext<EditorPanelContext>(null)

packages/chart/src/components/EditorPanel/components/Panels/Panel.Series.tsx

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import ConfigContext from '../../../../ConfigContext'
55
import InputSelect from '@cdc/core/components/inputs/InputSelect'
66
import Check from '@cdc/core/assets/icon-check.svg'
77
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
8-
import { sequentialPalettes } from '@cdc/core/data/colorPalettes'
8+
import { colorPalettesChartV1, colorPalettesChartV2, sequentialPalettes } from '@cdc/core/data/colorPalettes'
9+
import { updatePaletteNames } from '@cdc/core/helpers/updatePaletteNames'
10+
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
911
import Icon from '@cdc/core/components/ui/Icon'
1012
import { Select } from '@cdc/core/components/EditorPanel/Inputs'
13+
import { buildForecastPaletteOptions } from '../../../../helpers/buildForecastPaletteOptions'
1114

1215
// Third Party
1316
import {
@@ -25,7 +28,7 @@ const SeriesContext = React.createContext({})
2528
const SeriesWrapper = props => {
2629
const { updateConfig, config, rawData } = useContext(ConfigContext)
2730

28-
const { getColumns, selectComponent } = props
31+
const { getColumns, selectComponent, handleForecastPaletteSelection } = props
2932

3033
const supportedRightAxisTypes = ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg']
3134

@@ -57,7 +60,9 @@ const SeriesWrapper = props => {
5760
}
5861

5962
return (
60-
<SeriesContext.Provider value={{ updateSeries, supportedRightAxisTypes, getColumns, selectComponent }}>
63+
<SeriesContext.Provider
64+
value={{ updateSeries, supportedRightAxisTypes, getColumns, selectComponent, handleForecastPaletteSelection }}
65+
>
6166
{props.children}
6267
</SeriesContext.Provider>
6368
)
@@ -240,7 +245,8 @@ const SeriesDropdownAxisPosition = props => {
240245
}
241246

242247
const SeriesDropdownForecastColor = props => {
243-
const { config, updateConfig } = useContext(ConfigContext)
248+
const { config } = useContext(ConfigContext)
249+
const { handleForecastPaletteSelection } = useContext(SeriesContext)
244250

245251
const { index, series } = props
246252

@@ -249,30 +255,32 @@ const SeriesDropdownForecastColor = props => {
249255
// Hide AxisPositionDropdown in certain cases.
250256
if (!series) return
251257

252-
const allowedForecastingColors = () => {
253-
return Object.keys(sequentialPalettes)
254-
}
258+
// Determine palette version and use appropriate palette set
259+
// Forecasting charts use sequentialPalettes for v1, sequential-only palettes for v2
260+
const paletteVersion = getColorPaletteVersion(config)
261+
262+
// Get version-appropriate palettes (v1 uses sequentialPalettes, v2 uses filtered v2 palettes)
263+
const forecastPalettes =
264+
paletteVersion === 1
265+
? sequentialPalettes
266+
: Object.fromEntries(Object.entries(colorPalettesChartV2).filter(([key]) => key.startsWith('sequential')))
267+
268+
// For dropdown options, only show version-specific palettes
269+
const processedPalettes = updatePaletteNames(forecastPalettes)
270+
const paletteOptions = buildForecastPaletteOptions(processedPalettes, paletteVersion)
255271

256272
return series?.stages?.map((stage, stageIndex) => (
257273
<InputSelect
258274
key={`${stage}--${stageIndex}`}
259275
initial='Select an option'
260-
value={
261-
config.series?.[index].stages?.[stageIndex].color ? config.series?.[index].stages?.[stageIndex].color : 'Select'
262-
}
276+
value={config.series?.[index].stages?.[stageIndex].color || 'Select'}
263277
label={`${stage.key} Series Color`}
264278
onChange={event => {
265-
const copyOfSeries = [...config.series] // copy the entire series array
266-
const copyOfStages = copyOfSeries[index].stages
267-
copyOfStages[stageIndex].color = event.target.value
268-
copyOfSeries[index] = { ...copyOfSeries[index], stages: copyOfStages }
269-
270-
updateConfig({
271-
...config,
272-
series: copyOfSeries
273-
})
279+
if (handleForecastPaletteSelection) {
280+
handleForecastPaletteSelection(event.target.value, index, stageIndex)
281+
}
274282
}}
275-
options={Object.keys(sequentialPalettes)}
283+
options={paletteOptions}
276284
/>
277285
))
278286
}

0 commit comments

Comments
 (0)