@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.
Files changed (145) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  3. package/dist/cdcchart.js +44236 -40355
  4. package/examples/feature/__data__/planet-example-data.json +0 -30
  5. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  6. package/examples/grouped-bar-test.json +400 -0
  7. package/examples/private/DEV-11825.json +573 -0
  8. package/examples/private/d.json +382 -0
  9. package/examples/private/example-2.json +49784 -0
  10. package/examples/private/f2.json +1 -0
  11. package/examples/private/f4.json +1577 -0
  12. package/examples/private/forecast.json +1180 -0
  13. package/examples/private/lollipop.json +468 -0
  14. package/examples/private/na.json +913 -0
  15. package/examples/private/new.json +48756 -0
  16. package/examples/private/pie-chart-legend.json +904 -0
  17. package/examples/private/test-data.csv +28 -0
  18. package/examples/suppressed_tooltip.json +480 -0
  19. package/index.html +2 -133
  20. package/package.json +25 -7
  21. package/src/CdcChart.tsx +9 -13
  22. package/src/CdcChartComponent.tsx +403 -92
  23. package/src/_stories/Chart.Anchors.stories.tsx +2 -2
  24. package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
  25. package/src/_stories/Chart.CI.stories.tsx +1 -1
  26. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  27. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  28. package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
  29. package/src/_stories/Chart.Filters.stories.tsx +2 -2
  30. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  31. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  32. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  33. package/src/_stories/Chart.Patterns.stories.tsx +20 -0
  34. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  37. package/src/_stories/Chart.stories.tsx +8 -5
  38. package/src/_stories/Chart.tooltip.stories.tsx +1 -1
  39. package/src/_stories/ChartAnnotation.stories.tsx +7 -4
  40. package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
  41. package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
  42. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  43. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  44. package/src/_stories/ChartEditor.stories.tsx +59 -60
  45. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  46. package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
  47. package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -2
  48. package/src/_stories/_mock/combo.json +451 -0
  49. package/src/_stories/_mock/editor-test-configs.json +376 -0
  50. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  51. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  52. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  53. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  54. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  55. package/src/_stories/_mock/pie_config.json +257 -62
  56. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  57. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  58. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  59. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  60. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  61. package/src/_stories/_mock/stacked-pattern-test.json +520 -0
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
  63. package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
  64. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  65. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  66. package/src/components/AreaChart/index.tsx +1 -2
  67. package/src/components/BarChart/components/BarChart.Horizontal.tsx +161 -22
  68. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
  69. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
  70. package/src/components/BarChart/components/BarChart.Vertical.tsx +155 -22
  71. package/src/components/BarChart/helpers/index.ts +43 -4
  72. package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
  73. package/src/components/BarChart/helpers/useBarChart.ts +25 -3
  74. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
  75. package/src/components/BoxPlot/helpers/index.ts +3 -3
  76. package/src/components/Brush/BrushChart.tsx +1 -1
  77. package/src/components/DeviationBar.jsx +9 -6
  78. package/src/components/EditorPanel/EditorPanel.tsx +563 -229
  79. package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
  80. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  81. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  82. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +461 -0
  83. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
  84. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
  86. package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
  87. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
  88. package/src/components/EditorPanel/editor-panel.scss +0 -20
  89. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
  90. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  91. package/src/components/Forecasting/Forecasting.tsx +175 -27
  92. package/src/components/ForestPlot/ForestPlot.tsx +11 -7
  93. package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
  94. package/src/components/Legend/Legend.Component.tsx +114 -14
  95. package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
  96. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  97. package/src/components/LegendWrapper.tsx +1 -1
  98. package/src/components/LineChart/LineChartProps.ts +0 -3
  99. package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
  100. package/src/components/LineChart/helpers.ts +1 -1
  101. package/src/components/LineChart/index.tsx +38 -15
  102. package/src/components/LinearChart.tsx +96 -84
  103. package/src/components/PairedBarChart.jsx +6 -4
  104. package/src/components/PieChart/PieChart.tsx +170 -54
  105. package/src/components/Regions/components/Regions.tsx +3 -24
  106. package/src/components/Sankey/components/Sankey.tsx +7 -1
  107. package/src/components/Sankey/types/index.ts +1 -1
  108. package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
  109. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  110. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  111. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  112. package/src/components/SmallMultiples/index.ts +2 -0
  113. package/src/data/initial-state.js +327 -293
  114. package/src/helpers/buildForecastPaletteMappings.ts +112 -0
  115. package/src/helpers/buildForecastPaletteOptions.ts +71 -0
  116. package/src/helpers/getColorScale.ts +82 -8
  117. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  118. package/src/helpers/getNewRuntime.ts +1 -1
  119. package/src/helpers/getTransformedData.ts +1 -1
  120. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  121. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  122. package/src/hooks/useChartHoverAnalytics.tsx +44 -0
  123. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  124. package/src/hooks/useReduceData.ts +105 -70
  125. package/src/hooks/useScales.ts +88 -34
  126. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  127. package/src/hooks/useTooltip.tsx +116 -29
  128. package/src/index.jsx +0 -2
  129. package/src/scss/main.scss +13 -80
  130. package/src/store/chart.actions.ts +2 -0
  131. package/src/store/chart.reducer.ts +5 -1
  132. package/src/test/CdcChart.test.jsx +8 -3
  133. package/src/types/ChartConfig.ts +53 -11
  134. package/src/types/ChartContext.ts +4 -0
  135. package/vite.config.js +1 -1
  136. package/vitest.config.ts +16 -0
  137. package/src/_stories/_mock/pie_data.json +0 -218
  138. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  139. package/src/coreStyles_chart.scss +0 -3
  140. package/src/helpers/configHelpers.ts +0 -28
  141. package/src/helpers/generateColorsArray.ts +0 -8
  142. package/src/helpers/sort.ts +0 -7
  143. package/src/hooks/useActiveElement.js +0 -19
  144. package/src/hooks/useChartClasses.js +0 -41
  145. 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