@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
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cdc/chart",
3
- "version": "4.26.3",
3
+ "version": "4.26.5",
4
4
  "description": "React component for visualizing tabular data in various types of charts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Matthew Pallansch <mpallansch@adittech.com>",
7
7
  "bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
8
8
  "dependencies": {
9
- "@cdc/core": "^4.26.3",
9
+ "@cdc/core": "^4.26.5",
10
10
  "@hello-pangea/dnd": "^16.2.0",
11
11
  "@react-spring/web": "^9.7.5",
12
12
  "@rollup/plugin-dsv": "^3.0.2",
@@ -18,6 +18,7 @@
18
18
  "@visx/event": "^3.12.0",
19
19
  "@visx/glyph": "^3.12.0",
20
20
  "@visx/gradient": "^3.12.0",
21
+ "@visx/heatmap": "^3.12.0",
21
22
  "@visx/legend": "^3.12.0",
22
23
  "@visx/marker": "^3.12.0",
23
24
  "@visx/mock-data": "^3.12.0",
@@ -34,7 +35,7 @@
34
35
  "d3-format": "^3.1.2",
35
36
  "d3-sankey": "^0.12.3",
36
37
  "d3-time-format": "^4.1.0",
37
- "dompurify": "^3.3.1",
38
+ "dompurify": "^3.4.0",
38
39
  "html-react-parser": "^5.2.3",
39
40
  "js-base64": "^2.5.2",
40
41
  "lodash": "^4.17.23",
@@ -56,7 +57,7 @@
56
57
  "@types/d3-sankey": "^0.12.4",
57
58
  "@types/d3-time-format": "^4.0.3"
58
59
  },
59
- "gitHead": "d50e45a074fbefa56cac904917e707d57f237737",
60
+ "gitHead": "61c025165d96b45a6002c34582c5a622a9d865a9",
60
61
  "main": "dist/cdcchart",
61
62
  "moduleName": "CdcChart",
62
63
  "peerDependencies": {
@@ -27,7 +27,6 @@ import parse from 'html-react-parser'
27
27
  import cloneDeep from 'lodash/cloneDeep'
28
28
  import defaultsDeep from 'lodash/defaultsDeep'
29
29
  import lodashDefaults from 'lodash/defaults'
30
- import findKey from 'lodash/findKey'
31
30
  import forEach from 'lodash/forEach'
32
31
  import get from 'lodash/get'
33
32
  import isEmpty from 'lodash/isEmpty'
@@ -44,6 +43,7 @@ import ConfigContext, { ChartDispatchContext } from './ConfigContext'
44
43
  import PieChart from './components/PieChart'
45
44
  import RadarChart from './components/RadarChart'
46
45
  import SankeyChart from './components/Sankey'
46
+ import HeatMap, { HeatMapGradientLegend } from './components/HeatMap'
47
47
  import LinearChart from './components/LinearChart'
48
48
  import { isDateScale, formatDate as coreFormatDate } from '@cdc/core/helpers/cove/date'
49
49
 
@@ -74,9 +74,11 @@ import { getVisibleAnnotations } from './components/Annotations/helpers/getVisib
74
74
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
75
75
  import { backfillDefaults } from '@cdc/core/helpers/backfillDefaults'
76
76
  import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
77
+ import { getAxisLabelFontSize } from './helpers/axisLabelFontSize'
77
78
  import { missingRequiredSections } from '@cdc/core/helpers/missingRequiredSections'
78
79
  import { filterVizData } from '@cdc/core/helpers/filterVizData'
79
80
  import { addValuesToFilters } from '@cdc/core/helpers/addValuesToFilters'
81
+ import { hasVisibleVizFilters } from '@cdc/core/helpers/filterVisibility'
80
82
  import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
81
83
  import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
82
84
  import numberFromString from '@cdc/core/helpers/numberFromString'
@@ -93,8 +95,12 @@ import { getComboChartConfig } from './helpers/getComboChartConfig'
93
95
  import { getExcludedData } from './helpers/getExcludedData'
94
96
  import { getColorScale } from './helpers/getColorScale'
95
97
  import { getTransformedData } from './helpers/getTransformedData'
98
+ import { getLegendHighlightKey, shouldResetSeriesHighlight } from './helpers/seriesHighlight'
96
99
  import { getPiePercent } from './helpers/getPiePercent'
97
100
  import { prepareSmallMultiplesDataTable } from './helpers/smallMultiplesHelpers'
101
+ import { calcInitialHeight } from './helpers/sizeHelpers'
102
+ import { ensureSpecialChartAxisTypes } from './helpers/ensureSpecialChartAxisTypes'
103
+ import { findColumnConfigByName } from './helpers/seriesColumnSettings'
98
104
 
99
105
  // styles
100
106
  import './scss/main.scss'
@@ -147,6 +153,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
147
153
  excludedData,
148
154
  filteredData,
149
155
  currentViewport,
156
+ vizViewport,
150
157
  isLoading,
151
158
  dimensions,
152
159
  container,
@@ -300,6 +307,17 @@ const CdcChart: React.FC<CdcChartProps> = ({
300
307
  }
301
308
  }
302
309
 
310
+ if (loadedConfig?.visualizationType === 'HeatMap' && !loadedConfig?.general?.palette) {
311
+ if (!defaultsWithoutPalette.general) {
312
+ defaultsWithoutPalette.general = {}
313
+ }
314
+ defaultsWithoutPalette.general.palette = {
315
+ isReversed: false,
316
+ version: '2.0',
317
+ name: 'sequential_blue'
318
+ }
319
+ }
320
+
303
321
  let newConfig = { ...defaultsWithoutPalette, ...loadedConfig }
304
322
 
305
323
  // Ensure Horizon Chart has enough palette colors for all layers
@@ -322,9 +340,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
322
340
  })
