@cdc/chart 4.26.3 → 4.26.5

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 (192) hide show
  1. package/CONFIG.md +489 -0
  2. package/README.md +42 -17
  3. package/dist/cdcchart.js +39024 -36587
  4. package/examples/feature/bar/additional-column-tooltip.json +3 -3
  5. package/examples/feature/deviation/planet-deviation-config.json +2 -2
  6. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  7. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  8. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  9. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  10. package/examples/feature/paired-bar/paired-bar-example.json +2 -2
  11. package/examples/feature/tests-case-rate/case-rate-example-config.json +2 -2
  12. package/examples/feature/tests-covid/covid-confidence-example-config.json +2 -2
  13. package/examples/feature/tests-covid/covid-example-config.json +2 -2
  14. package/examples/feature/tests-cutoff/cutoff-example-config.json +2 -2
  15. package/examples/feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json +2 -2
  16. package/examples/metadata-variables.json +1 -1
  17. package/examples/minimal-example.json +24 -0
  18. package/examples/private/bump-chart.json +1116 -0
  19. package/examples/private/test-123.json +624 -0
  20. package/examples/private/test2.json +35012 -0
  21. package/package.json +5 -4
  22. package/src/CdcChartComponent.tsx +144 -60
  23. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  24. package/src/_stories/{Chart.HTMLInDataTable.stories.tsx → Chart.HTMLInDataTable.smoke.stories.tsx} +10 -0
  25. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  26. package/src/_stories/Chart.Minimal.smoke.stories.tsx +25 -0
  27. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  28. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +108 -0
  29. package/src/_stories/{Chart.stories.tsx → Chart.smoke.stories.tsx} +12 -0
  30. package/src/_stories/{Chart.tooltip.stories.tsx → Chart.tooltip.smoke.stories.tsx} +68 -0
  31. package/src/_stories/{ChartAxisTitles.stories.tsx → ChartAxisTitles.smoke.stories.tsx} +13 -0
  32. package/src/_stories/ChartBar.Editor.stories.tsx +151 -85
  33. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  34. package/src/_stories/{ChartBrush.stories.tsx → ChartBrush.smoke.stories.tsx} +13 -4
  35. package/src/_stories/ChartEditor.Editor.stories.tsx +23 -0
  36. package/src/_stories/ChartLine.DashedDates.smoke.stories.tsx +23 -0
  37. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  38. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  39. package/src/_stories/_mock/heatmap.json +156 -0
  40. package/src/_stories/_mock/line_chart_dashed_dates.json +276 -0
  41. package/src/_stories/_mock/paired-bar-abbr.json +1 -1
  42. package/src/_stories/_mock/paired-bar.json +1 -1
  43. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  44. package/src/_stories/_mock/sankey_mock.json +78 -0
  45. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  46. package/src/components/Annotations/components/AnnotationDraggable.tsx +2 -2
  47. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  48. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -2
  49. package/src/components/Axis/BottomAxis.tsx +1 -1
  50. package/src/components/Axis/LeftAxis.tsx +15 -14
  51. package/src/components/Axis/LeftAxisGridlines.tsx +14 -13
  52. package/src/components/BarChart/components/BarChart.Horizontal.tsx +9 -5
  53. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +9 -5
  54. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +9 -5
  55. package/src/components/BarChart/components/BarChart.Vertical.tsx +9 -6
  56. package/src/components/BarChart/components/BarChart.tsx +2 -6
  57. package/src/components/BarChart/helpers/useBarChart.ts +1 -1
  58. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +3 -3
  59. package/src/components/Brush/BrushSelector.tsx +88 -187
  60. package/src/components/Brush/brushInteraction.ts +22 -0
  61. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  62. package/src/components/EditorPanel/EditorPanel.tsx +946 -960
  63. package/src/components/EditorPanel/EditorPanelContext.ts +1 -1
  64. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +229 -165
  65. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  66. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +169 -116
  67. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +159 -109
  68. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +3 -3
  69. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +14 -28
  70. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  71. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  72. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  73. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  74. package/src/components/EditorPanel/useEditorPermissions.ts +32 -21
  75. package/src/components/Forecasting/Forecasting.tsx +2 -2
  76. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  77. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  78. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  79. package/src/components/HeatMap/heatmap.css +71 -0
  80. package/src/components/HeatMap/helpers/index.ts +261 -0
  81. package/src/components/HeatMap/index.tsx +5 -0
  82. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  83. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  84. package/src/components/HorizonChart/HorizonChart.tsx +10 -2
  85. package/src/components/HoverLine/HoverLine.tsx +6 -8
  86. package/src/components/Legend/Legend.Component.tsx +4 -4
  87. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +5 -0
  88. package/src/components/Legend/LegendGroup/LegendGroup.tsx +3 -1
  89. package/src/components/Legend/helpers/index.ts +0 -2
  90. package/src/components/LineChart/LineChartProps.ts +1 -0
  91. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +22 -24
  92. package/src/components/LineChart/helpers.test.ts +345 -0
  93. package/src/components/LineChart/helpers.ts +15 -5
  94. package/src/components/LineChart/index.tsx +18 -5
  95. package/src/components/LinearChart/VisualizationRenderer.tsx +11 -1
  96. package/src/components/LinearChart/tests/LinearChart.test.tsx +228 -7
  97. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  98. package/src/components/LinearChart.tsx +155 -77
  99. package/src/components/PairedBarChart.jsx +2 -2
  100. package/src/components/PieChart/PieChart.tsx +15 -10
  101. package/src/components/RadarChart/RadarChart.tsx +3 -1
  102. package/src/components/Regions/components/Regions.tsx +199 -35
  103. package/src/components/Sankey/components/Sankey.tsx +7 -3
  104. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  105. package/src/components/ScatterPlot/ScatterPlot.jsx +19 -17
  106. package/src/components/SmallMultiples/SmallMultipleTile.tsx +31 -3
  107. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  108. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  109. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  110. package/src/components/Sparkline/components/SparkLine.tsx +5 -10
  111. package/src/components/WarmingStripes/WarmingStripes.tsx +5 -3
  112. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  113. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  114. package/src/data/initial-state.js +12 -0
  115. package/src/helpers/axisLabelFontSize.ts +8 -0
  116. package/src/helpers/bumpChartTooltip.ts +54 -0
  117. package/src/helpers/calculateLeftYAxisWidth.ts +197 -0
  118. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  119. package/src/helpers/getMinMax.ts +2 -3
  120. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  121. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  122. package/src/helpers/handleChartAriaLabels.ts +23 -10
  123. package/src/helpers/seriesHighlight.ts +26 -0
  124. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  125. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  126. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  127. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  128. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  129. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  130. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  131. package/src/helpers/tests/tooltipHelpers.test.ts +151 -0
  132. package/src/helpers/tooltipHelpers.ts +176 -0
  133. package/src/hooks/tests/useScales.test.tsx +105 -0
  134. package/src/hooks/useHighlightedBars.ts +9 -16
  135. package/src/hooks/useProgrammaticTooltip.ts +16 -2
  136. package/src/hooks/useScales.ts +19 -9
  137. package/src/hooks/useSmallMultipleSynchronization.ts +3 -4
  138. package/src/hooks/useTooltip.tsx +167 -38
  139. package/src/index.jsx +5 -2
  140. package/src/scss/main.scss +32 -49
  141. package/src/test/CdcChart.test.jsx +24 -0
  142. package/src/types/ChartConfig.ts +19 -0
  143. package/src/types/ChartContext.ts +2 -0
  144. package/examples/chart-regression-2.json +0 -2360
  145. package/examples/feature/__data__/age-adjusted-rates.json +0 -1486
  146. package/examples/feature/__data__/city-temperature.json +0 -2198
  147. package/examples/feature/bar/Barchart_with_negative.json +0 -34
  148. package/examples/feature/boxplot/box-plot-data.json +0 -71
  149. package/examples/feature/boxplot/boxplot-data.json +0 -156
  150. package/examples/feature/combo/combochart-categories_are_numbers .json +0 -18
  151. package/examples/feature/forest-plot/log.json +0 -26
  152. package/examples/feature/tests-date-exclusions/date-exclusions-data.json +0 -162
  153. package/examples/region-issue.json +0 -2065
  154. package/examples/xaxis.json +0 -493
  155. package/src/_stories/Chart.SmallMultiples.stories.tsx +0 -63
  156. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  157. /package/examples/feature/bar/{tall-data.json → __data__/tall-data.json} +0 -0
  158. /package/examples/feature/deviation/{planet-deviation-data.json → __data__/planet-deviation-data.json} +0 -0
  159. /package/examples/feature/paired-bar/{paired-bar-data.json → __data__/paired-bar-data.json} +0 -0
  160. /package/examples/feature/paired-bar/{paired-bar-formatted.json → __data__/paired-bar-formatted.json} +0 -0
  161. /package/examples/feature/tests-case-rate/{case-rate-example-data.json → __data__/case-rate-example-data.json} +0 -0
  162. /package/examples/feature/tests-covid/{covid-example-data-confidence.json → __data__/covid-example-data-confidence.json} +0 -0
  163. /package/examples/feature/tests-covid/{covid-example-data.json → __data__/covid-example-data.json} +0 -0
  164. /package/examples/feature/tests-cutoff/{cutoff-example-data.json → __data__/cutoff-example-data.json} +0 -0
  165. /package/examples/feature/tests-non-numerics/{planet-example-data-nonnumeric.json → __data__/planet-example-data-nonnumeric.json} +0 -0
  166. /package/src/_stories/{Chart.Anchors.stories.tsx → Chart.Anchors.smoke.stories.tsx} +0 -0
  167. /package/src/_stories/{Chart.BoxPlot.stories.tsx → Chart.BoxPlot.smoke.stories.tsx} +0 -0
  168. /package/src/_stories/{Chart.CI.stories.tsx → Chart.CI.smoke.stories.tsx} +0 -0
  169. /package/src/_stories/{Chart.Combo.stories.tsx → Chart.Combo.smoke.stories.tsx} +0 -0
  170. /package/src/_stories/{Chart.CustomColors.stories.tsx → Chart.CustomColors.smoke.stories.tsx} +0 -0
  171. /package/src/_stories/{Chart.Defaults.stories.tsx → Chart.Defaults.smoke.stories.tsx} +0 -0
  172. /package/src/_stories/{Chart.DynamicSeries.stories.tsx → Chart.DynamicSeries.smoke.stories.tsx} +0 -0
  173. /package/src/_stories/{Chart.Filters.stories.tsx → Chart.Filters.smoke.stories.tsx} +0 -0
  174. /package/src/_stories/{Chart.Forecast.stories.tsx → Chart.Forecast.smoke.stories.tsx} +0 -0
  175. /package/src/_stories/{Chart.Legend.Gradient.stories.tsx → Chart.Legend.Gradient.smoke.stories.tsx} +0 -0
  176. /package/src/_stories/{Chart.Patterns.stories.tsx → Chart.Patterns.smoke.stories.tsx} +0 -0
  177. /package/src/_stories/{Chart.PreserveDecimals.stories.tsx → Chart.PreserveDecimals.smoke.stories.tsx} +0 -0
  178. /package/src/_stories/{Chart.Regions.Categorical.stories.tsx → Chart.Regions.Categorical.smoke.stories.tsx} +0 -0
  179. /package/src/_stories/{Chart.Regions.DateScale.stories.tsx → Chart.Regions.DateScale.smoke.stories.tsx} +0 -0
  180. /package/src/_stories/{Chart.Regions.DateTimeScale.stories.tsx → Chart.Regions.DateTimeScale.smoke.stories.tsx} +0 -0
  181. /package/src/_stories/{Chart.ScatterPlot.stories.tsx → Chart.ScatterPlot.smoke.stories.tsx} +0 -0
  182. /package/src/_stories/{Chart.SmallestLeftAxisMax.stories.tsx → Chart.SmallestLeftAxisMax.smoke.stories.tsx} +0 -0
  183. /package/src/_stories/{ChartAnnotation.stories.tsx → ChartAnnotation.smoke.stories.tsx} +0 -0
  184. /package/src/_stories/{ChartAxisLabels.stories.tsx → ChartAxisLabels.smoke.stories.tsx} +0 -0
  185. /package/src/_stories/{ChartBrush.Matrix.Continuous.stories.tsx → ChartBrush.Matrix.Continuous.smoke.stories.tsx} +0 -0
  186. /package/src/_stories/{ChartBrush.Matrix.Date.stories.tsx → ChartBrush.Matrix.Date.smoke.stories.tsx} +0 -0
  187. /package/src/_stories/{ChartBrush.Matrix.DateTime.stories.tsx → ChartBrush.Matrix.DateTime.smoke.stories.tsx} +0 -0
  188. /package/src/_stories/{ChartLine.QuadrantAngles.stories.tsx → ChartLine.QuadrantAngles.smoke.stories.tsx} +0 -0
  189. /package/src/_stories/{ChartLine.Suppression.stories.tsx → ChartLine.Suppression.smoke.stories.tsx} +0 -0
  190. /package/src/_stories/{ChartLine.Symbols.stories.tsx → ChartLine.Symbols.smoke.stories.tsx} +0 -0
  191. /package/src/_stories/{ChartPrefixSuffix.stories.tsx → ChartPrefixSuffix.smoke.stories.tsx} +0 -0
  192. /package/src/_stories/{TechAdoptionWithLinks.stories.tsx → TechAdoptionWithLinks.smoke.stories.tsx} +0 -0
