@cdc/chart 4.26.1 → 4.26.2

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 (132) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +45357 -43655
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/line-chart-states.json +1085 -0
  9. package/examples/private/123.json +694 -0
  10. package/examples/private/anchor-issue.json +4094 -0
  11. package/examples/private/backwards-slider.json +10430 -0
  12. package/examples/private/georgia.csv +160 -0
  13. package/examples/private/timeline-data.json +1 -0
  14. package/examples/private/timeline.json +389 -0
  15. package/examples/radar-chart-simple.json +133 -0
  16. package/examples/radar-chart.json +148 -0
  17. package/index.html +1 -31
  18. package/package.json +57 -59
  19. package/src/CdcChartComponent.tsx +99 -18
  20. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  21. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  22. package/src/_stories/Chart.CI.stories.tsx +13 -0
  23. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  24. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  25. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  26. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  27. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  28. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  29. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  30. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  31. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  32. package/src/_stories/Chart.Regions.Categorical.stories.tsx +13 -0
  33. package/src/_stories/Chart.Regions.DateScale.stories.tsx +19 -0
  34. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +25 -10
  35. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  36. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  37. package/src/_stories/Chart.stories.tsx +37 -0
  38. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  39. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  40. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  41. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  42. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  43. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  44. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  45. package/src/_stories/ChartBrush.stories.tsx +7 -0
  46. package/src/_stories/ChartEditor.stories.tsx +7 -0
  47. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  48. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  49. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  50. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  51. package/src/_stories/TechAdoptionWithLinks.stories.tsx +7 -0
  52. package/src/_stories/_mock/brush_continuous.json +86 -0
  53. package/src/_stories/_mock/brush_date_large.json +176 -0
  54. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  55. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  56. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  57. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  58. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  59. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  60. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  61. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  62. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  63. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  64. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  65. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  66. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  67. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  68. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  69. package/src/components/Axis/BottomAxis.tsx +270 -0
  70. package/src/components/Axis/LeftAxis.tsx +404 -0
  71. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  72. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  73. package/src/components/Axis/README.md +94 -0
  74. package/src/components/Axis/RightAxis.tsx +108 -0
  75. package/src/components/Axis/axis.constants.ts +21 -0
  76. package/src/components/Axis/index.ts +7 -0
  77. package/src/components/BarChart/components/BarChart.tsx +7 -1
  78. package/src/components/Brush/BrushSelector.tsx +154 -22
  79. package/src/components/Brush/MiniChartPreview.tsx +138 -21
  80. package/src/components/EditorPanel/EditorPanel.tsx +25 -11
  81. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  82. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +81 -1
  83. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -1
  84. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  85. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  86. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +21 -1
  87. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  88. package/src/components/EditorPanel/useEditorPermissions.ts +55 -26
  89. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  90. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  91. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  92. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  93. package/src/components/HorizonChart/index.tsx +3 -0
  94. package/src/components/Legend/Legend.Component.tsx +52 -4
  95. package/src/components/Legend/Legend.tsx +1 -1
  96. package/src/components/Legend/LegendValueRange.tsx +77 -0
  97. package/src/components/Legend/helpers/createFormatLabels.tsx +13 -0
  98. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  99. package/src/components/LineChart/helpers/README.md +292 -0
  100. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  101. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  102. package/src/components/LineChart/index.tsx +44 -8
  103. package/src/components/LinearChart/README.md +109 -0
  104. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  105. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  106. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  107. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  108. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  109. package/src/components/LinearChart.tsx +250 -1059
  110. package/src/components/PieChart/PieChart.tsx +1 -1
  111. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  112. package/src/components/RadarChart/RadarChart.tsx +298 -0
  113. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  114. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  115. package/src/components/RadarChart/helpers.ts +83 -0
  116. package/src/components/RadarChart/index.tsx +3 -0
  117. package/src/components/WarmingStripes/WarmingStripes.tsx +95 -25
  118. package/src/data/initial-state.js +14 -1
  119. package/src/helpers/getExcludedData.ts +4 -0
  120. package/src/helpers/handleChartAriaLabels.ts +19 -19
  121. package/src/helpers/handleLineType.ts +22 -18
  122. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  123. package/src/hooks/useScales.ts +7 -0
  124. package/src/hooks/useTooltip.tsx +3 -0
  125. package/src/scss/main.scss +5 -0
  126. package/src/selectors/README.md +68 -0
  127. package/src/store/chart.reducer.ts +2 -0
  128. package/src/types/ChartConfig.ts +18 -0
  129. package/src/types/ChartContext.ts +1 -0
  130. package/src/types/Horizon.ts +64 -0
  131. package/preview.html +0 -1616
  132. package/src/components/Annotations/components/helpers/index.tsx +0 -46