323
341
  })
324
342
 
325
- if (newConfig.visualizationType === 'Bump Chart') {
326
- newConfig.xAxis.type === 'date-time'
327
- }
343
+ ensureSpecialChartAxisTypes(newConfig)
328
344
  if (!isDashboard) return coveUpdateWorker(newConfig)
329
345
  return newConfig
330
346
  }
@@ -380,6 +396,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
380
396
  const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
381
397
  const newConfig = cloneConfig(_config)
382
398
  let data = dataOverride || stateData
399
+ const shouldUseHeatMapSideTitlePlacement =
400
+ newConfig.visualizationType === 'HeatMap' && !newConfig.yAxis?.titlePlacement
401
+
402
+ ensureSpecialChartAxisTypes(newConfig)
383
403
 
384
404
  data = handleRankByValue(data, newConfig)
385
405
 
@@ -388,6 +408,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
388
408
 
389
409
  // Backfill missing properties from defaults, respecting legacy values
390
410
  backfillDefaults(newConfig, defaults, LEGACY_CHART_DEFAULTS)
411
+ if (shouldUseHeatMapSideTitlePlacement) {
412
+ newConfig.yAxis.titlePlacement = 'side'
413
+ }
391
414
 
392
415
  // Auto-populate table.defaultSort for date-axis charts if not already set by user
393
416
  const hasDateAxisType = ['date-time', 'date'].includes(newConfig.xAxis?.type)
@@ -528,6 +551,31 @@ const CdcChart: React.FC<CdcChartProps> = ({
528
551
  }
529
552
  }
530
553
 
554
+ if (newConfig.visualizationType === 'HeatMap') {
555
+ const heatMapSeriesKeys = newConfig.series.map(series => series.dataKey)
556
+ const heatMapSeriesLabels = newConfig.series.reduce<Record<string, string>>((acc, series) => {
557
+ const heatMapColumnConfig = findColumnConfigByName(newConfig.columns || {}, series.dataKey)?.columnConfig
558
+ const configuredColumnLabel = heatMapColumnConfig?.label
559
+ const hasCustomColumnLabel = configuredColumnLabel && configuredColumnLabel !== series.dataKey
560
+ acc[series.dataKey] = hasCustomColumnLabel ? configuredColumnLabel : series.name || series.dataKey
561
+ return acc
562
+ }, {})
563
+
564
+ newConfig.legend = {
565
+ ...newConfig.legend,
566
+ position: newConfig.legend?.position || 'top',
567
+ style: newConfig.legend?.style || 'gradient',
568
+ subStyle: newConfig.legend?.subStyle || 'smooth'
569
+ }
570
+ newConfig.yAxis = {
571
+ ...newConfig.yAxis,
572
+ type: 'categorical'
573
+ }
574
+ newConfig.runtime.seriesKeys = heatMapSeriesKeys
575
+ newConfig.runtime.seriesLabelsAll = heatMapSeriesKeys
576
+ newConfig.runtime.seriesLabels = heatMapSeriesLabels
577
+ }
578
+
531
579
  if (isHorizontalVariant) {
532
580
  // For horizontal charts, axes are swapped, so processedYAxis goes to runtime.xAxis and vice versa
533
581
  const horizontalXAxisSource = cloneDeep((newConfig.yAxis as any)?.yAxis || newConfig.yAxis)
@@ -540,7 +588,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
540
588
  ...horizontalYAxisSource,
541
589
  label: runtimeYAxisLabel ?? horizontalYAxisSource?.label
542
590
  }