@@ -19,7 +19,7 @@ export type EditorPanelContext = {
19
19
  handleUpdateHighlightedBarColor?: Function
20
20
  handleUpdateHighlightedBorderWidth?: Function
21
21
  highlightedBarValues?: Function
22
- highlightedSeriesValues?: Function
22
+ getHighlightedSeriesValues?: Function
23
23
  isPaletteReversed?: boolean
24
24
  handleRemoveHighlightedBar?: Function
25
25
  setLollipopShape?: Function
@@ -1,12 +1,22 @@
1
1
  import React, { useContext } from 'react'
2
+ import { Draggable } from '@hello-pangea/dnd'
2
3
  import ConfigContext from '../../../../ConfigContext.js'
3
4
  import { useEditorPermissions } from '../../useEditorPermissions'
4
5
 
5
6
  // CDC Core
6
7
  import Accordion from '@cdc/core/components/ui/Accordion'
7
8
  import Button from '@cdc/core/components/elements/Button'
9
+ import GroupedList from '@cdc/core/components/EditorPanel/GroupedList'
10
+ import Icon from '@cdc/core/components/ui/Icon'
8
11
  import { CheckBox, Select } from '@cdc/core/components/EditorPanel/Inputs'
9
12
  import cloneDeep from 'lodash/cloneDeep'
13
+ import {
14
+ Accordion as AccessibleAccordion,
15
+ AccordionItem,
16
+ AccordionItemButton,
17
+ AccordionItemHeading,
18
+ AccordionItemPanel
19
+ } from 'react-accessible-accordion'
10
20
 
11
21
  // types
12
22
  import { type PanelProps } from './../PanelProps'
@@ -102,6 +112,16 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
102
112
  })