@@ -10,25 +10,63 @@ interface MiniChartPreviewProps {
10
10
  dataKey: string
11
11
  xScale: any
12
12
  miniYScale: any
13
+ miniRightYScale?: any
13
14
  colorScale: any
14
15
  config: any
15
16
  xMax: number
16
17
  }
17
18
 
18
19
  const MiniChartPreview = memo<MiniChartPreviewProps>(
19
- ({ series, tableData, dataKey, xScale, miniYScale, colorScale, config }) => {
20
+ ({ series, tableData, dataKey, xScale, miniYScale, miniRightYScale, colorScale, config }) => {
20
21
  if (!series || !series.length || !tableData || !tableData.length || !xScale || !miniYScale) {
21
22
  return null
22
23
  }
23
24
 
24
25
  const bandwidth = xScale.bandwidth?.() || 0
26
+ const isComboChart = config?.visualizationType === 'Combo'
25
27
  const isBarChart = config?.visualizationType === 'Bar'
26
28
  const isStacked = config?.visualizationSubType === 'stacked'
27
29
  const isAreaChart = config?.visualizationType === 'Area Chart'
30
+ const barSeriesTypes = new Set(['Bar', 'Paired Bar', 'Deviation Bar', 'Combo'])
31
+ const barSeries = isComboChart ? series.filter(s => barSeriesTypes.has(s.type)) : series
32
+ const areaSeries = isComboChart ? series.filter(s => s.type === 'Area Chart') : series
33
+ const lineSeries = isComboChart
34
+ ? series.filter(s => !barSeriesTypes.has(s.type) && s.type !== 'Area Chart')
35
+ : series
36
+
37
+ let barElements: React.ReactElement[] = []
28
38
 
29
39
  // For bar charts, render rectangles
30
- if (isBarChart) {
40
+ if (isBarChart || (isComboChart && barSeries.length > 0)) {
31
41
  const bars: React.ReactElement[] = []
42
+ const barSeriesToRender = isBarChart ? series : barSeries
43
+
44
+ const barStrokeColor = config?.barHasBorder === 'true' ? '#000' : 'transparent'
45
+ const barStrokeWidth = config?.barHasBorder === 'true' ? 1 : 0
46
+
47
+ const getPatternUrl = (datum, seriesKey: string, value: string | number) => {
48
+ if (!config.legend?.patterns || Object.keys(config.legend.patterns).length === 0) {
49
+ return null
50
+ }
51
+
52
+ for (const [patternKey, patternObj] of Object.entries(config.legend.patterns)) {
53
+ const pattern = patternObj as any
54
+ if (pattern.dataKey && pattern.dataValue) {
55
+ if (pattern.dataKey === seriesKey && String(value) === String(pattern.dataValue)) {
56
+ return `url(#chart-pattern-${patternKey})`
57
+ }
58
+
59
+ if (!config.runtime?.seriesLabels || !config.runtime.seriesLabels[pattern.dataKey]) {
60
+ const dataFieldValue = datum[pattern.dataKey]
61
+ if (String(dataFieldValue) === String(pattern.dataValue)) {
62
+ return `url(#chart-pattern-${patternKey})`
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ return null
69
+ }
32
70
 
33
71
  tableData.forEach((d, i) => {
34
72
  const xVal = xScale(d[dataKey])
@@ -47,11 +85,12 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
47
85
  let cumulativeValue = 0
48
86
  const zeroY = miniYScale(0)
49
87
 
50
- series.forEach((s, seriesIndex) => {
88
+ barSeriesToRender.forEach((s, seriesIndex) => {
51
89
  const value = parseFloat(d[s.dataKey])
52
90
  if (isNaN(value) || value === 0) return
53
91
 
54
92
  const seriesColor = colorScale?.(config.runtime.seriesLabels?.[s.dataKey] || s.dataKey) || '#666'
93
+ const patternUrl = getPatternUrl(d, s.dataKey, value)
55
94
 
56
95
  // Calculate the bottom and top of this segment
57
96
  // For stacked bars, each segment sits on top of the previous one
@@ -72,22 +111,41 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
72
111
  width={barWidth}
73
112
  height={barHeight}
74
113
  fill={seriesColor}
75
- fillOpacity={0.7}
114
+ stroke={barStrokeColor}
115
+ strokeWidth={barStrokeWidth}
116
+ fillOpacity={1}
76
117
  pointerEvents='none'
77
118
  />
78
119
  )
120
+ if (patternUrl) {
121
+ bars.push(
122
+ <rect
123
+ key={`mini-bar-stacked-pattern-${i}-${seriesIndex}`}
124
+ x={x - barWidth / 2}
125
+ y={y}
126
+ width={barWidth}
127
+ height={barHeight}
128
+ fill={patternUrl}
129
+ stroke='transparent'
130
+ strokeWidth={0}
131
+ fillOpacity={1}
132
+ pointerEvents='none'
133
+ />
134
+ )
135
+ }
79
136
  })
80
137
  } else {
81
138
  // For grouped bars, render bars side by side
82
- const seriesCount = series.length
139
+ const seriesCount = barSeriesToRender.length
83
140
  const groupBarWidth = barWidth / seriesCount
84
141
  const zeroY = miniYScale(0)
85
142
 
86
- series.forEach((s, seriesIndex) => {
143
+ barSeriesToRender.forEach((s, seriesIndex) => {
87
144
  const value = parseFloat(d[s.dataKey])
88
145
  if (isNaN(value)) return
89
146
 
90
147
  const seriesColor = colorScale?.(config.runtime.seriesLabels?.[s.dataKey] || s.dataKey) || '#666'
148
+ const patternUrl = getPatternUrl(d, s.dataKey, value)
91
149
 
92
150
  // Calculate bar position and height
93
151
  const valueY = miniYScale(value)
@@ -100,18 +158,39 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
100
158
  key={`mini-bar-grouped-${i}-${seriesIndex}`}
101
159
  x={barX}
102
160
  y={y}
103
- width={groupBarWidth * 0.9} // Wider bars with small gap between them
161
+ width={groupBarWidth}
104
162
  height={barHeight}
105
163
  fill={seriesColor}
106
- fillOpacity={0.7}
164
+ stroke={barStrokeColor}
165
+ strokeWidth={barStrokeWidth}
166
+ fillOpacity={1}
107
167
  pointerEvents='none'
108
168
  />
109
169
  )
170
+ if (patternUrl) {
171
+ bars.push(
172
+ <rect
173
+ key={`mini-bar-grouped-pattern-${i}-${seriesIndex}`}
174
+ x={barX}
175
+ y={y}
176
+ width={groupBarWidth}
177
+ height={barHeight}
178
+ fill={patternUrl}
179
+ stroke='transparent'
180
+ strokeWidth={0}
181
+ fillOpacity={1}
182
+ pointerEvents='none'
183
+ />
184
+ )
185
+ }
110
186
  })
111
187
  }
112
188
  })
113
189
 
114
- return <>{bars}</>
190
+ barElements = bars
191
+ if (isBarChart && !isComboChart) {
192
+ return <>{barElements}</>
193
+ }
115
194
  }
116
195
 
117
196
  // For stacked area charts, use AreaStack
@@ -217,10 +296,10 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
217
296
  )
218
297
  }
219
298
 
220
- // For line/area charts, render lines or areas
299
+ // For line/area charts (and combo line/area series), render lines or areas
221
300
  return (
222
301
  <>
223
- {series.map((s, i) => {
302
+ {(isComboChart ? areaSeries : isAreaChart ? series : []).map((s, i) => {
224
303
  const seriesKey = s.dataKey
225
304
  const seriesColor = colorScale?.(config.runtime.seriesLabels?.[seriesKey] || seriesKey) || '#666'
226
305
 
@@ -231,25 +310,31 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
231
310
  const curve = allCurves[seriesLineType] || allCurves.curveLinear
232
311
  const strokeDasharray = handleLineType(seriesStyle)
233
312
 
234
- // Filter to only valid data points
235
- const validData = tableData.filter(d => {
236
- const xVal = xScale(d[dataKey])
237
- const yVal = parseFloat(d[s.dataKey])
238
- return xVal !== undefined && !isNaN(yVal)
239
- })
313
+ // Use the right-axis scale when the series is on the right axis
314
+ const yScaleForSeries = s.axis === 'Right' && miniRightYScale ? miniRightYScale : miniYScale
315
+
316
+ // Filter to only valid data points, then sort by X position so the area
317
+ // renders left-to-right regardless of the original data order.
318
+ const validData = tableData
319
+ .filter(d => {
320
+ const xVal = xScale(d[dataKey])
321
+ const yVal = parseFloat(d[s.dataKey])
322
+ return xVal !== undefined && !isNaN(yVal)
323
+ })
324
+ .sort((a, b) => (xScale(a[dataKey]) ?? 0) - (xScale(b[dataKey]) ?? 0))
240
325
 
241
326
  if (validData.length === 0) return null
242
327
 
243
328
  const getX = d => xScale(d[dataKey]) + bandwidth / 2
244
- const getY = d => miniYScale(parseFloat(d[s.dataKey]))
329
+ const getY = d => yScaleForSeries(parseFloat(d[s.dataKey]))
245
330
 
246
- return isAreaChart ? (
331
+ return (
247
332
  <AreaClosed
248
333
  key={`mini-area-${seriesKey}-${i}`}
249
334
  data={validData}
250
335
  x={getX}
251
336
  y={getY}
252
- yScale={miniYScale}
337
+ yScale={yScaleForSeries}
253
338
  fill={seriesColor}
254
339
  fillOpacity={1}
255
340
  stroke={seriesColor}
@@ -258,7 +343,39 @@ const MiniChartPreview = memo<MiniChartPreviewProps>(
258
343
  curve={curve}
259
344
  pointerEvents='none'
260
345
  />
261
- ) : (
346
+ )
347
+ })}
348
+ {barElements}
349
+ {(isAreaChart && !isComboChart ? [] : isComboChart ? lineSeries : series).map((s, i) => {
350
+ const seriesKey = s.dataKey
351
+ const seriesColor = colorScale?.(config.runtime.seriesLabels?.[seriesKey] || seriesKey) || '#666'
352
+
353
+ // Get series-specific styling
354
+ const seriesWeight = s.weight || 2
355
+ const seriesLineType = s.lineType || 'curveLinear'
356
+ const seriesStyle = s.type || 'solid'
357
+ const curve = allCurves[seriesLineType] || allCurves.curveLinear
358
+ const strokeDasharray = handleLineType(seriesStyle)
359
+
360
+ // Use the right-axis scale when the series is on the right axis
361
+ const yScaleForSeries = s.axis === 'Right' && miniRightYScale ? miniRightYScale : miniYScale
362
+
363
+ // Filter to only valid data points, then sort by X position so the line
364
+ // connects left-to-right regardless of the original data order.
365
+ const validData = tableData
366
+ .filter(d => {
367
+ const xVal = xScale(d[dataKey])
368
+ const yVal = parseFloat(d[s.dataKey])
369
+ return xVal !== undefined && !isNaN(yVal)
370
+ })
371
+ .sort((a, b) => (xScale(a[dataKey]) ?? 0) - (xScale(b[dataKey]) ?? 0))
372
+
373
+ if (validData.length === 0) return null
374
+
375
+ const getX = d => xScale(d[dataKey]) + bandwidth / 2
376
+ const getY = d => yScaleForSeries(parseFloat(d[s.dataKey]))
377
+
378
+ return (
262
379
  <LinePath
263
380
  key={`mini-line-${seriesKey}-${i}`}
264
381
  data={validData}
@@ -653,6 +653,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
653
653
  visHasLegendAxisAlign,
654
654
  visHasLegendColorCategory,
655
655
  visHasSelectableLegendValues,
656
+ visSupportsClickingLegend,
656
657
  visSupportsDateCategoryAxis,
657
658
  visSupportsDateCategoryAxisLabel,
658
659
  visSupportsDateCategoryAxisLine,
@@ -756,6 +757,10 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
756
757
  if (isDateScale(updatedConfig.xAxis) && !updatedConfig.xAxis.padding) {
757
758
  updatedConfig.xAxis.padding = 0
758
759
  }
760
+ // Default Radar charts to a taller height
761
+ if (updatedConfig.visualizationType === 'Radar' && updatedConfig.heights?.vertical <= 400) {
762
+ updatedConfig.heights.vertical = 400
763
+ }
759
764
  // DEV-8008 - Remove Bar styling when Line is converted to Bar
760
765
  if (updatedConfig.visualizationType === 'Line') {
761
766
  updatedConfig.visualizationSubType = 'regular'
@@ -1892,6 +1897,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
1892
1897
  </AccordionItem>
1893
1898
  )}
1894
1899
  <Panels.BoxPlot name='Measures' />
1900
+ <Panels.Radar name='Radar Chart Settings' />
1895
1901
  {/* Left Value Axis */}
1896
1902
  {visSupportsLeftValueAxis() && (
1897
1903
  <AccordionItem>
@@ -4250,7 +4256,10 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4250
4256
  />
4251
4257
 
4252
4258
  <CheckBox
4253
- display={config.preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value)}
4259
+ display={
4260
+ config.visualizationType !== 'Radar' &&
4261
+ config.preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value)
4262
+ }
4254
4263
  value={config.legend.hideSuppressedLabels}
4255
4264
  section='legend'
4256
4265
  fieldName='hideSuppressedLabels'
@@ -4274,7 +4283,10 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4274
4283
  }
4275
4284
  />
4276
4285
  <CheckBox
4277
- display={config.preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value)}
4286
+ display={
4287
+ config.visualizationType !== 'Radar' &&
4288
+ config.preliminaryData?.some(pd => pd.label && pd.type === 'suppression' && pd.value)
4289
+ }
4278
4290
  value={config.legend.hideSuppressionLink}
4279
4291
  section='legend'
4280
4292
  fieldName='hideSuppressionLink'
@@ -4296,7 +4308,7 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4296
4308
  />
4297
4309
 
4298
4310
  <Select
4299
- display={hasDynamicCategory || hasMultipleSeries}
4311
+ display={visSupportsClickingLegend() && (hasDynamicCategory || hasMultipleSeries)}
4300
4312
  value={config.legend.behavior}
4301
4313
  section='legend'
4302
4314
  fieldName='behavior'
@@ -4580,14 +4592,16 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
4580
4592
  )}
4581
4593
  <Panels.Annotate name='Text Annotations' />
4582
4594
  {/* {(config.visualizationType === 'Bar' || config.visualizationType === 'Line') && <Panels.DateHighlighting name='Date Highlighting' />} */}
4583
- <PanelMarkup
4584
- name='Markup Variables'
4585
- markupVariables={config.markupVariables || []}
4586
- data={rawData}
4587
- enableMarkupVariables={config.enableMarkupVariables || false}
4588
- onMarkupVariablesChange={variables => updateField(null, null, 'markupVariables', variables)}
4589
- onToggleEnable={enabled => updateField(null, null, 'enableMarkupVariables', enabled)}
4590
- />
4595
+ {config.visualizationType !== 'Radar' && (
4596
+ <PanelMarkup
4597
+ name='Markup Variables'
4598
+ markupVariables={config.markupVariables || []}
4599
+ data={rawData}
4600
+ enableMarkupVariables={config.enableMarkupVariables || false}
4601
+ onMarkupVariablesChange={variables => updateField(null, null, 'markupVariables', variables)}
4602
+ onToggleEnable={enabled => updateField(null, null, 'enableMarkupVariables', enabled)}
4603
+ />
4604
+ )}
4591
4605
  <Panels.SmallMultiples name='Small Multiples' />
4592
4606
  </Accordion>
4593
4607
  {config.type !== 'Spark Line' && (
@@ -1,5 +1,6 @@
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'
@@ -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,6 +148,59 @@ 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 />
@@ -206,6 +206,86 @@ const PanelGeneral: FC<PanelProps> = props => {
206
206
  options={['Below Bar', 'On Date/Category Axis']}
207
207
  />
208
208
  )}
209
+ {visualizationType === 'Horizon Chart' && (
210
+ <>
211
+ <TextField
212
+ type='number'
213
+ value={config.horizon?.numLayers || 4}
214
+ section='horizon'
215
+ fieldName='numLayers'
216
+ label='Number of Layers'
217
+ updateField={updateField}
218
+ min={1}
219
+ max={9}
220
+ onBlur={e => {
221
+ const parsed = Number(e.target.value)
222
+ if (!isNaN(parsed)) {
223
+ const value = Math.round(parsed)
224
+ const clamped = Math.min(9, Math.max(1, value))
225
+ if (clamped !== parsed) {
226
+ updateField('horizon', null, 'numLayers', clamped)
227
+ }
228
+ }
229
+ }}
230
+ tooltip={
231
+ <Tooltip style={{ textTransform: 'none' }}>
232
+ <Tooltip.Target>
233
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
234
+ </Tooltip.Target>
235
+ <Tooltip.Content>
236
+ <p>
237
+ The number of layers determines how many "bands" the horizon chart is divided into. The optimal
238
+ number of layers may depend on the data range and the desired level of detail.
239
+ </p>
240
+ <hr />
241
+ <p>Valid range: 1-9. Defaults to 4 layers.</p>
242
+ </Tooltip.Content>
243
+ </Tooltip>
244
+ }
245
+ />
246
+ <TextField
247
+ type='number'
248
+ value={config.horizon?.bandGap ?? 15}
249
+ section='horizon'
250
+ fieldName='bandGap'
251
+ label='Band Gap'
252
+ updateField={updateField}
253
+ tooltip={
254
+ <Tooltip style={{ textTransform: 'none' }}>
255
+ <Tooltip.Target>
256
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
257
+ </Tooltip.Target>
258
+ <Tooltip.Content>
259
+ <p>The vertical spacing (in pixels) between each series row in the horizon chart.</p>
260
+ <hr />
261
+ <p>Defaults to 15 pixels.</p>
262
+ </Tooltip.Content>
263
+ </Tooltip>
264
+ }
265
+ />
266
+ <TextField
267
+ type='number'
268
+ value={config.horizon?.bottomPadding ?? 15}
269
+ section='horizon'
270
+ fieldName='bottomPadding'
271
+ label='Bottom Padding'
272
+ updateField={updateField}
273
+ tooltip={
274
+ <Tooltip style={{ textTransform: 'none' }}>
275
+ <Tooltip.Target>
276
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
277
+ </Tooltip.Target>
278
+ <Tooltip.Content>
279
+ <p>The padding (in pixels) below the last series row in the horizon chart.</p>
280
+ <hr />
281
+ <p>Defaults to 15 pixels.</p>
282
+ </Tooltip.Content>
283
+ </Tooltip>
284
+ }
285
+ />
286
+ </>
287
+ )}
288
+
209
289
  {visHasNumbersOnBars() ? (
210
290
  <CheckBox
211
291
  value={config.yAxis.displayNumbersOnBar}
@@ -316,7 +396,7 @@ const PanelGeneral: FC<PanelProps> = props => {
316
396
  />
317
397
  </>
318
398
  )}
319
- {config.visualizationType !== 'Warming Stripes' && (
399
+ {config.visualizationType !== 'Warming Stripes' && config.visualizationType !== 'Radar' && (
320
400
  <CheckBox
321
401
  tooltip={
322
402
  <Tooltip style={{ textTransform: 'none' }}>
@@ -311,7 +311,7 @@ const PanelPatternSettings: FC<PanelProps> = props => {
311
311
  updateConfig(updatedConfig)
312
312
  }
313
313
 
314
- if (config.visualizationType === 'Warming Stripes') return
314
+ if (config.visualizationType === 'Warming Stripes' || config.visualizationType === 'Radar') return
315
315
 
316
316
  return (
317
317
  <AccordionItem>