@cdc/chart 4.26.1 → 4.26.3

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 (173) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  4. package/dist/cdcchart.js +54742 -49796
  5. package/examples/data/data-with-metadata.json +10 -0
  6. package/examples/default.json +378 -0
  7. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  8. package/examples/feature/annotations/index.json +3 -6
  9. package/examples/feature/horizon/horizon-chart.json +395 -0
  10. package/examples/feature/pie/planet-pie-example-config.json +2 -1
  11. package/examples/line-chart-states.json +1085 -0
  12. package/examples/metadata-variables.json +58 -0
  13. package/examples/private/123.json +694 -0
  14. package/examples/private/anchor-issue.json +4094 -0
  15. package/examples/private/backwards-slider.json +10430 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/timeline-data.json +1 -0
  18. package/examples/private/timeline.json +389 -0
  19. package/examples/radar-chart-simple.json +133 -0
  20. package/examples/radar-chart.json +148 -0
  21. package/index.html +1 -31
  22. package/package.json +57 -59
  23. package/src/CdcChart.tsx +8 -4
  24. package/src/CdcChartComponent.tsx +398 -284
  25. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  26. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  27. package/src/_stories/Chart.CI.stories.tsx +13 -0
  28. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  29. package/src/_stories/Chart.CustomColors.stories.tsx +78 -0
  30. package/src/_stories/Chart.Defaults.stories.tsx +95 -0
  31. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  32. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  33. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  34. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  35. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  36. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  37. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  38. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  39. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  40. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  41. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  42. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  43. package/src/_stories/Chart.SmallestLeftAxisMax.stories.tsx +64 -0
  44. package/src/_stories/Chart.stories.tsx +72 -1
  45. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  46. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  47. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  48. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  49. package/src/_stories/ChartBar.Editor.stories.tsx +97 -38
  50. package/src/_stories/ChartBrush.Editor.stories.tsx +11 -25
  51. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  52. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  53. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  54. package/src/_stories/ChartBrush.stories.tsx +7 -0
  55. package/src/_stories/ChartEditor.Editor.stories.tsx +1 -1
  56. package/src/_stories/ChartEditor.stories.tsx +7 -0
  57. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  58. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  59. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  60. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  61. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  62. package/src/_stories/_mock/brush_continuous.json +86 -0
  63. package/src/_stories/_mock/brush_date_large.json +176 -0
  64. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  65. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  66. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  67. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  68. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  69. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  70. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  71. package/src/_stories/_mock/paired-bar-abbr.json +421 -0
  72. package/src/_stories/_mock/pie_custom_colors.json +268 -0
  73. package/src/_stories/_mock/smallest_left_axis_max.json +104 -0
  74. package/src/components/Annotations/components/AnnotationDraggable.styles.css +14 -20
  75. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  76. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  77. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  78. package/src/components/Annotations/components/AnnotationList.styles.css +12 -18
  79. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  80. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  81. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  82. package/src/components/Axis/BottomAxis.tsx +277 -0
  83. package/src/components/Axis/LeftAxis.tsx +404 -0
  84. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  85. package/src/components/Axis/PairedBarAxis.tsx +192 -0
  86. package/src/components/Axis/README.md +94 -0
  87. package/src/components/Axis/RightAxis.tsx +108 -0
  88. package/src/components/Axis/axis.constants.ts +21 -0
  89. package/src/components/Axis/index.ts +7 -0
  90. package/src/components/BarChart/components/BarChart.Horizontal.tsx +12 -28
  91. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -30
  92. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +12 -31
  93. package/src/components/BarChart/components/BarChart.Vertical.tsx +12 -28
  94. package/src/components/BarChart/components/BarChart.tsx +7 -1
  95. package/src/components/BarChart/helpers/getPatternUrl.ts +94 -0
  96. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +134 -0
  97. package/src/components/BarChart/helpers/useBarChart.ts +3 -0
  98. package/src/components/Brush/BrushSelector.tsx +155 -22
  99. package/src/components/Brush/MiniChartPreview.tsx +133 -21
  100. package/src/components/EditorPanel/EditorPanel.tsx +81 -54
  101. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +67 -29
  102. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +0 -78
  103. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +120 -2
  104. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +25 -43
  105. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  106. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +83 -3
  107. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +66 -43
  108. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  109. package/src/components/EditorPanel/editor-panel.scss +1 -1
  110. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  111. package/src/components/ForestPlot/ForestPlot.tsx +26 -22
  112. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  113. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  114. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  115. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  116. package/src/components/HorizonChart/index.tsx +3 -0
  117. package/src/components/Legend/Legend.Component.tsx +52 -4
  118. package/src/components/Legend/Legend.tsx +1 -1
  119. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +4 -4
  120. package/src/components/Legend/LegendValueRange.tsx +77 -0
  121. package/src/components/Legend/helpers/createFormatLabels.tsx +16 -2
  122. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  123. package/src/components/LineChart/helpers/README.md +292 -0
  124. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  125. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  126. package/src/components/LineChart/index.tsx +44 -8
  127. package/src/components/LinearChart/README.md +109 -0
  128. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  129. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  130. package/src/components/LinearChart/tests/LinearChart.test.tsx +278 -0
  131. package/src/components/LinearChart/tests/mockConfigContext.ts +131 -0
  132. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  133. package/src/components/LinearChart.tsx +268 -1057
  134. package/src/components/PieChart/PieChart.tsx +20 -5
  135. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  136. package/src/components/RadarChart/RadarChart.tsx +298 -0
  137. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  138. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  139. package/src/components/RadarChart/helpers.ts +83 -0
  140. package/src/components/RadarChart/index.tsx +3 -0
  141. package/src/components/Regions/components/Regions.tsx +6 -6
  142. package/src/components/Sankey/components/Sankey.tsx +3 -3
  143. package/src/components/Sankey/sankey.scss +1 -1
  144. package/src/components/SmallMultiples/SmallMultiples.css +5 -5
  145. package/src/components/Sparkline/index.scss +4 -2
  146. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  147. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +8 -8
  148. package/src/data/initial-state.js +37 -15
  149. package/src/data/legacy-defaults.ts +18 -0
  150. package/src/helpers/abbreviateNumber.ts +24 -17
  151. package/src/helpers/getChartPatternId.ts +17 -0
  152. package/src/helpers/getExcludedData.ts +4 -0
  153. package/src/helpers/getMinMax.ts +16 -2
  154. package/src/helpers/handleChartAriaLabels.ts +19 -19
  155. package/src/helpers/handleLineType.ts +22 -18
  156. package/src/helpers/seriesColumnSettings.ts +114 -0
  157. package/src/helpers/tests/countNumOfTicks.test.ts +77 -0
  158. package/src/helpers/tests/seriesColumnSettings.test.ts +84 -0
  159. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  160. package/src/hooks/useRightAxis.ts +14 -0
  161. package/src/hooks/useScales.ts +99 -56
  162. package/src/hooks/useTooltip.tsx +23 -3
  163. package/src/scss/main.scss +157 -79
  164. package/src/selectors/README.md +68 -0
  165. package/src/store/chart.reducer.ts +2 -0
  166. package/src/test/CdcChart.test.jsx +2 -2
  167. package/src/types/ChartConfig.ts +22 -0
  168. package/src/types/ChartContext.ts +1 -0
  169. package/src/types/Horizon.ts +64 -0
  170. package/tests/fixtures/chart-config-with-metadata.json +29 -0
  171. package/tests/fixtures/data-with-metadata.json +10 -0
  172. package/preview.html +0 -1616
  173. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -1,11 +1,12 @@