103
113
  }
104
114
 
115
+ const handleAnnotationReorder = (sourceIndex: number, destinationIndex: number) => {
116
+ const annotations = [...config.annotations]
117
+ const [movedAnnotation] = annotations.splice(sourceIndex, 1)
118
+ annotations.splice(destinationIndex, 0, movedAnnotation)
119
+ updateConfig({
120
+ ...config,
121
+ annotations
122
+ })
123
+ }
124
+
105
125
  return (
106
126
  <Accordion key={props.name}>
107
127
  <Accordion.Section title={props.name} key={props.name}>
@@ -114,6 +134,19 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
114
134
  updateField={updateField}
115
135
  />
116
136
 
137
+ <Select
138
+ label='Mobile Annotations'
139
+ value={config?.general?.mobileAnnotationDisplay || 'symbol'}
140
+ options={[
141
+ { value: 'symbol', label: 'Show Symbol' },
142
+ { value: 'text', label: 'Show Text' }
143
+ ]}
144
+ section='general'
145
+ subsection={null}
146
+ fieldName='mobileAnnotationDisplay'
147
+ updateField={updateField}
148
+ />
149
+
117
150
  {config.general.showAnnotationDropdown && (
118
151
  <label key={`key-2`}>
119
152
  Annotation Dropdown Title:
@@ -133,190 +166,221 @@ const PanelAnnotate: React.FC<PanelProps> = props => {
133
166
  />
134
167
  </label>
135
168
  )}
136
- {config?.annotations &&
137
- config?.annotations.map((annotation, index) => (
138
- <Accordion key={index}>
139
- <Accordion.Section
140
- title={annotation.text ? annotation.text.substring(0, 15) + '...' : `Annotation ${index + 1}`}
141
- >
142
- <div className='annotation-group'>
143
- <label>
144
- Annotation Text:
145
- <textarea
146
- rows={5}
147
- value={annotation.text}
148
- onChange={e => handleAnnotationUpdate(e.target.value, 'text', index)}
149
- />
150
- </label>
169
+ <GroupedList
170
+ items={config?.annotations}
171
+ label='Text Annotations'
172
+ droppableId='chart-annotations-order'
173
+ onDragEnd={({ source, destination }) => {
174
+ if (!destination || source.index === destination.index) return
175
+ handleAnnotationReorder(source.index, destination.index)
176
+ }}
177
+ renderItem={(annotation, index) => (
178
+ <Draggable key={`annotation-${index}`} draggableId={`annotation-${index}`} index={index}>
179
+ {(provided, snapshot) => (
180
+ <div
181
+ ref={provided.innerRef}
182
+ {...provided.draggableProps}
183
+ {...provided.dragHandleProps}
184
+ className={snapshot.isDragging ? 'currently-dragging' : ''}
185
+ style={provided.draggableProps.style}
186
+ >
187
+ <AccessibleAccordion allowZeroExpanded preExpanded={['panel']}>
188
+ <AccordionItem uuid='panel' className='series-item series-item--chart'>
189
+ <AccordionItemHeading className='series-item__title'>
190
+ <AccordionItemButton className='accordion__button'>
191
+ <Icon display='move' size={15} style={{ cursor: 'default' }} />
192
+ {annotation.text ? annotation.text.substring(0, 15) + '...' : `Annotation ${index + 1}`}
193
+ </AccordionItemButton>
194
+ </AccordionItemHeading>
195
+ <AccordionItemPanel>
196
+ <div className='series-item__panel-actions'>
197
+ <Button
198
+ type='button'
199
+ variant='danger'
200
+ size='sm'
201
+ className='grouped-list__remove'
202
+ onClick={() => handleRemoveAnnotation(index)}
203
+ >
204
+ Delete Annotation
205
+ </Button>
206
+ </div>
207
+ <label>
208
+ Annotation Text:
209
+ <textarea
210
+ rows={5}
211
+ value={annotation.text}
212
+ onChange={e => handleAnnotationUpdate(e.target.value, 'text', index)}
213
+ />
214
+ </label>
151
215
 
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
216
+ {visSupportsDataAnnotations() && (
217
+ <Select
218
+ label='Position Mode:'
219
+ value={annotation.anchorMode || 'fixed'}
220
+ options={[
221
+ { value: 'fixed', label: 'Fixed position' },
222
+ { value: 'data', label: 'Snap to data' }
223
+ ]}
224
+ section='annotations'
225
+ subsection={null}
226
+ fieldName='anchorMode'
227
+ updateField={(section, subsection, fieldName, value) => {
228
+ const updatedAnnotations = cloneDeep(config?.annotations)
229
+ updatedAnnotations[index].anchorMode = value
166
230
 
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
- }
231
+ // When switching to data mode, ensure seriesKey and dataX are initialized
232
+ if (value === 'data') {
233
+ if (!updatedAnnotations[index].seriesKey) {
234
+ updatedAnnotations[index].seriesKey = config.series?.[0]?.dataKey || ''
235
+ }
236
+ if (!updatedAnnotations[index].dataX) {
237
+ updatedAnnotations[index].dataX = transformedData?.[0]?.[config.xAxis.dataKey] || ''
238
+ }
239
+ }
176
240
 
177
- updateConfig({
178
- ...config,
179
- annotations: updatedAnnotations
180
- })
181
- }}
182
- />
183
- )}
241
+ updateConfig({
242
+ ...config,
243
+ annotations: updatedAnnotations
244
+ })
245
+ }}
246
+ />
247
+ )}
184
248
 
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
- )}
249
+ {visSupportsDataAnnotations() && annotation.anchorMode === 'data' && (
250
+ <Select
251
+ label='Series:'
252
+ value={annotation.seriesKey || config.series?.[0]?.dataKey || ''}
253
+ options={config.series.map(s => s.dataKey)}
254
+ section='annotations'
255
+ subsection={null}
256
+ fieldName='seriesKey'
257
+ updateField={(section, subsection, fieldName, value) => {
258
+ const updatedAnnotations = cloneDeep(config?.annotations)
259
+ updatedAnnotations[index].seriesKey = value || config.series?.[0]?.dataKey
260
+ updateConfig({
261
+ ...config,
262
+ annotations: updatedAnnotations
263
+ })
264
+ }}
265
+ />
266
+ )}
203
267
 
204
- <label>
205
- Opacity
206
- <br />
207
- <input
208
- type='range'
209
- onChange={e => {
210
- const updatedAnnotations = cloneDeep(config?.annotations)
211
- updatedAnnotations[index].opacity = e.target.value
212
- updateConfig({
213
- ...config,
214
- annotations: updatedAnnotations
215
- })
216
- }}
217
- value={config?.annotations?.[index]?.opacity || '100'}
218
- />
219
- </label>
268
+ <label>
269
+ Opacity
270
+ <br />
271
+ <input
272
+ type='range'
273
+ onChange={e => {
274
+ const updatedAnnotations = cloneDeep(config?.annotations)
275
+ updatedAnnotations[index].opacity = e.target.value
276
+ updateConfig({
277
+ ...config,
278
+ annotations: updatedAnnotations
279
+ })
280
+ }}
281
+ value={config?.annotations?.[index]?.opacity || '100'}
282
+ />
283
+ </label>
220
284
 
221
- <CheckBox
222
- value={config?.annotations[index]?.edit?.subject || false}
223
- section='annotations'
224
- subsection={null}
225
- fieldName={`${index}.edit.subject`}
226
- label='Edit Subject'
227
- updateField={(section, subsection, fieldName, value) => {
228
- const updatedAnnotations = cloneDeep(config?.annotations)
229
- updatedAnnotations[index].edit.subject = value
230
- updateConfig({
231
- ...config,
232
- annotations: updatedAnnotations
233
- })
234
- }}
235
- />
236
- <CheckBox
237
- value={config?.annotations[index]?.edit?.label || false}
238
- section='annotations'
239
- subsection={null}
240
- fieldName={`${index}.edit.label`}
241
- label='Edit Label'
242
- updateField={(section, subsection, fieldName, value) => {
243
- const updatedAnnotations = cloneDeep(config?.annotations)
244
- updatedAnnotations[index].edit.label = value
245
- updateConfig({
246
- ...config,
247
- annotations: updatedAnnotations
248
- })
249
- }}
250
- />
251
-
252
- <Select
253
- label='Connection Type:'
254
- value={config?.annotations[index]?.connectionType}
255
- options={['Select', 'curve', 'line', 'elbow', 'none']}
256
- section='annotations'
257
- subsection={null}
258
- fieldName='connectionType'
259
- updateField={(section, subsection, fieldName, value) => {
260
- const updatedAnnotations = cloneDeep(config?.annotations)
261
- updatedAnnotations[index].connectionType = value
262
- updateConfig({
263
- ...config,
264
- annotations: updatedAnnotations
265
- })
266
- }}
267
- />
285
+ <CheckBox
286
+ value={config?.annotations[index]?.edit?.subject || false}
287
+ section='annotations'
288
+ subsection={null}
289
+ fieldName={`${index}.edit.subject`}
290
+ label='Edit Subject'
291
+ updateField={(section, subsection, fieldName, value) => {
292
+ const updatedAnnotations = cloneDeep(config?.annotations)
293
+ updatedAnnotations[index].edit.subject = value
294
+ updateConfig({
295
+ ...config,
296
+ annotations: updatedAnnotations
297
+ })
298
+ }}
299
+ />
300
+ <CheckBox
301
+ value={config?.annotations[index]?.edit?.label || false}
302
+ section='annotations'
303
+ subsection={null}
304
+ fieldName={`${index}.edit.label`}
305
+ label='Edit Label'
306
+ updateField={(section, subsection, fieldName, value) => {
307
+ const updatedAnnotations = cloneDeep(config?.annotations)
308
+ updatedAnnotations[index].edit.label = value
309
+ updateConfig({
310
+ ...config,
311
+ annotations: updatedAnnotations
312
+ })
313
+ }}
314
+ />
268
315
 
269
- {annotation.connectionType === 'curve' && (
270
- <>
271
- <label>
272
- Curve Control
273
- {/* create a range input */}
274
- <input
275
- type='range'
276
- min='-20'
277
- max='20'
278
- value={config?.annotations[index]?.bezier || 0}
279
- onChange={e => {
316
+ <Select
317
+ label='Connection Type:'
318
+ value={config?.annotations[index]?.connectionType}
319
+ options={['Select', 'curve', 'line', 'elbow', 'none']}
320
+ section='annotations'
321
+ subsection={null}
322
+ fieldName='connectionType'
323
+ updateField={(section, subsection, fieldName, value) => {
280
324
  const updatedAnnotations = cloneDeep(config?.annotations)
281
- updatedAnnotations[index].bezier = e.target.value
325
+ updatedAnnotations[index].connectionType = value
282
326
  updateConfig({
283
327
  ...config,
284
328
  annotations: updatedAnnotations
285
329
  })
286
330
  }}
287
331
  />
288
- </label>
289
- </>
290
- )}
291
332
 
292
- <Select
293
- label='Marker'
294
- value={annotation.marker}
295
- options={['arrow', 'circle']}
296
- section='annotations'
297
- subsection={null}
298
- fieldName='marker'
299
- updateField={(section, subsection, fieldName, value) => {
300
- const updatedAnnotations = cloneDeep(config?.annotations)
301
- updatedAnnotations[index].marker = value
302
- updateConfig({
303
- ...config,
304
- annotations: updatedAnnotations
305
- })
306
- }}
307
- />
333
+ {annotation.connectionType === 'curve' && (
334
+ <>
335
+ <label>
336
+ Curve Control
337
+ {/* create a range input */}
338
+ <input
339
+ type='range'
340
+ min='-20'
341
+ max='20'
342
+ value={config?.annotations[index]?.bezier || 0}
343
+ onChange={e => {
344
+ const updatedAnnotations = cloneDeep(config?.annotations)
345
+ updatedAnnotations[index].bezier = e.target.value
346
+ updateConfig({
347
+ ...config,
348
+ annotations: updatedAnnotations
349
+ })
350
+ }}
351
+ />
352
+ </label>
353
+ </>
354
+ )}
308
355
 
309
- <Button className='btn btn-danger full-width' onClick={() => handleRemoveAnnotation(index)}>
310
- Delete Annotation
311
- </Button>
356
+ <Select
357
+ label='Marker'
358
+ value={annotation.marker}
359
+ options={['arrow', 'circle']}
360
+ section='annotations'
361
+ subsection={null}
362
+ fieldName='marker'
363
+ updateField={(section, subsection, fieldName, value) => {
364
+ const updatedAnnotations = cloneDeep(config?.annotations)
365
+ updatedAnnotations[index].marker = value
366
+ updateConfig({
367
+ ...config,
368
+ annotations: updatedAnnotations
369
+ })
370
+ }}
371
+ />
372
+ </AccordionItemPanel>
373
+ </AccordionItem>
374
+ </AccessibleAccordion>
312
375
  </div>
313
- </Accordion.Section>
314
- </Accordion>
315
- ))}
376
+ )}
377
+ </Draggable>
378
+ )}
379
+ />
316
380
  {config?.annotations?.length < 3 && (
317
- <button onClick={handleAddAnnotation} className='btn btn-primary mt-2 full-width'>
381
+ <Button variant='editor-primary' onClick={handleAddAnnotation}>
318
382
  Add Annotation
319
- </button>
383
+ </Button>
320
384
  )}
