@cdc/chart 4.25.8 → 4.25.11
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-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +44236 -40355
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/DEV-11825.json +573 -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/na.json +913 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/private/test-data.csv +28 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +2 -133
- package/package.json +25 -7
- package/src/CdcChart.tsx +9 -13
- package/src/CdcChartComponent.tsx +403 -92
- 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.Combo.stories.tsx +18 -0
- 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.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +20 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +7 -4
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +59 -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/combo.json +451 -0
- package/src/_stories/_mock/editor-test-configs.json +376 -0
- package/src/_stories/_mock/editor-test-datasets.json +477 -0
- package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
- package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
- package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
- package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
- package/src/_stories/_mock/pie_config.json +257 -62
- package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
- package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
- package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
- 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/Annotations/components/findNearestDatum.ts +6 -41
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +161 -22
- 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 +155 -22
- 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/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +563 -229
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +461 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
- package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +175 -27
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +114 -14
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +38 -15
- package/src/components/LinearChart.tsx +96 -84
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
- package/src/components/SmallMultiples/index.ts +2 -0
- package/src/data/initial-state.js +327 -293
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +71 -0
- package/src/helpers/getColorScale.ts +82 -8
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +116 -29
- package/src/index.jsx +0 -2
- package/src/scss/main.scss +13 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +5 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +53 -11
- package/src/types/ChartContext.ts +4 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
- package/src/hooks/useColorPalette.js +0 -76
|
@@ -0,0 +1,461 @@
|
|
|
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 { Select } from '@cdc/core/components/EditorPanel/Inputs'
|
|
14
|
+
import ConfigContext from '../../../../ConfigContext'
|
|
15
|
+
import { ChartContext } from '../../../../types/ChartContext'
|
|
16
|
+
import { PanelProps } from '../PanelProps'
|
|
17
|
+
import { checkColorContrast, getColorContrast } from '@cdc/core/helpers/cove/accessibility'
|
|
18
|
+
import { getColorScale } from '../../../../helpers/getColorScale'
|
|
19
|
+
import _ from 'lodash'
|
|
20
|
+
|
|
21
|
+
const PanelPatternSettings: FC<PanelProps> = props => {
|
|
22
|
+
const { config, updateConfig, transformedData } = useContext<ChartContext>(ConfigContext)
|
|
23
|
+
|
|
24
|
+
type LegendPattern = {
|
|
25
|
+
label?: string
|
|
26
|
+
color?: string
|
|
27
|
+
shape?: 'circles' | 'lines' | 'diagonalLines' | 'waves'
|
|
28
|
+
dataKey?: string
|
|
29
|
+
dataValue?: string
|
|
30
|
+
contrastCheck?: boolean
|
|
31
|
+
patternSize?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Safe legend reference with defaults to avoid crashes when legend is undefined
|
|
35
|
+
const legendCfg = (config.legend || { patterns: {} }) as {
|
|
36
|
+
patterns: Record<string, LegendPattern>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const patternTypes = [
|
|
40
|
+
{ value: 'circles', label: 'Circles' },
|
|
41
|
+
{ value: 'lines', label: 'Horizontal Lines' },
|
|
42
|
+
{ value: 'diagonalLines', label: 'Diagonal Lines' },
|
|
43
|
+
{ value: 'waves', label: 'Waves' }
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const patternSizes = [
|
|
47
|
+
{ value: 'small', label: 'Small' },
|
|
48
|
+
{ value: 'medium', label: 'Medium' },
|
|
49
|
+
{ value: 'large', label: 'Large' }
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
// Convert numeric size to text and vice versa
|
|
53
|
+
const getPatternSizeText = (numericSize: number): string => {
|
|
54
|
+
if (numericSize <= 6) return 'small'
|
|
55
|
+
if (numericSize <= 12) return 'medium'
|
|
56
|
+
return 'large'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getPatternSizeNumeric = (textSize: string): number => {
|
|
60
|
+
switch (textSize) {
|
|
61
|
+
case 'small':
|
|
62
|
+
return 6
|
|
63
|
+
case 'medium':
|
|
64
|
+
return 10
|
|
65
|
+
case 'large':
|
|
66
|
+
return 16
|
|
67
|
+
default:
|
|
68
|
+
return 8
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get unique values from a specific data field for dropdown options
|
|
73
|
+
const getDataValueOptions = (dataKey: string) => {
|
|
74
|
+
if (!dataKey || !Array.isArray(transformedData) || transformedData.length === 0) {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const uniqueValues = Array.from(new Set(transformedData.map(row => row[dataKey])))
|
|
79
|
+
.filter(val => val !== undefined && val !== null && val !== '')
|
|
80
|
+
.sort()
|
|
81
|
+
|
|
82
|
+
return uniqueValues.map(value => ({ value: String(value), label: String(value) }))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const getFieldOptions = () => {
|
|
86
|
+
if (!Array.isArray(transformedData) || transformedData.length === 0) return []
|
|
87
|
+
|
|
88
|
+
const firstRow = transformedData[0] || {}
|
|
89
|
+
return Object.keys(firstRow).map(key => ({ value: key, label: key }))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Checks contrast and logs warning if needed
|
|
93
|
+
const checkAndLogContrast = (
|
|
94
|
+
patternColor: string,
|
|
95
|
+
backgroundColor: string,
|
|
96
|
+
dataValue: string,
|
|
97
|
+
dataKey: string
|
|
98
|
+
): boolean => {
|
|
99
|
+
if (!backgroundColor || !patternColor) return true // Default to true if colors are missing
|
|
100
|
+
|
|
101
|
+
const contrastCheck = checkColorContrast(patternColor, backgroundColor)
|
|
102
|
+
|
|
103
|
+
if (!contrastCheck) {
|
|
104
|
+
console.error(
|
|
105
|
+
`COVE: pattern contrast check failed for ${dataValue} in ${dataKey} with:
|
|
106
|
+
pattern color: ${patternColor}
|
|
107
|
+
background color: ${backgroundColor}
|
|
108
|
+
contrast: ${getColorContrast(patternColor, backgroundColor)}`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return contrastCheck
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Perform contrast check for a specific pattern against actual bar colors
|
|
116
|
+
const performContrastCheck = (patternKey: string, patternColor: string) => {
|
|
117
|
+
if (!patternColor || patternColor === '') {
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get the actual bar colors that the pattern will be overlaid on
|
|
122
|
+
let seriesColors: string[] = []
|
|
123
|
+
|
|
124
|
+
if (config.customColors && config.customColors.length > 0) {
|
|
125
|
+
// Use custom colors if available
|
|
126
|
+
seriesColors = config.customColors
|
|
127
|
+
} else {
|
|
128
|
+
// Use the same color generation logic as the chart
|
|
129
|
+
try {
|
|
130
|
+
const colorScale = getColorScale(config)
|
|
131
|
+
// Get colors for all series labels (not keys!)
|
|
132
|
+
const seriesLabels = config.runtime?.seriesLabelsAll || []
|
|
133
|
+
seriesColors = seriesLabels.map(label => colorScale(label)).filter(color => color !== null)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (seriesColors.length === 0) {
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check contrast against all series colors (bars that the pattern will overlay)
|
|
144
|
+
// Pattern should have good contrast against ALL bar colors it will appear on
|
|
145
|
+
let allContrastsPass = true
|
|
146
|
+
const contrastResults: Array<{ color: string; passes: boolean; ratio: number | false }> = []
|
|
147
|
+
|
|
148
|
+
seriesColors.forEach((barColor, index) => {
|
|
149
|
+
const contrastPasses = checkAndLogContrast(patternColor, barColor, patternKey, `series-${index}`)
|
|
150
|
+
const contrastRatio = getColorContrast(patternColor, barColor)
|
|
151
|
+
|
|
152
|
+
contrastResults.push({
|
|
153
|
+
color: barColor,
|
|
154
|
+
passes: contrastPasses,
|
|
155
|
+
ratio: contrastRatio
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
if (!contrastPasses) {
|
|
159
|
+
allContrastsPass = false
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return allContrastsPass
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fieldOptions = getFieldOptions()
|
|
167
|
+
const currentPatterns: Record<string, LegendPattern> = legendCfg.patterns || {}
|
|
168
|
+
|
|
169
|
+
// Check if all patterns pass contrast requirements
|
|
170
|
+
const checkPatternContrasts = () => {
|
|
171
|
+
return Object.values(currentPatterns).every(pattern => pattern.contrastCheck !== false)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleAddPattern = () => {
|
|
175
|
+
const currentPatterns = legendCfg.patterns || {}
|
|
176
|
+
|
|
177
|
+
// For charts, we'll add a default pattern that users can configure
|
|
178
|
+
const newPatternKey = `Pattern${Object.keys(currentPatterns).length + 1}`
|
|
179
|
+
const defaultColor = '#000000'
|
|
180
|
+
const defaultDataKey = fieldOptions.length > 0 ? fieldOptions[0].value : ''
|
|
181
|
+
|
|
182
|
+
const newPatterns = {
|
|
183
|
+
...currentPatterns,
|
|
184
|
+
[newPatternKey]: {
|
|
185
|
+
label: newPatternKey,
|
|
186
|
+
color: defaultColor,
|
|
187
|
+
shape: 'circles' as const,
|
|
188
|
+
dataKey: defaultDataKey,
|
|
189
|
+
dataValue: '',
|
|
190
|
+
patternSize: 8, // Default pattern size
|
|
191
|
+
contrastCheck: performContrastCheck(newPatternKey, defaultColor)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const updatedConfig = {
|
|
196
|
+
...config,
|
|
197
|
+
legend: {
|
|
198
|
+
...(config.legend || {}),
|
|
199
|
+
patterns: newPatterns
|
|
200
|
+
},
|
|
201
|
+
runtime: {
|
|
202
|
+
...config.runtime
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if all patterns pass and set error message
|
|
207
|
+
const allPatternsPass = Object.values(newPatterns).every((p: any) => p.contrastCheck !== false)
|
|
208
|
+
updatedConfig.runtime.editorErrorMessage = allPatternsPass
|
|
209
|
+
? ''
|
|
210
|
+
: 'One or more patterns do not pass the WCAG 2.1 contrast ratio of 3:1.'
|
|
211
|
+
|
|
212
|
+
updateConfig(updatedConfig)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handleRemovePattern = (patternKey: string) => {
|
|
216
|
+
const newPatterns = { ...(legendCfg.patterns || {}) }
|
|
217
|
+
delete newPatterns[patternKey]
|
|
218
|
+
|
|
219
|
+
const updatedConfig = {
|
|
220
|
+
...config,
|
|
221
|
+
legend: {
|
|
222
|
+
...(config.legend || {}),
|
|
223
|
+
patterns: newPatterns
|
|
224
|
+
},
|
|
225
|
+
runtime: {
|
|
226
|
+
...config.runtime
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check if all remaining patterns pass and clear error message if needed
|
|
231
|
+
const allPatternsPass = Object.values(newPatterns).every((p: any) => p.contrastCheck !== false)
|
|
232
|
+
if (allPatternsPass || Object.keys(newPatterns).length === 0) {
|
|
233
|
+
updatedConfig.runtime.editorErrorMessage = ''
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
updateConfig(updatedConfig)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const handlePatternKeyChange = (oldKey: string, newKey: string) => {
|
|
240
|
+
if (newKey === oldKey || !newKey.trim()) return
|
|
241
|
+
|
|
242
|
+
const currentPatterns = legendCfg.patterns || {}
|
|
243
|
+
const patternData = currentPatterns[oldKey]
|
|
244
|
+
|
|
245
|
+
if (!patternData) return
|
|
246
|
+
|
|
247
|
+
// Create new patterns object with updated key
|
|
248
|
+
const newPatterns = { ...currentPatterns }
|
|
249
|
+
delete newPatterns[oldKey]
|
|
250
|
+
newPatterns[newKey] = patternData
|
|
251
|
+
|
|
252
|
+
updateConfig({
|
|
253
|
+
...config,
|
|
254
|
+
legend: {
|
|
255
|
+
...(config.legend || {}),
|
|
256
|
+
patterns: newPatterns
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const reviewColorContrast = (updatedConfig: any, patternKey: string) => {
|
|
262
|
+
// Re-check the contrast for the updated pattern
|
|
263
|
+
const pattern = updatedConfig.legend.patterns[patternKey]
|
|
264
|
+
|
|
265
|
+
if (pattern?.color) {
|
|
266
|
+
pattern.contrastCheck = performContrastCheck(patternKey, pattern.color)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Update error message based on whether all patterns pass contrast checks
|
|
270
|
+
const allPatterns = Object.values(updatedConfig.legend.patterns || {})
|
|
271
|
+
|
|
272
|
+
const allPatternsPass = allPatterns.every((p: any) => p.contrastCheck !== false)
|
|
273
|
+
|
|
274
|
+
const errorMsg = allPatternsPass ? '' : 'One or more patterns do not pass the WCAG 2.1 contrast ratio of 3:1.'
|
|
275
|
+
// Set error message AFTER spreading runtime to avoid it being overwritten
|
|
276
|
+
updatedConfig.runtime.editorErrorMessage = errorMsg
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const handlePatternUpdate = (patternKey: string, field: string, value: any) => {
|
|
280
|
+
const updatedPattern = {
|
|
281
|
+
...(legendCfg.patterns?.[patternKey] || {}),
|
|
282
|
+
[field]: value
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Clear dataValue if dataKey is being cleared or set to 'Select'
|
|
286
|
+
if (field === 'dataKey' && (value === 'Select' || value === '')) {
|
|
287
|
+
updatedPattern.dataValue = ''
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const newPatterns = {
|
|
291
|
+
...(legendCfg.patterns || {}),
|
|
292
|
+
[patternKey]: updatedPattern
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const updatedConfig = {
|
|
296
|
+
...config,
|
|
297
|
+
legend: {
|
|
298
|
+
...(config.legend || {}),
|
|
299
|
+
patterns: newPatterns
|
|
300
|
+
},
|
|
301
|
+
runtime: {
|
|
302
|
+
...config.runtime
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Perform contrast check whenever color changes (even if cleared)
|
|
307
|
+
if (field === 'color') {
|
|
308
|
+
reviewColorContrast(updatedConfig, patternKey)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
updateConfig(updatedConfig)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<AccordionItem>
|
|
316
|
+
<AccordionItemHeading>
|
|
317
|
+
<AccordionItemButton>Pattern Settings</AccordionItemButton>
|
|
318
|
+
</AccordionItemHeading>
|
|
319
|
+
<AccordionItemPanel>
|
|
320
|
+
{Object.keys(currentPatterns).length > 0 && (
|
|
321
|
+
<>
|
|
322
|
+
<Alert
|
|
323
|
+
type={checkPatternContrasts() ? 'success' : 'danger'}
|
|
324
|
+
message='Pattern colors must comply with <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.'
|
|
325
|
+
showCloseButton={false}
|
|
326
|
+
/>
|
|
327
|
+
<br />
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{/* Individual Pattern Configurations */}
|
|
332
|
+
{Object.entries(currentPatterns).map(([patternKey, pattern], index) => {
|
|
333
|
+
const p: LegendPattern = pattern || {}
|
|
334
|
+
const dataValueOptions = p.dataKey ? getDataValueOptions(p.dataKey) : []
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<Accordion allowZeroExpanded key={`pattern-accordion-${index}`}>
|
|
338
|
+
<AccordionItem>
|
|
339
|
+
<AccordionItemHeading>
|
|
340
|
+
<AccordionItemButton>
|
|
341
|
+
{p.dataKey && p.dataValue ? `${p.dataKey}: ${p.dataValue}` : `Pattern ${index + 1}`}
|
|
342
|
+
</AccordionItemButton>
|
|
343
|
+
</AccordionItemHeading>
|
|
344
|
+
<AccordionItemPanel>
|
|
345
|
+
{p.contrastCheck ?? true ? (
|
|
346
|
+
<Alert type='success' message='This pattern passes contrast checks' showCloseButton={false} />
|
|
347
|
+
) : (
|
|
348
|
+
<Alert
|
|
349
|
+
type='danger'
|
|
350
|
+
message='Error: <a href="https://webaim.org/resources/contrastchecker/" target="_blank"> Review Color Contrast</a>'
|
|
351
|
+
showCloseButton={false}
|
|
352
|
+
/>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
<Select
|
|
356
|
+
label='Data Key:'
|
|
357
|
+
value={p.dataKey || ''}
|
|
358
|
+
options={fieldOptions}
|
|
359
|
+
initial='Select Data Key'
|
|
360
|
+
fieldName={`pattern-datakey-${patternKey}`}
|
|
361
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
362
|
+
handlePatternUpdate(patternKey, 'dataKey', value)
|
|
363
|
+
}
|
|
364
|
+
/>
|
|
365
|
+
|
|
366
|
+
{p.dataKey && (
|
|
367
|
+
<>
|
|
368
|
+
<label htmlFor={`pattern-datavalue-${patternKey}`}>
|
|
369
|
+
Data Value:
|
|
370
|
+
<input
|
|
371
|
+
type='text'
|
|
372
|
+
id={`pattern-datavalue-${patternKey}`}
|
|
373
|
+
value={p.dataValue || ''}
|
|
374
|
+
onChange={e => handlePatternUpdate(patternKey, 'dataValue', e.target.value)}
|
|
375
|
+
placeholder='Enter data value'
|
|
376
|
+
/>
|
|
377
|
+
</label>
|
|
378
|
+
</>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
<label htmlFor={`pattern-label-${patternKey}`}>
|
|
382
|
+
Label (optional):
|
|
383
|
+
<input
|
|
384
|
+
type='text'
|
|
385
|
+
id={`pattern-label-${patternKey}`}
|
|
386
|
+
value={p.label || ''}
|
|
387
|
+
onChange={e => handlePatternUpdate(patternKey, 'label', e.target.value)}
|
|
388
|
+
/>
|
|
389
|
+
</label>
|
|
390
|
+
|
|
391
|
+
<Select
|
|
392
|
+
label='Pattern Type:'
|
|
393
|
+
value={p.shape || 'circles'}
|
|
394
|
+
options={patternTypes}
|
|
395
|
+
fieldName={`pattern-type-${patternKey}`}
|
|
396
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
397
|
+
handlePatternUpdate(patternKey, 'shape', value)
|
|
398
|
+
}
|
|
399
|
+
/>
|
|
400
|
+
|
|
401
|
+
<Select
|
|
402
|
+
label='Pattern Size:'
|
|
403
|
+
value={getPatternSizeText(p.patternSize || 8)}
|
|
404
|
+
options={patternSizes}
|
|
405
|
+
fieldName={`pattern-size-${patternKey}`}
|
|
406
|
+
updateField={(section, subsection, fieldName, value) =>
|
|
407
|
+
handlePatternUpdate(patternKey, 'patternSize', getPatternSizeNumeric(value))
|
|
408
|
+
}
|
|
409
|
+
/>
|
|
410
|
+
|
|
411
|
+
<div className='mt-3'>
|
|
412
|
+
<label htmlFor={`pattern-color-${patternKey}`}>
|
|
413
|
+
Pattern Color
|
|
414
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
415
|
+
<Tooltip.Target>
|
|
416
|
+
<Icon
|
|
417
|
+
display='question'
|
|
418
|
+
style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
|
|
419
|
+
/>
|
|
420
|
+
</Tooltip.Target>
|
|
421
|
+
<Tooltip.Content>
|
|
422
|
+
<p>
|
|
423
|
+
If this setting is used, it is the responsibility of the visualization author to verify the
|
|
424
|
+
visualization colors meet WCAG 3:1 contrast ratios.
|
|
425
|
+
</p>
|
|
426
|
+
</Tooltip.Content>
|
|
427
|
+
</Tooltip>
|
|
428
|
+
<input
|
|
429
|
+
type='text'
|
|
430
|
+
value={p.color || ''}
|
|
431
|
+
id={`pattern-color-${patternKey}`}
|
|
432
|
+
onChange={e => handlePatternUpdate(patternKey, 'color', e.target.value)}
|
|
433
|
+
placeholder='#666666'
|
|
434
|
+
/>
|
|
435
|
+
</label>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
<Button onClick={() => handleRemovePattern(patternKey)} className='btn btn-danger'>
|
|
439
|
+
Remove Pattern
|
|
440
|
+
</Button>
|
|
441
|
+
</AccordionItemPanel>
|
|
442
|
+
</AccordionItem>
|
|
443
|
+
</Accordion>
|
|
444
|
+
)
|
|
445
|
+
})}
|
|
446
|
+
|
|
447
|
+
{/* Add Pattern Button */}
|
|
448
|
+
<button className='btn btn-primary full-width mt-2' onClick={handleAddPattern}>
|
|
449
|
+
Add Pattern
|
|
450
|
+
</button>
|
|
451
|
+
|
|
452
|
+
{Object.keys(currentPatterns).length === 0 && (
|
|
453
|
+
<p style={{ color: '#666', fontStyle: 'italic' }}>
|
|
454
|
+
No patterns configured. Use "Add Pattern" to create pattern configurations.
|
|
455
|
+
</p>
|
|
456
|
+
)}
|
|
457
|
+
</AccordionItemPanel>
|
|
458
|
+
</AccordionItem>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
export default PanelPatternSettings
|