543
- newConfig.runtime.yAxis.labelOffset *= -1
544
591
 
545
592
  newConfig.runtime.horizontal = false
546
593
  newConfig.orientation = 'horizontal'
@@ -580,6 +627,19 @@ const CdcChart: React.FC<CdcChartProps> = ({
580
627
  } else {
581
628
  newConfig.runtime.editorErrorMessage = ''
582
629
  }
630
+ } else if (newConfig.visualizationType === 'HeatMap') {
631
+ const missingXColumn = !newConfig.xAxis.dataKey || newConfig.xAxis.dataKey === ''
632
+ const missingSeries = !Array.isArray(newConfig.series) || newConfig.series.length === 0
633
+
634
+ if (missingXColumn && missingSeries) {
635
+ newConfig.runtime.editorErrorMessage = 'Date/Category Axis and Data Series must be set for heatmaps.'
636
+ } else if (missingXColumn) {
637
+ newConfig.runtime.editorErrorMessage = 'Date/Category Axis must be set for heatmaps.'
638
+ } else if (missingSeries) {
639
+ newConfig.runtime.editorErrorMessage = 'Data Series must be set for heatmaps.'
640
+ } else {
641
+ newConfig.runtime.editorErrorMessage = ''
642
+ }
583
643
  } else {
584
644
  // No errors
585
645
  newConfig.runtime.editorErrorMessage = ''
@@ -871,17 +931,15 @@ const CdcChart: React.FC<CdcChartProps> = ({
871
931
  }, [config, stateData, getProcessedAxisLabels, dispatch, editorContext, isEditor, isDashboard])
872
932
 
873
933
  // Called on legend click, highlights/unhighlights the data series with the given label
874
- const highlight = (label: Label): void => {
934
+ const highlight = (label: Label | string): void => {
935
+ const newHighlight = getLegendHighlightKey(config.runtime.seriesLabels, label)
936
+
875
937
  if (
876
- seriesHighlight.length + 1 === config.runtime.seriesKeys.length &&
877
- config.visualizationType !== 'Forecasting' &&
878
- !seriesHighlight.includes(label.datum)
938
+ shouldResetSeriesHighlight(seriesHighlight, config.runtime.seriesKeys, newHighlight, config.visualizationType)
879
939
  ) {
880
940
  return handleShowAll()
881
941
  }
882
942
 
883
- const newHighlight = findKey(config.runtime.seriesLabels, v => v === label.datum) || label.datum
884
-
885
943
  const newSeriesHighlight = xor(seriesHighlight, [newHighlight])
886
944
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newSeriesHighlight })
887
945
  }
@@ -1200,6 +1258,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1200
1258
 
1201
1259
  // Filter annotations to only those visible in current data view
1202
1260
  const visibleAnnotations = getVisibleAnnotations(config.annotations, transformedData, config.xAxis?.dataKey)
1261
+ const isHeatMap = config.visualizationType === 'HeatMap'
1203
1262
  const isTp5Treatment = ENABLE_CHART_MAP_TP5_TREATMENT && config.visual?.tp5Treatment