321
385
  </Accordion.Section>
322
386
  </Accordion>
@@ -13,12 +13,15 @@ import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
13
13
  import { TextField, Select, CheckBox } from '@cdc/core/components/EditorPanel/Inputs'
14
14
  import Tooltip from '@cdc/core/components/ui/Tooltip'
15
15
  import Icon from '@cdc/core/components/ui/Icon'
16
+ import { resolveAltTextDescription } from '@cdc/core/helpers/resolveAltTextDescription'
16
17
 
17
18
  // contexts
18
19
  import { useEditorPermissions } from '../../useEditorPermissions.js'
19
20
  import { useEditorPanelContext } from '../../EditorPanelContext.js'
20
21
  import ConfigContext from '../../../../ConfigContext.js'
21
22
  import { PanelProps } from '../PanelProps'
23
+ import { getVisualizationTypeConfigUpdate } from '../../helpers/getVisualizationTypeConfigUpdate'
24
+ import { type VisualizationType } from '../../../../types/ChartConfig'
22
25
 
23
26
  const PanelGeneral: FC<PanelProps> = props => {
24
27
  const { config, updateConfig } = useContext(ConfigContext)
@@ -79,22 +82,12 @@ const PanelGeneral: FC<PanelProps> = props => {
79
82
  updateField={updateField}
80
83
  options={enabledChartTypes}
81
84
  onChange={event => {
82
- const newVisType = event.target.value
85
+ const newVisType = event.target.value as VisualizationType
83
86
 
84
87
  updateField(null, null, 'visualizationType', newVisType)
85
88
 
86
- if (newVisType === 'Forecasting' && config.xAxis.type === 'categorical') {
87
- updateConfig({
88
- ...config,
89
- visualizationType: newVisType,
90
- xAxis: {
91
- ...config.xAxis,
92
- type: 'date',
93
- dateParseFormat: config.xAxis.dateParseFormat || '%Y-%m-%d',
94
- dateDisplayFormat: config.xAxis.dateDisplayFormat || '%Y-%m-%d'
95
- }
96
- })
97
- }
89
+ const updatedConfig = getVisualizationTypeConfigUpdate(config, newVisType)
90
+ if (updatedConfig) updateConfig(updatedConfig)
98
91
  }}
99
92
  />
100
93
  )}
