@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,414 @@
|
|
|
1
|
+
import { useContext, FC } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Accordion,
|
|
4
|
+
AccordionItem,
|
|
5
|
+
AccordionItemHeading,
|
|
6
|
+
AccordionItemPanel,
|
|
7
|
+
AccordionItemButton
|
|
8
|
+
} from 'react-accessible-accordion'
|
|
9
|
+
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
10
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
11
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
12
|
+
import Alert from '@cdc/core/components/Alert'
|
|
13
|
+
import ConfigContext from '../../../../ConfigContext'
|
|
14
|
+
import { ChartContext } from '../../../../types/ChartContext'
|
|
15
|
+
import { PanelProps } from '../PanelProps'
|
|
16
|
+
import { checkColorContrast, getColorContrast } from '@cdc/core/helpers/cove/accessibility'
|
|
17
|
+
import { getColorScale } from '../../../../helpers/getColorScale'
|
|
18
|
+
import _ from 'lodash'
|
|
19
|
+
|
|
20
|
+
const PanelPatternSettings: FC<PanelProps> = props => {
|
|
21
|
+
const { config, updateConfig, transformedData } = useContext<ChartContext>(ConfigContext)
|
|
22
|
+
|
|
23
|
+
type LegendPattern = {
|
|
24
|
+
label?: string
|
|
25
|
+
color?: string
|
|
26
|
+
shape?: 'circles' | 'lines' | 'diagonalLines' | 'waves'
|
|
27
|
+
dataKey?: string
|
|
28
|
+
dataValue?: string
|
|
29
|
+
contrastCheck?: boolean
|
|
30
|
+
patternSize?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Safe legend reference with defaults to avoid crashes when legend is undefined
|
|
34
|
+
const legendCfg = (config.legend || { patterns: {} }) as {
|
|
35
|
+
patterns: Record<string, LegendPattern>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const patternTypes = [
|
|
39
|
+
{ value: 'circles', label: 'Circles' },
|
|
40
|
+
{ value: 'lines', label: 'Horizontal Lines' },
|
|
41
|
+
{ value: 'diagonalLines', label: 'Diagonal Lines' },
|
|
42
|
+
{ value: 'waves', label: 'Waves' }
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const patternSizes = [
|
|
46
|
+
{ value: 'small', label: 'Small' },
|
|
47
|
+
{ value: 'medium', label: 'Medium' },
|
|
48
|
+
{ value: 'large', label: 'Large' }
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
// Convert numeric size to text and vice versa
|
|
52
|
+
const getPatternSizeText = (numericSize: number): string => {
|
|
53
|
+
if (numericSize <= 6) return 'small'
|
|
54
|
+
if (numericSize <= 12) return 'medium'
|
|
55
|
+
return 'large'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const getPatternSizeNumeric = (textSize: string): number => {
|
|
59
|
+
switch (textSize) {
|
|
60
|
+
case 'small':
|
|
61
|
+
return 6
|
|
62
|
+
case 'medium':
|
|
63
|
+
return 10
|
|
64
|
+
case 'large':
|
|
65
|
+
return 16
|
|
66
|
+
default:
|
|
67
|
+
return 8
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get unique values from a specific data field for dropdown options
|
|
72
|
+
const getDataValueOptions = (dataKey: string) => {
|
|
73
|
+
if (!dataKey || !Array.isArray(transformedData) || transformedData.length === 0) {
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const uniqueValues = Array.from(new Set(transformedData.map(row => row[dataKey])))
|
|
78
|
+
.filter(val => val !== undefined && val !== null && val !== '')
|
|
79
|
+
.sort()
|
|
80
|
+
|
|
81
|
+
return uniqueValues.map(value => ({ value: String(value), label: String(value) }))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const getFieldOptions = () => {
|
|
85
|
+
if (!Array.isArray(transformedData) || transformedData.length === 0) return []
|
|
86
|
+
|
|
87
|
+
const firstRow = transformedData[0] || {}
|
|
88
|
+
return Object.keys(firstRow).map(key => ({ value: key, label: key }))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Checks contrast and logs warning if needed
|
|
92
|
+
const checkAndLogContrast = (fill: string, patternColor: string, dataValue: string, dataKey: string): boolean => {
|
|
93
|
+
if (!fill || !patternColor) return true // Default to true if colors are missing
|
|
94
|
+
|
|
95
|
+
const contrastCheck = checkColorContrast(fill, patternColor)
|
|
96
|
+
|
|
97
|
+
if (!contrastCheck) {
|
|
98
|
+
console.error(
|
|
99
|
+
`COVE: pattern contrast check failed for ${dataValue} in ${dataKey} with:
|
|
100
|
+
pattern color: ${patternColor}
|
|
101
|
+
background color: ${fill}
|
|
102
|
+
contrast: ${getColorContrast(fill, patternColor)}`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return contrastCheck
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Perform contrast check for a specific pattern against actual bar colors
|
|
110
|
+
const performContrastCheck = (patternKey: string, patternColor: string) => {
|
|
111
|
+
if (!patternColor || patternColor === '') return true
|
|
112
|
+
|
|
113
|
+
// Get the actual bar colors that the pattern will be overlaid on
|
|
114
|
+
let seriesColors: string[] = []
|
|
115
|
+
|
|
116
|
+
if (config.customColors && config.customColors.length > 0) {
|
|
117
|
+
// Use custom colors if available
|
|
118
|
+
seriesColors = config.customColors
|
|
119
|
+
} else {
|
|
120
|
+
// Use the same color generation logic as the chart
|
|
121
|
+
try {
|
|
122
|
+
const colorScale = getColorScale(config)
|
|
123
|
+
// Get colors for all series labels (not keys!)
|
|
124
|
+
const seriesLabels = config.runtime?.seriesLabelsAll || []
|
|
125
|
+
seriesColors = seriesLabels.map(label => colorScale(label)).filter(color => color !== null)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (seriesColors.length === 0) {
|
|
132
|
+
return true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check contrast against all series colors (bars that the pattern will overlay)
|
|
136
|
+
// Pattern should have good contrast against ALL bar colors it will appear on
|
|
137
|
+
let allContrastsPass = true
|
|
138
|
+
const contrastResults: Array<{ color: string; passes: boolean; ratio: number | false }> = []
|
|
139
|
+
|
|
140
|
+
seriesColors.forEach((barColor, index) => {
|
|
141
|
+
const contrastPasses = checkAndLogContrast(barColor, patternColor, patternKey, `series-${index}`)
|
|
142
|
+
const contrastRatio = getColorContrast(barColor, patternColor)
|
|
143
|
+
|
|
144
|
+
contrastResults.push({
|
|
145
|
+
color: barColor,
|
|
146
|
+
passes: contrastPasses,
|
|
147
|
+
ratio: contrastRatio
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (!contrastPasses) {
|
|
151
|
+
allContrastsPass = false
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
return allContrastsPass
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fieldOptions = getFieldOptions()
|
|
159
|
+
const currentPatterns: Record<string, LegendPattern> = legendCfg.patterns || {}
|
|
160
|
+
|
|
161
|
+
// Check if all patterns pass contrast requirements
|
|
162
|
+
const checkPatternContrasts = () => {
|
|
163
|
+
return Object.values(currentPatterns).every(pattern => pattern.contrastCheck !== false)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const handleAddPattern = () => {
|
|
167
|
+
const currentPatterns = legendCfg.patterns || {}
|
|
168
|
+
|
|
169
|
+
// For charts, we'll add a default pattern that users can configure
|
|
170
|
+
const newPatternKey = `Pattern${Object.keys(currentPatterns).length + 1}`
|
|
171
|
+
const defaultColor = '#000000'
|
|
172
|
+
const defaultDataKey = fieldOptions.length > 0 ? fieldOptions[0].value : ''
|
|
173
|
+
|
|
174
|
+
const newPatterns = {
|
|
175
|
+
...currentPatterns,
|
|
176
|
+
[newPatternKey]: {
|
|
177
|
+
label: newPatternKey,
|
|
178
|
+
color: defaultColor,
|
|
179
|
+
shape: 'circles' as const,
|
|
180
|
+
dataKey: defaultDataKey,
|
|
181
|
+
dataValue: '',
|
|
182
|
+
patternSize: 8, // Default pattern size
|
|
183
|
+
contrastCheck: performContrastCheck(newPatternKey, defaultColor)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
updateConfig({
|
|
188
|
+
...config,
|
|
189
|
+
legend: {
|
|
190
|
+
...(config.legend || {}),
|
|
191
|
+
patterns: newPatterns
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const handleRemovePattern = (patternKey: string) => {
|
|
197
|
+
const newPatterns = { ...(legendCfg.patterns || {}) }
|
|
198
|
+
delete newPatterns[patternKey]
|
|
199
|
+
|
|
200
|
+
updateConfig({
|
|
201
|
+
...config,
|
|
202
|
+
legend: {
|
|
203
|
+
...(config.legend || {}),
|
|
204
|
+
patterns: newPatterns
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const handlePatternKeyChange = (oldKey: string, newKey: string) => {
|
|
210
|
+
if (newKey === oldKey || !newKey.trim()) return
|
|
211
|
+
|
|
212
|
+
const currentPatterns = legendCfg.patterns || {}
|
|
213
|
+
const patternData = currentPatterns[oldKey]
|
|
214
|
+
|
|
215
|
+
if (!patternData) return
|
|
216
|
+
|
|
217
|
+
// Create new patterns object with updated key
|
|
218
|
+
const newPatterns = { ...currentPatterns }
|
|
219
|
+
delete newPatterns[oldKey]
|
|
220
|
+
newPatterns[newKey] = patternData
|
|
221
|
+
|
|
222
|
+
updateConfig({
|
|
223
|
+
...config,
|
|
224
|
+
legend: {
|
|
225
|
+
...(config.legend || {}),
|
|
226
|
+
patterns: newPatterns
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const handlePatternUpdate = (patternKey: string, field: string, value: any) => {
|
|
232
|
+
const updatedPattern = {
|
|
233
|
+
...(legendCfg.patterns?.[patternKey] || {}),
|
|
234
|
+
[field]: value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Perform contrast check if color is being updated
|
|
238
|
+
if (field === 'color') {
|
|
239
|
+
updatedPattern.contrastCheck = performContrastCheck(patternKey, value)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const newPatterns = {
|
|
243
|
+
...(legendCfg.patterns || {}),
|
|
244
|
+
[patternKey]: updatedPattern
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
updateConfig({
|
|
248
|
+
...config,
|
|
249
|
+
legend: {
|
|
250
|
+
...(config.legend || {}),
|
|
251
|
+
patterns: newPatterns
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<AccordionItem>
|
|
258
|
+
<AccordionItemHeading>
|
|
259
|
+
<AccordionItemButton>Pattern Settings</AccordionItemButton>
|
|
260
|
+
</AccordionItemHeading>
|
|
261
|
+
<AccordionItemPanel>
|
|
262
|
+
{Object.keys(currentPatterns).length > 0 && (
|
|
263
|
+
<>
|
|
264
|
+
<Alert
|
|
265
|
+
type={checkPatternContrasts() ? 'success' : 'danger'}
|
|
266
|
+
message='Pattern colors must comply with <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.'
|
|
267
|
+
showCloseButton={false}
|
|
268
|
+
/>
|
|
269
|
+
<br />
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Individual Pattern Configurations */}
|
|
274
|
+
{Object.entries(currentPatterns).map(([patternKey, pattern], index) => {
|
|
275
|
+
const p: LegendPattern = pattern || {}
|
|
276
|
+
const dataValueOptions = p.dataKey ? getDataValueOptions(p.dataKey) : []
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<Accordion allowZeroExpanded key={`pattern-accordion-${index}`}>
|
|
280
|
+
<AccordionItem>
|
|
281
|
+
<AccordionItemHeading>
|
|
282
|
+
<AccordionItemButton>
|
|
283
|
+
{p.dataKey && p.dataValue ? `${p.dataKey}: ${p.dataValue}` : `Pattern ${index + 1}`}
|
|
284
|
+
</AccordionItemButton>
|
|
285
|
+
</AccordionItemHeading>
|
|
286
|
+
<AccordionItemPanel>
|
|
287
|
+
{p.contrastCheck ?? true ? (
|
|
288
|
+
<Alert type='success' message='This pattern passes contrast checks' showCloseButton={false} />
|
|
289
|
+
) : (
|
|
290
|
+
<Alert
|
|
291
|
+
type='danger'
|
|
292
|
+
message='Error: <a href="https://webaim.org/resources/contrastchecker/" target="_blank"> Review Color Contrast</a>'
|
|
293
|
+
showCloseButton={false}
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
<label htmlFor={`pattern-datakey-${patternKey}`}>Data Key:</label>
|
|
298
|
+
<select
|
|
299
|
+
id={`pattern-datakey-${patternKey}`}
|
|
300
|
+
value={p.dataKey || 'Select'}
|
|
301
|
+
onChange={e => handlePatternUpdate(patternKey, 'dataKey', e.target.value)}
|
|
302
|
+
>
|
|
303
|
+
<option value='Select'>Select Data Key</option>
|
|
304
|
+
{fieldOptions.map((option, index) => (
|
|
305
|
+
<option value={option.value} key={index}>
|
|
306
|
+
{option.label}
|
|
307
|
+
</option>
|
|
308
|
+
))}
|
|
309
|
+
</select>
|
|
310
|
+
|
|
311
|
+
{p.dataKey && (
|
|
312
|
+
<>
|
|
313
|
+
<label htmlFor={`pattern-datavalue-${patternKey}`}>
|
|
314
|
+
Data Value:
|
|
315
|
+
<input
|
|
316
|
+
type='text'
|
|
317
|
+
id={`pattern-datavalue-${patternKey}`}
|
|
318
|
+
value={p.dataValue || ''}
|
|
319
|
+
onChange={e => handlePatternUpdate(patternKey, 'dataValue', e.target.value)}
|
|
320
|
+
placeholder='Enter data value'
|
|
321
|
+
/>
|
|
322
|
+
</label>
|
|
323
|
+
</>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
<label htmlFor={`pattern-label-${patternKey}`}>
|
|
327
|
+
Label (optional):
|
|
328
|
+
<input
|
|
329
|
+
type='text'
|
|
330
|
+
id={`pattern-label-${patternKey}`}
|
|
331
|
+
value={p.label || ''}
|
|
332
|
+
onChange={e => handlePatternUpdate(patternKey, 'label', e.target.value)}
|
|
333
|
+
/>
|
|
334
|
+
</label>
|
|
335
|
+
|
|
336
|
+
<label htmlFor={`pattern-type-${patternKey}`}>Pattern Type:</label>
|
|
337
|
+
<select
|
|
338
|
+
id={`pattern-type-${patternKey}`}
|
|
339
|
+
value={p.shape || 'circles'}
|
|
340
|
+
onChange={e => handlePatternUpdate(patternKey, 'shape', e.target.value)}
|
|
341
|
+
>
|
|
342
|
+
{patternTypes.map((patternType, index) => (
|
|
343
|
+
<option value={patternType.value} key={index}>
|
|
344
|
+
{patternType.label}
|
|
345
|
+
</option>
|
|
346
|
+
))}
|
|
347
|
+
</select>
|
|
348
|
+
|
|
349
|
+
<label htmlFor={`pattern-size-${patternKey}`}>Pattern Size:</label>
|
|
350
|
+
<select
|
|
351
|
+
id={`pattern-size-${patternKey}`}
|
|
352
|
+
value={getPatternSizeText(p.patternSize || 8)}
|
|
353
|
+
onChange={e =>
|
|
354
|
+
handlePatternUpdate(patternKey, 'patternSize', getPatternSizeNumeric(e.target.value))
|
|
355
|
+
}
|
|
356
|
+
>
|
|
357
|
+
{patternSizes.map((size, index) => (
|
|
358
|
+
<option value={size.value} key={index}>
|
|
359
|
+
{size.label}
|
|
360
|
+
</option>
|
|
361
|
+
))}
|
|
362
|
+
</select>
|
|
363
|
+
|
|
364
|
+
<div className='mt-3'>
|
|
365
|
+
<label htmlFor={`pattern-color-${patternKey}`}>
|
|
366
|
+
Pattern Color
|
|
367
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
368
|
+
<Tooltip.Target>
|
|
369
|
+
<Icon
|
|
370
|
+
display='question'
|
|
371
|
+
style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
|
|
372
|
+
/>
|
|
373
|
+
</Tooltip.Target>
|
|
374
|
+
<Tooltip.Content>
|
|
375
|
+
<p>
|
|
376
|
+
If this setting is used, it is the responsibility of the visualization author to verify the
|
|
377
|
+
visualization colors meet WCAG 3:1 contrast ratios.
|
|
378
|
+
</p>
|
|
379
|
+
</Tooltip.Content>
|
|
380
|
+
</Tooltip>
|
|
381
|
+
<input
|
|
382
|
+
type='text'
|
|
383
|
+
value={p.color || '#666666'}
|
|
384
|
+
id={`pattern-color-${patternKey}`}
|
|
385
|
+
onChange={e => handlePatternUpdate(patternKey, 'color', e.target.value)}
|
|
386
|
+
placeholder='#666666'
|
|
387
|
+
/>
|
|
388
|
+
</label>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<Button onClick={() => handleRemovePattern(patternKey)} className='btn btn-danger'>
|
|
392
|
+
Remove Pattern
|
|
393
|
+
</Button>
|
|
394
|
+
</AccordionItemPanel>
|
|
395
|
+
</AccordionItem>
|
|
396
|
+
</Accordion>
|
|
397
|
+
)
|
|
398
|
+
})}
|
|
399
|
+
|
|
400
|
+
{/* Add Pattern Button */}
|
|
401
|
+
<button className='btn btn-primary full-width mt-2' onClick={handleAddPattern}>
|
|
402
|
+
Add Pattern
|
|
403
|
+
</button>
|
|
404
|
+
|
|
405
|
+
{Object.keys(currentPatterns).length === 0 && (
|
|
406
|
+
<p style={{ color: '#666', fontStyle: 'italic' }}>
|
|
407
|
+
No patterns configured. Use "Add Pattern" to create pattern configurations.
|
|
408
|
+
</p>
|
|
409
|
+
)}
|
|
410
|
+
</AccordionItemPanel>
|
|
411
|
+
</AccordionItem>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
export default PanelPatternSettings
|
|
@@ -5,9 +5,12 @@ import ConfigContext from '../../../../ConfigContext'
|
|
|
5
5
|
import InputSelect from '@cdc/core/components/inputs/InputSelect'
|
|
6
6
|
import Check from '@cdc/core/assets/icon-check.svg'
|
|
7
7
|
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'
|
|
9
11
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
10
12
|
import { Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
13
|
+
import { buildForecastPaletteOptions } from '../../../../helpers/buildForecastPaletteOptions'
|
|
11
14
|
|
|
12
15
|
// Third Party
|
|
13
16
|
import {
|
|
@@ -25,7 +28,7 @@ const SeriesContext = React.createContext({})
|
|
|
25
28
|
const SeriesWrapper = props => {
|
|
26
29
|
const { updateConfig, config, rawData } = useContext(ConfigContext)
|
|
27
30
|
|
|
28
|
-
const { getColumns, selectComponent } = props
|
|
31
|
+
const { getColumns, selectComponent, handleForecastPaletteSelection } = props
|
|
29
32
|
|
|
30
33
|
const supportedRightAxisTypes = ['Line', 'dashed-sm', 'dashed-md', 'dashed-lg']
|
|
31
34
|
|
|
@@ -57,7 +60,9 @@ const SeriesWrapper = props => {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
return (
|
|
60
|
-
<SeriesContext.Provider
|
|
63
|
+
<SeriesContext.Provider
|
|
64
|
+
value={{ updateSeries, supportedRightAxisTypes, getColumns, selectComponent, handleForecastPaletteSelection }}
|
|
65
|
+
>
|
|
61
66
|
{props.children}
|
|
62
67
|
</SeriesContext.Provider>
|
|
63
68
|
)
|
|
@@ -240,7 +245,8 @@ const SeriesDropdownAxisPosition = props => {
|
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
const SeriesDropdownForecastColor = props => {
|
|
243
|
-
const { config
|
|
248
|
+
const { config } = useContext(ConfigContext)
|
|
249
|
+
const { handleForecastPaletteSelection } = useContext(SeriesContext)
|
|
244
250
|
|
|
245
251
|
const { index, series } = props
|
|
246
252
|
|
|
@@ -249,30 +255,32 @@ const SeriesDropdownForecastColor = props => {
|
|
|
249
255
|
// Hide AxisPositionDropdown in certain cases.
|
|
250
256
|
if (!series) return
|
|
251
257
|
|
|
252
|
-
|
|
253
|
-
|
|
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)
|
|
255
271
|
|
|
256
272
|
return series?.stages?.map((stage, stageIndex) => (
|
|
257
273
|
<InputSelect
|
|
258
274
|
key={`${stage}--${stageIndex}`}
|
|
259
275
|
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'}
|
|
263
277
|
label={`${stage.key} Series Color`}
|
|
264
278
|
onChange={event => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
274
282
|
}}
|
|
275
|
-
options={
|
|
283
|
+
options={paletteOptions}
|
|
276
284
|
/>
|
|
277
285
|
))
|
|
278
286
|
}
|