1204
1263
  const visualSettingClasses = new Set([
1205
1264
  'component--has-border-color-theme',
@@ -1220,6 +1279,12 @@ const CdcChart: React.FC<CdcChartProps> = ({
1220
1279
  bodyClasses.push('component--hide-background-color')
1221
1280
  }
1222
1281
  if (isTp5Treatment && !bodyClasses.includes('no-borders')) bodyClasses.push('no-borders')
1282
+ const bodyWrapClasses = [
1283
+ isTp5Treatment ? 'cdc-callout d-flex flex-column tp5-chart-callout' : '',
1284
+ isHeatMap ? 'cdc-heatmap__body-wrap' : ''
1285
+ ]
1286
+ .filter(Boolean)
1287
+ .join(' ')
1223
1288
  const chartTitle = (
1224
1289
  <Title
1225
1290
  showTitle={config.showTitle}
@@ -1242,7 +1307,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
1242
1307
  return kebabCase(string)
1243
1308
  }
1244
1309
  const getChartWrapperClasses = () => {
1245
- const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
1246
1310
  const classes = ['chart-container', 'visualization-container', 'p-relative']
1247
1311
  const visualSettingClasses = ['component--has-border-color-theme', 'component--has-accent']
1248
1312
  if (legend?.position) {
@@ -1253,11 +1317,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
1253
1317
  }
1254
1318
  }
1255
1319
  if (legend?.hide) classes.push('legend-hidden')
1320
+ if (isHeatMap) classes.push('cdc-heatmap__container')
1256
1321
  if (contentClasses.includes('sparkline')) classes.push('sparkline')
1257
1322
  if (lineDatapointClass) classes.push(lineDatapointClass)
1258
1323
  if (!config.barHasBorder) classes.push('chart-bar--no-border')
1259
- if (config.xAxis.brushActive && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || legend.hide))
1260
- classes.push('dashboard-brush')
1261
1324
 
1262
1325
  if (!ENABLE_CHART_VISUAL_SETTINGS) {
1263
1326
  const filtered = classes.filter(className => !visualSettingClasses.includes(className))
@@ -1276,6 +1339,34 @@ const CdcChart: React.FC<CdcChartProps> = ({
1276
1339
  return classes
1277
1340
  }
1278
1341
 
1342
+ const showTopYAxisTitle =
1343
+ config.yAxis?.titlePlacement === 'top' &&
1344
+ !config.smallMultiples?.mode &&
1345
+ !config.hideYAxisLabel &&
1346
+ Boolean(config.runtime?.yAxis?.label)
1347
+ const topYAxisTitleFontSize = getAxisLabelFontSize(vizViewport)
1348
+
1349
+ const renderTopYAxisTitle = () =>
1350
+ showTopYAxisTitle ? (
1351
+ <div className='y-axis-top-title' style={{ fontSize: `${topYAxisTitleFontSize}px` }}>
1352
+ {config.runtime.yAxis.label}
1353
+ </div>
1354
+ ) : null
1355
+
1356
+ const renderLinearChartWithParentSize = (
1357
+ getParentWidth: (parent: { width: number; height: number }) => number = parent => parent.width,
1358
+ wrapperStyle: React.CSSProperties = { width: '100%' }
1359
+ ) => (
1360
+ <>
1361
+ {renderTopYAxisTitle()}
1362
+ <div ref={parentRef} style={wrapperStyle}>
1363
+ <ParentSize>
1364
+ {parent => <LinearChart ref={svgRef} parentWidth={getParentWidth(parent)} parentHeight={parent.height} />}
1365
+ </ParentSize>
1366
+ </div>
1367
+ </>
1368
+ )
1369
+
1279
1370
  if (!isLoading) {
1280
1371
  const tableLink = (
1281
1372
  <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
@@ -1288,11 +1379,11 @@ const CdcChart: React.FC<CdcChartProps> = ({
1288
1379
  {!missingRequiredSections(config) && !config.newViz && (
1289
1380
  <VisualizationContent
1290
1381
  innerClassName={`type-${makeClassName(config.visualizationType)}`}
1291
- innerProps={{ 'aria-label': handleChartAriaLabels(config), tabIndex: 0 }}
1382
+ innerProps={{ tabIndex: 0 }}
1292
1383
  bodyClassName={bodyClasses.join(' ')}
1293
- bodyWrapClassName={isTp5Treatment ? 'cdc-callout d-flex flex-column tp5-chart-callout' : ''}
1384
+ bodyWrapClassName={bodyWrapClasses}
1294
1385
  filters={
1295
- config.filters?.length > 0 && !externalFilters && config.visualizationType !== 'Spark Line' ? (
1386
+ hasVisibleVizFilters(config.filters) && !externalFilters && config.visualizationType !== 'Spark Line' ? (
1296
1387
  <Filters
1297
1388
  config={config}
1298
1389
  setFilters={setFilters}
@@ -1304,7 +1395,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1304
1395
  }
1305
1396
  bodySubtext={
1306
1397
  processedDescription && config.visualizationType !== 'Spark Line' ? (
1307
- <div className={getChartSubTextClasses().join(' ')}>{parse(processedDescription)}</div>
1398
+ <div className={`cove-prose ${getChartSubTextClasses().join(' ')}`}>{parse(processedDescription)}</div>
1308
1399
  ) : null
1309
1400
  }
1310
1401
  bodyFooter={
@@ -1391,21 +1482,26 @@ const CdcChart: React.FC<CdcChartProps> = ({
1391
1482
  )}
1392
1483
  {visibleAnnotations.length > 0 && <Annotation.Dropdown />}
1393
1484
  {processedLegacyFootnotes && (
1394
- <section className='footnotes pt-2 mt-4'>{parse(processedLegacyFootnotes)}</section>
1485
+ <section className='footnotes cove-prose pt-2 mt-4'>{parse(processedLegacyFootnotes)}</section>
1395
1486
  )}
1396
1487
  </>
1397
1488
  }
1398
1489
  header={isTp5Treatment ? null : chartTitle}
1399
1490
  messageIsIntroText={config.visualizationType !== 'Spark Line' && !!processedIntroText}
1400
- message={config.visualizationType !== 'Spark Line' && processedIntroText ? parse(processedIntroText) : null}
1491
+ message={
1492
+ config.visualizationType !== 'Spark Line' && processedIntroText ? (
1493
+ <div className='cove-prose'>{parse(processedIntroText)}</div>
1494
+ ) : null
1495
+ }
1401
1496
  footer={
1402
1497
  <FootnotesStandAlone
1403
- config={configObj.footnotes}
1498
+ config={config.footnotes}
1404
1499
  filters={config.filters?.filter(f => f.filterFootnotes)}
1405
1500
  markupVariables={config.markupVariables}
1406
1501
  enableMarkupVariables={config.enableMarkupVariables}
1407
1502
  data={config.data}
1408
1503
  dataMetadata={config.dataMetadata}
1504
+ footerClassName='cove-visualization__footnotes'
1409
1505
  />
1410
1506
  }
1411
1507
  >
@@ -1443,15 +1539,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
1443
1539
  {/* All charts with LinearChart */}
1444
1540
  {filteredData &&
1445
1541
  filteredData.length > 0 &&
1446
- !['Spark Line', 'Line', 'Sankey', 'Pie', 'Radar'].includes(config.visualizationType) && (
1447
- <div ref={parentRef} style={{ width: `100%` }}>
1448
- <ParentSize>
1449
- {parent => (
1450
- <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1451
- )}
1452
- </ParentSize>
1453
- </div>
1454
- )}
1542
+ !['Spark Line', 'Line', 'Sankey', 'Pie', 'Radar', 'HeatMap'].includes(config.visualizationType) &&
1543
+ renderLinearChartWithParentSize()}
1455
1544
 
1456
1545
  {filteredData && filteredData.length > 0 && config.visualizationType === 'Pie' && (
1457
1546
  <ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
@@ -1478,38 +1567,31 @@ const CdcChart: React.FC<CdcChartProps> = ({
1478
1567
  )}
1479
1568
  </ParentSize>
1480
1569
  )}
1570
+ {filteredData && filteredData.length > 0 && config.visualizationType === 'HeatMap' && (
1571
+ <div
1572
+ ref={parentRef}
1573
+ style={{ width: '100%', height: `${calcInitialHeight(config, currentViewport) || 400}px` }}
1574
+ >
1575
+ <ParentSize>
1576
+ {parent => <HeatMap parentWidth={parent.width} parentHeight={parent.height} />}
1577
+ </ParentSize>
1578
+ </div>
1579
+ )}
1481
1580
  {/* Line Chart */}
1482
1581
  {filteredData &&
1483
1582
  filteredData.length > 0 &&
1484
1583
  config.visualizationType === 'Line' &&
1485
- (convertLineToBarGraph ? (
1486
- <div ref={parentRef} style={{ width: `100%` }}>
1487
- <ParentSize>
1488
- {parent => (
1489
- <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1490
- )}
1491
- </ParentSize>
1492
- </div>
1493
- ) : (
1494
- <div ref={parentRef} style={{ width: '100%' }}>
1495
- <ParentSize>
1496
- {parent => {
1497
- const labelMargin = 120
1498
- const widthReduction =
1499
- config.showLineSeriesLabels && (config.legend.position !== 'right' || config.legend.hide)
1500
- ? labelMargin
1501
- : 0
1502
- return (
1503
- <LinearChart
1504
- ref={svgRef}
1505
- parentWidth={parent.width - widthReduction}
1506
- parentHeight={parent.height}
1507
- />
1508
- )
1509
- }}
1510
- </ParentSize>
1511
- </div>
1512
- ))}
1584
+ (convertLineToBarGraph
1585
+ ? renderLinearChartWithParentSize()
1586
+ : renderLinearChartWithParentSize(parent => {
1587
+ const labelMargin = 120
1588
+ const widthReduction =
1589
+ config.showLineSeriesLabels && (config.legend.position !== 'right' || config.legend.hide)
1590
+ ? labelMargin
1591
+ : 0
1592
+
1593
+ return parent.width - widthReduction
1594
+ }))}
1513
1595
  {/* Sparkline */}
1514
1596
  {config.visualizationType === 'Spark Line' && (
1515
1597
  <>
@@ -1521,7 +1603,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1521
1603
  interactionLabel={interactionLabel}
1522
1604
  />
1523
1605
  {processedIntroText && (
1524
- <section className='introText' style={{ padding: '0px 0 35px' }}>
1606
+ <section className='introText cove-prose' style={{ padding: '0px 0 35px' }}>
1525
1607
  {parse(processedIntroText)}
1526
1608
  </section>
1527
1609
  )}
@@ -1529,7 +1611,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1529
1611
  <ParentSize>{parent => <SparkLine width={parent.width} height={parent.height} />}</ParentSize>
1530
1612
  </div>
1531
1613
  {processedDescription && (
1532
- <div className='subtext' style={{ padding: '35px 0 1.5rem' }}>
1614
+ <div className='subtext cove-prose' style={{ padding: '35px 0 1.5rem' }}>
1533
1615
  {parse(processedDescription)}
1534
1616
  </div>
1535
1617
  )}
@@ -1546,6 +1628,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1546
1628
  {!config.legend.hide &&
1547
1629
  config.visualizationType !== 'Spark Line' &&
1548
1630
  config.visualizationType !== 'Sankey' &&
1631
+ config.visualizationType !== 'HeatMap' &&
1549
1632
  !(config.visualizationType === 'Warming Stripes' && config.legend?.style === 'gradient') &&
1550
1633
  !(config.visualizationType === 'Warming Stripes' && config.smallMultiples?.mode) && (
1551
1634
  <Legend
@@ -1557,6 +1640,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1557
1640
  {config.visualizationType === 'Warming Stripes' &&
1558
1641
  config.legend?.style === 'gradient' &&
1559
1642
  !config.smallMultiples?.mode && <WarmingStripesGradientLegend />}
1643
+ {config.visualizationType === 'HeatMap' && <HeatMapGradientLegend />}
1560
1644
  </LegendWrapper>
1561
1645
  </div>
1562
1646
  </VisualizationContent>
@@ -0,0 +1,136 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { within, expect } from 'storybook/test'
3
+ import Chart from '../CdcChartComponent'
4
+ import {
5
+ assertVisualizationRendered,
6
+ waitForPresence,
7
+ waitForEditor,
8
+ openAccordion,
9
+ MIN_ANIMATION_DELAY_MS
10
+ } from '@cdc/core/helpers/testing'
11
+ import altTextConfig from './_mock/alt_text_metadata.json'
12
+
13
+ const meta: Meta<typeof Chart> = {
14
+ title: 'Components/Templates/Chart/Accessible Alt Text',
15
+ component: Chart,
16
+ parameters: {
17
+ docs: {
18
+ description: {
19
+ component:
20
+ 'Demonstrates SVG accessibility: auto-generated aria-label with optional configurable description concatenated into the label for reliable screen reader support.'
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ export default meta
27
+ type Story = StoryObj<typeof Chart>
28
+
29
+ export const MetadataDescription: Story = {
30
+ args: {
31
+ config: {
32
+ ...altTextConfig,
33
+ altText: { type: 'metadata', metadataKey: 'altDescription' }
34
+ },
35
+ isEditor: false
36
+ },
37
+ parameters: {
38
+ docs: {
39
+ description: {
40
+ story:
41
+ 'Description pulled from dataMetadata is concatenated into the aria-label after the auto-generated title for reliable screen reader support.'
42
+ }
43
+ }
44
+ },
45
+ play: async ({ canvasElement }) => {
46
+ await assertVisualizationRendered(canvasElement)
47
+ const svg = await waitForPresence('svg[role="img"]', canvasElement)
48
+
49
+ const expected = `Line chart with the title: COVID-19 Test Positivity. ${altTextConfig.dataMetadata.altDescription}`
50
+ expect(svg?.getAttribute('aria-label')).toBe(expected)
51
+ }
52
+ }
53
+
54
+ export const StaticDescription: Story = {
55
+ args: {
56
+ config: {
57
+ ...altTextConfig,
58
+ altText: {
59
+ type: 'static',
60
+ value: 'COVID-19 positivity has doubled in 8 weeks, suggesting increased community transmission.'
61
+ }
62
+ },
63
+ isEditor: false
64
+ },
65
+ parameters: {
66
+ docs: {
67
+ description: {
68
+ story: 'Static manually written description concatenated with the auto-generated title.'
69
+ }
70
+ }
71
+ },
72
+ play: async ({ canvasElement }) => {
73
+ await assertVisualizationRendered(canvasElement)
74
+ const svg = await waitForPresence('svg[role="img"]', canvasElement)
75
+
76
+ expect(svg?.getAttribute('aria-label')).toBe(
77
+ 'Line chart with the title: COVID-19 Test Positivity. COVID-19 positivity has doubled in 8 weeks, suggesting increased community transmission.'
78
+ )
79
+ }
80
+ }
81
+
82
+ export const AutoGenerated: Story = {
83
+ args: {
84
+ config: altTextConfig,
85
+ isEditor: false
86
+ },
87
+ parameters: {
88
+ docs: {
89
+ description: {
90
+ story:
91
+ 'No altText configured. Falls back to the auto-generated aria-label: "Line chart with the title: COVID-19 Test Positivity".'
92
+ }
93
+ }
94
+ },
95
+ play: async ({ canvasElement }) => {
96
+ await assertVisualizationRendered(canvasElement)
97
+ const svg = await waitForPresence('svg[role="img"]', canvasElement)
98
+
99
+ expect(svg?.getAttribute('aria-label')).toBe('Line chart with the title: COVID-19 Test Positivity')
100
+ }
101
+ }
102
+
103
+ export const EditorWithMetadata: Story = {
104
+ args: {
105
+ config: {
106
+ ...altTextConfig,
107
+ altText: { type: 'metadata', metadataKey: 'altDescription' }
108
+ },
109
+ isEditor: true
110
+ },
111
+ parameters: {
112
+ docs: {
113
+ description: {
114
+ story:
115
+ 'Editor mode showing the alt text description control in the General accordion with metadata-driven description.'
116
+ }
117
+ }
118
+ },
119
+ play: async ({ canvasElement }) => {
120
+ const canvas = within(canvasElement)
121
+ await waitForEditor(canvas)
122
+
123
+ if (MIN_ANIMATION_DELAY_MS > 0) {
124
+ await new Promise(resolve => setTimeout(resolve, MIN_ANIMATION_DELAY_MS))
125
+ }
126
+
127
+ await openAccordion(canvas, 'General')
128
+
129
+ if (MIN_ANIMATION_DELAY_MS > 0) {
130
+ await new Promise(resolve => setTimeout(resolve, MIN_ANIMATION_DELAY_MS))
131
+ }
132
+
133
+ const descPreview = await waitForPresence('[data-testid="alt-text-desc-preview"]', canvasElement)
134
+ expect(descPreview?.textContent).toContain('COVID-19 test positivity')
135
+ }
136
+ }
@@ -493,6 +493,16 @@ export const PieChartWithHTMLFootnotes: Story = {
493
493
  Footnote: '<strong>7 states</strong> with 60-79% coverage<br>Target: Move to <em>high category</em>'
494
494
  }
495
495
  ],
496
+ xAxis: {
497
+ label: 'Coverage Category',
498
+ dataKey: 'Category',
499
+ type: 'categorical'
500
+ },
501
+ yAxis: {
502
+ label: 'Percentage',
503
+ dataKey: 'Percentage',
504
+ type: 'linear'
505
+ },
496
506
  series: [
497
507
  {
498
508
  dataKey: 'Percentage',