1
1
  import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../../../ConfigContext.js'
3
+ import { useEditorPermissions } from '../../useEditorPermissions'
3
4
 
4
5
  // CDC Core
5
6
  import Accordion from '@cdc/core/components/ui/Accordion'
6
7
  import Button from '@cdc/core/components/elements/Button'
7
8
  import { CheckBox, Select } from '@cdc/core/components/EditorPanel/Inputs'
8
- import _ from 'lodash'
9
+ import cloneDeep from 'lodash/cloneDeep'
9
10
 
10
11
  // types
11
12
  import { type PanelProps } from './../PanelProps'
@@ -13,7 +14,8 @@ import { type PanelProps } from './../PanelProps'
13
14
  import './../panels.scss'
14
15
 
15
16
  const PanelAnnotate: React.FC<PanelProps> = props => {
16
- const { updateConfig, config, svgRef } = useContext(ConfigContext)
17
+ const { updateConfig, config, transformedData } = useContext(ConfigContext)
18
+ const { visSupportsDataAnnotations } = useEditorPermissions()
17
19
 
18
20
  const updateField = (section, subsection, fieldName, value) => {
19
21
  if (subsection) {
@@ -39,11 +41,8 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
39
41
  }
40
42
 
41
43
  const handleAnnotationUpdate = (value, property, index) => {
42
- const svgContainer = document.querySelector('.chart-container > svg')?.getBoundingClientRect()
43
- const newSvgDims = [svgContainer?.width, svgContainer?.height]
44
44
  const annotations = [...config?.annotations]
45
45
  annotations[index][property] = value
46
- annotations[index].savedDimensions = newSvgDims
47
46
 
48
47
  updateConfig({
49
48
  ...config,
@@ -52,15 +51,9 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
52
51
  }
53
52
 
54
53
  const handleAddAnnotation = () => {
55
- // check if svg is animated svg or standard svg
56
- const newSvgDims = [
57
- svgRef?.current?.width?.baseVal?.value || svgRef?.current?.width,
58
- svgRef?.current?.height?.baseVal?.value || svgRef?.current?.height
59
- ]
60
-
61
54
  const newAnnotation = {
62
- text: 'New Annotation',
63
- snapToNearestPoint: false,
55
+ text: 'New annotation',
56
+ anchorMode: 'fixed',
64
57
  fontSize: 16,
65
58
  bezier: 10,
66
59
  show: {
@@ -84,20 +77,12 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
84
77
  subject: true,
85
78
  label: true
86
79
  },
87
- seriesKey: '',
80
+ // seriesKey and dataX are only set when switching to data mode
88
81
  x: 50,
89
- y: Number(newSvgDims?.[1] / 2),
90
- xKey:
91
- config.xAxis.type === 'date'
92
- ? new Date(config?.data?.[0]?.[config.xAxis.dataKey]).getTime()
93
- : config.xAxis.type === 'categorical'
94
- ? '1/15/2016'
95
- : '',
96
- yKey: '',
82
+ y: 50,
97
83
  dx: 20,
98
84
  dy: -20,
99
85
  opacity: '100',
100
- savedDimensions: newSvgDims,
101
86
  connectionType: 'line'
102
87
  }
103
88
 
@@ -163,13 +148,66 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
163
148
  onChange={e => handleAnnotationUpdate(e.target.value, 'text', index)}
164
149
  />
165
150
  </label>
151
+
152
+ {visSupportsDataAnnotations() && (
153
+ <Select
154
+ label='Position Mode:'
155
+ value={annotation.anchorMode || 'fixed'}
156
+ options={[
157
+ { value: 'fixed', label: 'Fixed position' },
158
+ { value: 'data', label: 'Snap to data' }
159
+ ]}
160
+ section='annotations'
161
+ subsection={null}
162
+ fieldName='anchorMode'
163
+ updateField={(section, subsection, fieldName, value) => {
164
+ const updatedAnnotations = cloneDeep(config?.annotations)
165
+ updatedAnnotations[index].anchorMode = value
166
+
167
+ // When switching to data mode, ensure seriesKey and dataX are initialized
168
+ if (value === 'data') {
169
+ if (!updatedAnnotations[index].seriesKey) {
170
+ updatedAnnotations[index].seriesKey = config.series?.[0]?.dataKey || ''
171
+ }
172
+ if (!updatedAnnotations[index].dataX) {
173
+ updatedAnnotations[index].dataX = transformedData?.[0]?.[config.xAxis.dataKey] || ''
174
+ }
175
+ }
176
+
177
+ updateConfig({
178
+ ...config,
179
+ annotations: updatedAnnotations
180
+ })
181
+ }}
182
+ />
183
+ )}
184
+
185
+ {visSupportsDataAnnotations() && annotation.anchorMode === 'data' && (
186
+ <Select
187
+ label='Series:'
188
+ value={annotation.seriesKey || config.series?.[0]?.dataKey || ''}
189
+ options={config.series.map(s => s.dataKey)}
190
+ section='annotations'
191
+ subsection={null}
192
+ fieldName='seriesKey'
193
+ updateField={(section, subsection, fieldName, value) => {
194
+ const updatedAnnotations = cloneDeep(config?.annotations)
195
+ updatedAnnotations[index].seriesKey = value || config.series?.[0]?.dataKey
196
+ updateConfig({
197
+ ...config,
198
+ annotations: updatedAnnotations
199
+ })
200
+ }}
201
+ />
202
+ )}
203
+
166
204
  <label>
167
205
  Opacity
168
206
  <br />
169
207
  <input
170
208
  type='range'
171
209
  onChange={e => {
172
- const updatedAnnotations = _.cloneDeep(config?.annotations)
210
+ const updatedAnnotations = cloneDeep(config?.annotations)
173
211
  updatedAnnotations[index].opacity = e.target.value
174
212
  updateConfig({
175
213
  ...config,
@@ -187,7 +225,7 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
187
225
  fieldName={`${index}.edit.subject`}
188
226
  label='Edit Subject'
189
227
  updateField={(section, subsection, fieldName, value) => {
190
- const updatedAnnotations = _.cloneDeep(config?.annotations)
228
+ const updatedAnnotations = cloneDeep(config?.annotations)
191
229
  updatedAnnotations[index].edit.subject = value
192
230
  updateConfig({
193
231
  ...config,
@@ -202,7 +240,7 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
202
240
  fieldName={`${index}.edit.label`}
203
241
  label='Edit Label'
204
242
  updateField={(section, subsection, fieldName, value) => {
205
- const updatedAnnotations = _.cloneDeep(config?.annotations)
243
+ const updatedAnnotations = cloneDeep(config?.annotations)
206
244
  updatedAnnotations[index].edit.label = value
207
245
  updateConfig({
208
246
  ...config,
@@ -219,7 +257,7 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
219
257
  subsection={null}
220
258
  fieldName='connectionType'
221
259
  updateField={(section, subsection, fieldName, value) => {
222
- const updatedAnnotations = _.cloneDeep(config?.annotations)
260
+ const updatedAnnotations = cloneDeep(config?.annotations)
223
261
  updatedAnnotations[index].connectionType = value
224
262
  updateConfig({
225
263
  ...config,
@@ -239,7 +277,7 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
239
277
  max='20'
240
278
  value={config?.annotations[index]?.bezier || 0}
241
279
  onChange={e => {
242
- const updatedAnnotations = _.cloneDeep(config?.annotations)
280
+ const updatedAnnotations = cloneDeep(config?.annotations)
243
281
  updatedAnnotations[index].bezier = e.target.value
244
282
  updateConfig({
245
283
  ...config,
@@ -259,7 +297,7 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
259
297
  subsection={null}
260
298
  fieldName='marker'
261
299
  updateField={(section, subsection, fieldName, value) => {
262
- const updatedAnnotations = _.cloneDeep(config?.annotations)
300
+ const updatedAnnotations = cloneDeep(config?.annotations)
263
301
  updatedAnnotations[index].marker = value
264
302
  updateConfig({
265
303
  ...config,
@@ -320,84 +320,6 @@ const ForestPlotSettings: FC<PanelProps> = ({ name }) => {
320
320
  <br />
321
321
  <hr />
322
322
  <br />
323
- <h4>Width Settings</h4>
324
-
325
- <label>
326
- <span className='edit-label column-heading'>Chart Offset Left (%)</span>
327
- <input
328
- type='number'
329
- min={0}
330
- max={100}
331
- value={config.forestPlot.leftWidthOffset || 0}
332
- onChange={e => {
333
- updateConfig({
334
- ...config,
335
- forestPlot: {
336
- ...config.forestPlot,
337
- leftWidthOffset: e.target.value
338
- }
339
- })
340
- }}
341
- />
342
- </label>
343
-
344
- <label>
345
- <span className='edit-label column-heading'>Chart Offset Left Mobile(%)</span>
346
- <input
347
- type='number'
348
- min={0}
349
- max={100}
350
- value={config.forestPlot.leftWidthOffsetMobile || 0}
351
- onChange={e => {
352
- updateConfig({
353
- ...config,
354
- forestPlot: {
355
- ...config.forestPlot,
356
- leftWidthOffsetMobile: e.target.value
357
- }
358
- })
359
- }}
360
- />
361
- </label>
362
-
363
- <label>
364
- <span className='edit-label column-heading'>Chart Offset Right (%)</span>
365
- <input
366
- type='number'
367
- min={0}
368
- max={100}
369
- value={config.forestPlot.rightWidthOffset || 0}
370
- onChange={e => {
371
- updateConfig({
372
- ...config,
373
- forestPlot: {
374
- ...config.forestPlot,
375
- rightWidthOffset: e.target.value
376
- }
377
- })
378
- }}
379
- />
380
- </label>
381
-
382
- <label>
383
- <span className='edit-label column-heading'>Chart Offset Right Mobile(%)</span>
384
- <input
385
- type='number'
386
- min={0}
387
- max={100}
388
- value={config.forestPlot.rightWidthOffsetMobile || 0}
389
- onChange={e => {
390
- updateConfig({
391
- ...config,
392
- forestPlot: {
393
- ...config.forestPlot,
394
- rightWidthOffsetMobile: e.target.value
395
- }
396
- })
397
- }}
398
- />
399
- </label>
400
-
401
323
  <TextField
402
324
  type='number'
403
325
  min={20}
@@ -47,6 +47,22 @@ const PanelGeneral: FC<PanelProps> = props => {
47
47
  }
48
48
  }
49
49
 
50
+ const handleTitleStyleChange = (newTitleStyle: string) => {
51
+ updateConfig({
52
+ ...config,
53
+ titleStyle: newTitleStyle,
54
+ visual:
55
+ newTitleStyle === 'legacy'
56
+ ? config.visual
57
+ : {
58
+ ...config.visual,
59
+ border: undefined,
60
+ borderColorTheme: undefined,
61
+ accent: undefined
62
+ }
63
+ })
64
+ }
65
+
50
66
  return (
51
67
  <AccordionItem>
52
68
  {' '}
@@ -206,6 +222,86 @@ const PanelGeneral: FC<PanelProps> = props => {
206
222
  options={['Below Bar', 'On Date/Category Axis']}
207
223
  />
208
224
  )}
225
+ {visualizationType === 'Horizon Chart' && (
226
+ <>
227
+ <TextField
228
+ type='number'
229
+ value={config.horizon?.numLayers || 4}
230
+ section='horizon'
231
+ fieldName='numLayers'
232
+ label='Number of Layers'
233
+ updateField={updateField}
234
+ min={1}
235
+ max={9}
236
+ onBlur={e => {
237
+ const parsed = Number(e.target.value)
238
+ if (!isNaN(parsed)) {
239
+ const value = Math.round(parsed)
240
+ const clamped = Math.min(9, Math.max(1, value))
241
+ if (clamped !== parsed) {
242
+ updateField('horizon', null, 'numLayers', clamped)
243
+ }
244
+ }
245
+ }}
246
+ tooltip={
247
+ <Tooltip style={{ textTransform: 'none' }}>
248
+ <Tooltip.Target>
249
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
250
+ </Tooltip.Target>
251
+ <Tooltip.Content>
252
+ <p>
253
+ The number of layers determines how many "bands" the horizon chart is divided into. The optimal
254
+ number of layers may depend on the data range and the desired level of detail.
255
+ </p>
256
+ <hr />
257
+ <p>Valid range: 1-9. Defaults to 4 layers.</p>
258
+ </Tooltip.Content>
259
+ </Tooltip>
260
+ }
261
+ />
262
+ <TextField
263
+ type='number'
264
+ value={config.horizon?.bandGap ?? 15}
265
+ section='horizon'
266
+ fieldName='bandGap'
267
+ label='Band Gap'
268
+ updateField={updateField}
269
+ tooltip={
270
+ <Tooltip style={{ textTransform: 'none' }}>
271
+ <Tooltip.Target>
272
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
273
+ </Tooltip.Target>
274
+ <Tooltip.Content>
275
+ <p>The vertical spacing (in pixels) between each series row in the horizon chart.</p>
276
+ <hr />
277
+ <p>Defaults to 15 pixels.</p>
278
+ </Tooltip.Content>
279
+ </Tooltip>
280
+ }
281
+ />
282
+ <TextField
283
+ type='number'
284
+ value={config.horizon?.bottomPadding ?? 15}
285
+ section='horizon'
286
+ fieldName='bottomPadding'
287
+ label='Bottom Padding'
288
+ updateField={updateField}
289
+ tooltip={
290
+ <Tooltip style={{ textTransform: 'none' }}>
291
+ <Tooltip.Target>
292
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
293
+ </Tooltip.Target>
294
+ <Tooltip.Content>
295
+ <p>The padding (in pixels) below the last series row in the horizon chart.</p>
296
+ <hr />
297
+ <p>Defaults to 15 pixels.</p>
298
+ </Tooltip.Content>
299
+ </Tooltip>
300
+ }
301
+ />
302
+ </>
303
+ )}
304
+
209
305
  {visHasNumbersOnBars() ? (
210
306
  <CheckBox
211
307
  value={config.yAxis.displayNumbersOnBar}
@@ -316,7 +412,7 @@ const PanelGeneral: FC<PanelProps> = props => {
316
412
  />
317
413
  </>
318
414
  )}
319
- {config.visualizationType !== 'Warming Stripes' && (
415
+ {config.visualizationType !== 'Warming Stripes' && config.visualizationType !== 'Radar' && (
320
416
  <CheckBox
321
417
  tooltip={
322
418
  <Tooltip style={{ textTransform: 'none' }}>
@@ -395,12 +491,12 @@ const PanelGeneral: FC<PanelProps> = props => {
395
491
  value={config.titleStyle}
396
492
  fieldName='titleStyle'
397
493
  label='Title Style'
398
- updateField={updateField}
399
494
  options={[
400
495
  { value: 'small', label: 'Small (h3)' },
401
496
  { value: 'large', label: 'Large (h2)' },
402
497
  { value: 'legacy', label: 'Legacy' }
403
498
  ]}
499
+ onChange={event => handleTitleStyleChange(event.target.value)}
404
500
  tooltip={
405
501
  <Tooltip style={{ textTransform: 'none' }}>
406
502
  <Tooltip.Target>
@@ -499,6 +595,28 @@ const PanelGeneral: FC<PanelProps> = props => {
499
595
  }
500
596
  />
501
597
  )}
598
+ <Select
599
+ value={config.locale}
600
+ fieldName='locale'
601
+ label='Language for dates and numbers'
602
+ updateField={updateField}
603
+ options={[
604
+ { value: 'en-US', label: 'English (en-US)' },
605
+ { value: 'es-MX', label: 'Spanish (es-MX)' }
606
+ ]}
607
+ tooltip={
608
+ <Tooltip style={{ textTransform: 'none' }}>
609
+ <Tooltip.Target>
610
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
611
+ </Tooltip.Target>
612
+ <Tooltip.Content>
613
+ <p>
614
+ Change the language (locale) for this visualization to alter the way dates and numbers are formatted.
615
+ </p>
616
+ </Tooltip.Content>
617
+ </Tooltip>
618
+ }
619
+ />
502
620
  </AccordionItemPanel>
503
621
  </AccordionItem>
504
622
  )
@@ -16,7 +16,7 @@ import { ChartContext } from '../../../../types/ChartContext'
16
16
  import { PanelProps } from '../PanelProps'
17
17
  import { checkColorContrast, getColorContrast } from '@cdc/core/helpers/cove/accessibility'
18
18
  import { getColorScale } from '../../../../helpers/getColorScale'
19
- import _ from 'lodash'
19
+ import { sanitizeToSvgId } from '@cdc/core/helpers/cove/string'
20
20
 
21
21
  const PanelPatternSettings: FC<PanelProps> = props => {
22
22
  const { config, updateConfig, transformedData } = useContext<ChartContext>(ConfigContext)
@@ -69,19 +69,6 @@ const PanelPatternSettings: FC<PanelProps> = props => {
69
69
  }
70
70
  }
71
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
72
  const getFieldOptions = () => {
86
73
  if (!Array.isArray(transformedData) || transformedData.length === 0) return []
87
74
 
@@ -282,11 +269,6 @@ const PanelPatternSettings: FC<PanelProps> = props => {
282
269
  [field]: value
283
270
  }
284
271
 
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
272
  const newPatterns = {
291
273
  ...(legendCfg.patterns || {}),
292
274
  [patternKey]: updatedPattern
@@ -311,7 +293,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
311
293
  updateConfig(updatedConfig)
312
294
  }
313
295
 
314
- if (config.visualizationType === 'Warming Stripes') return
296
+ if (config.visualizationType === 'Warming Stripes' || config.visualizationType === 'Radar') return
315
297
 
316
298
  return (
317
299
  <AccordionItem>
@@ -333,14 +315,18 @@ const PanelPatternSettings: FC<PanelProps> = props => {
333
315
  {/* Individual Pattern Configurations */}
334
316
  {Object.entries(currentPatterns).map(([patternKey, pattern], index) => {
335
317
  const p: LegendPattern = pattern || {}
336
- const dataValueOptions = p.dataKey ? getDataValueOptions(p.dataKey) : []
318
+ const domPatternKey = `${sanitizeToSvgId(patternKey)}-${index}`
337
319
 
338
320
  return (
339
321
  <Accordion allowZeroExpanded key={`pattern-accordion-${index}`}>
340
322
  <AccordionItem>
341
323
  <AccordionItemHeading>
342
324
  <AccordionItemButton>
343
- {p.dataKey && p.dataValue ? `${p.dataKey}: ${p.dataValue}` : `Pattern ${index + 1}`}
325
+ {p.dataKey && p.dataValue
326
+ ? `${p.dataKey}: ${p.dataValue}`
327
+ : p.dataValue
328
+ ? `All Series: ${p.dataValue}`
329
+ : `Pattern ${index + 1}`}
344
330
  </AccordionItemButton>
345
331
  </AccordionItemHeading>
346
332
  <AccordionItemPanel>
@@ -359,32 +345,28 @@ const PanelPatternSettings: FC<PanelProps> = props => {
359
345
  value={p.dataKey || ''}
360
346
  options={fieldOptions}
361
347
  initial='Select Data Key'
362
- fieldName={`pattern-datakey-${patternKey}`}
348
+ fieldName={`pattern-datakey-${domPatternKey}`}
363
349
  updateField={(section, subsection, fieldName, value) =>
364
350
  handlePatternUpdate(patternKey, 'dataKey', value)
365
351
  }
366
352
  />
367
353
 
368
- {p.dataKey && (
369
- <>
370
- <label htmlFor={`pattern-datavalue-${patternKey}`}>
371
- Data Value:
372
- <input
373
- type='text'
374
- id={`pattern-datavalue-${patternKey}`}
375
- value={p.dataValue || ''}
376
- onChange={e => handlePatternUpdate(patternKey, 'dataValue', e.target.value)}
377
- placeholder='Enter data value'
378
- />
379
- </label>
380
- </>
381
- )}
354
+ <label htmlFor={`pattern-datavalue-${domPatternKey}`}>
355
+ Data Value:
356
+ <input
357
+ type='text'
358
+ id={`pattern-datavalue-${domPatternKey}`}
359
+ value={p.dataValue || ''}
360
+ onChange={e => handlePatternUpdate(patternKey, 'dataValue', e.target.value)}
361
+ placeholder='Enter data value'
362
+ />
363
+ </label>
382
364
 
383
- <label htmlFor={`pattern-label-${patternKey}`}>
365
+ <label htmlFor={`pattern-label-${domPatternKey}`}>
384
366
  Label (optional):
385
367
  <input
386
368
  type='text'
387
- id={`pattern-label-${patternKey}`}
369
+ id={`pattern-label-${domPatternKey}`}
388
370
  value={p.label || ''}
389
371
  onChange={e => handlePatternUpdate(patternKey, 'label', e.target.value)}
390
372
  />
@@ -394,7 +376,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
394
376
  label='Pattern Type:'
395
377
  value={p.shape || 'circles'}
396
378
  options={patternTypes}
397
- fieldName={`pattern-type-${patternKey}`}
379
+ fieldName={`pattern-type-${domPatternKey}`}
398
380
  updateField={(section, subsection, fieldName, value) =>
399
381
  handlePatternUpdate(patternKey, 'shape', value)
400
382
  }
@@ -404,14 +386,14 @@ const PanelPatternSettings: FC<PanelProps> = props => {
404
386
  label='Pattern Size:'
405
387
  value={getPatternSizeText(p.patternSize || 8)}
406
388
  options={patternSizes}
407
- fieldName={`pattern-size-${patternKey}`}
389
+ fieldName={`pattern-size-${domPatternKey}`}
408
390
  updateField={(section, subsection, fieldName, value) =>
409
391
  handlePatternUpdate(patternKey, 'patternSize', getPatternSizeNumeric(value))
410
392
  }
411
393
  />
412
394
 
413
395
  <div className='mt-3'>
414
- <label htmlFor={`pattern-color-${patternKey}`}>
396
+ <label htmlFor={`pattern-color-${domPatternKey}`}>
415
397
  Pattern Color
416
398
  <Tooltip style={{ textTransform: 'none' }}>
417
399
  <Tooltip.Target>
@@ -430,7 +412,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
430
412
  <input
431
413
  type='text'
432
414
  value={p.color || ''}
433
- id={`pattern-color-${patternKey}`}
415
+ id={`pattern-color-${domPatternKey}`}
434
416
  onChange={e => handlePatternUpdate(patternKey, 'color', e.target.value)}
435
417
  placeholder='#666666'
436
418
  />