@@ -617,6 +610,105 @@ const PanelGeneral: FC<PanelProps> = props => {
617
610
  </Tooltip>
618
611
  }
619
612
  />
613
+
614
+ {/* Accessible Alt Text Description */}
615
+ {(() => {
616
+ const metadataKeys = Object.keys(config.dataMetadata || {})
617
+ const hasMetadata = metadataKeys.length > 0
618
+ const descType = config.altText?.type || ''
619
+ const resolvedDescription = resolveAltTextDescription(config.altText, config.dataMetadata)
620
+ return (
621
+ <>
622
+ <Select
623
+ value={descType}
624
+ fieldName='altTextType'
625
+ label='Alt Text Description'
626
+ options={[
627
+ { value: '', label: 'None' },
628
+ { value: 'static', label: 'Static (manual text)' },
629
+ { value: 'metadata', label: 'Data File Metadata' }
630
+ ]}
631
+ updateField={(_section, _subsection, _fieldName, value) => {
632
+ if (value === '') {
633
+ updateField(null, null, 'altText', undefined)
634
+ } else {
635
+ updateField(null, null, 'altText', { type: value as 'static' | 'metadata' })
636
+ }
637
+ }}
638
+ tooltip={
639
+ <Tooltip style={{ textTransform: 'none' }}>
640
+ <Tooltip.Target>
641
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
642
+ </Tooltip.Target>
643
+ <Tooltip.Content>
644
+ <p>
645
+ Add a longer description for screen readers. The chart title is always auto-generated. Use
646
+ "Static" for manually written text, or "Data File Metadata" to pull it from a key in your data
647
+ file.
648
+ </p>
649
+ </Tooltip.Content>
650
+ </Tooltip>
651
+ }
652
+ />
653
+ {descType === 'static' && (
654
+ <TextField
655
+ value={config.altText?.value || ''}
656
+ fieldName='altTextValue'
657
+ type='textarea'
658
+ label='Description Text'
659
+ placeholder='Longer interpretive description of chart insights...'
660
+ updateField={(_section, _subsection, _fieldName, value) => {
661
+ updateField(null, null, 'altText', { ...config.altText, value })
662
+ }}
663
+ />
664
+ )}
665
+ {descType === 'metadata' && (
666
+ <>
667
+ {hasMetadata ? (
668
+ <Select
669
+ value={config.altText?.metadataKey || ''}
670
+ fieldName='altTextMetadataKey'
671
+ label='Description Metadata Field'
672
+ options={[
673
+ { value: '', label: 'Select Metadata Field...' },
674
+ ...metadataKeys.map(key => ({
675
+ value: key,
676
+ label: `${key}: ${config.dataMetadata[key]}`
677
+ }))
678
+ ]}
679
+ updateField={(_section, _subsection, _fieldName, value) => {
680
+ updateField(null, null, 'altText', { ...config.altText, metadataKey: value })
681
+ }}
682
+ />
683
+ ) : (
684
+ <span className='subtext'>
685
+ No metadata fields are available. Your data file must be a JSON object with a <code>data</code>{' '}
686
+ array and sibling key-value pairs, for example:{' '}
687
+ <code>{`{ "altDescription": "...", "data": [...] }`}</code>
688
+ </span>
689
+ )}
690
+ </>
691
+ )}
692
+ {resolvedDescription && (
693
+ <div
694
+ style={{
695
+ marginTop: '1em',
696
+ padding: '0.75em',
697
+ background: '#f5f5f5',
698
+ borderRadius: '4px',
699
+ fontSize: '0.8em',
700
+ textTransform: 'none'
701
+ }}
702
+ >
703
+ <strong style={{ display: 'block', marginBottom: '0.25em' }}>Preview:</strong>
704
+ <p data-testid='alt-text-desc-preview' style={{ margin: 0, fontStyle: 'italic' }}>
705
+ {resolvedDescription}
706
+ </p>
707
+ </div>
708
+ )}
709
+ </>
710
+ )
711
+ })()}
620
712
  </AccordionItemPanel>
621
713
  </AccordionItem>
622
714
  )