@cdc/chart 4.26.4 → 4.26.6

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 (141) hide show
  1. package/CONFIG.md +353 -29
  2. package/LICENSE +201 -0
  3. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  4. package/dist/cdcchart.js +37885 -35117
  5. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  6. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  7. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  8. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  9. package/examples/private/bad-y-axis.json +543 -0
  10. package/examples/private/bar-chart-negative-value.json +455 -0
  11. package/examples/private/bump-chart.json +1116 -0
  12. package/examples/private/test-123.json +624 -0
  13. package/package.json +4 -3
  14. package/src/CdcChart.tsx +5 -4
  15. package/src/CdcChartComponent.tsx +166 -62
  16. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  17. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  18. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  19. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  20. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  21. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  22. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  23. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  24. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  25. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  26. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  27. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  28. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  29. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  30. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  31. package/src/_stories/_mock/heatmap.json +156 -0
  32. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  33. package/src/_stories/_mock/sankey_mock.json +78 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  35. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  36. package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
  37. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  38. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  39. package/src/components/Axis/LeftAxis.tsx +17 -11
  40. package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
  41. package/src/components/Axis/RightAxis.tsx +3 -0
  42. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  43. package/src/components/Brush/BrushSelector.tsx +88 -187
  44. package/src/components/Brush/brushInteraction.ts +22 -0
  45. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  46. package/src/components/EditorPanel/EditorPanel.tsx +677 -185
  47. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
  48. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  49. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  51. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  52. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  53. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  54. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  55. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
  56. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  57. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  58. package/src/components/EditorPanel/components/panels.scss +10 -0
  59. package/src/components/EditorPanel/editor-panel.scss +47 -0
  60. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
  61. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
  62. package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
  63. package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
  64. package/src/components/HeatMap/components/HeatMap.tsx +767 -0
  65. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
  66. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  67. package/src/components/HeatMap/heatmap.css +85 -0
  68. package/src/components/HeatMap/helpers/index.ts +261 -0
  69. package/src/components/HeatMap/index.tsx +5 -0
  70. package/src/components/HeatMap/tests/HeatMap.test.tsx +1072 -0
  71. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  72. package/src/components/Legend/Legend.Component.tsx +2 -2
  73. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  74. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  75. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  76. package/src/components/LineChart/helpers.test.ts +574 -0
  77. package/src/components/LineChart/helpers.ts +39 -16
  78. package/src/components/LineChart/index.tsx +28 -5
  79. package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
  80. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  81. package/src/components/LinearChart.tsx +103 -47
  82. package/src/components/PieChart/PieChart.tsx +4 -1
  83. package/src/components/RadarChart/RadarChart.tsx +3 -1
  84. package/src/components/Regions/components/Regions.tsx +195 -41
  85. package/src/components/Sankey/components/Sankey.tsx +7 -3
  86. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  87. package/src/components/ScatterPlot/ScatterPlot.jsx +3 -3
  88. package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
  89. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  90. package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
  91. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  92. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  93. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  94. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  95. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  96. package/src/data/initial-state.js +12 -0
  97. package/src/helpers/axisLabelFontSize.ts +8 -0
  98. package/src/helpers/bumpChartTooltip.ts +54 -0
  99. package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
  100. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  101. package/src/helpers/dataHelpers.ts +11 -1
  102. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  103. package/src/helpers/getAxisMaxOverride.ts +16 -0
  104. package/src/helpers/getCleanTopTickMax.ts +64 -0
  105. package/src/helpers/getColorScale.ts +18 -9
  106. package/src/helpers/getHasBoundarySuppression.ts +26 -0
  107. package/src/helpers/getMinMax.ts +26 -25
  108. package/src/helpers/getYAxisDomainData.ts +38 -0
  109. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  110. package/src/helpers/handleChartAriaLabels.ts +23 -10
  111. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  112. package/src/helpers/seriesColumnSettings.ts +5 -4
  113. package/src/helpers/seriesHighlight.ts +26 -0
  114. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  115. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  116. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  117. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  118. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  119. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  120. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  121. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  122. package/src/helpers/tests/getColorScale.test.ts +197 -0
  123. package/src/helpers/tests/getMinMax.test.ts +211 -0
  124. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  125. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  126. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  127. package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
  128. package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
  129. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  130. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  131. package/src/hooks/tests/useRightAxis.test.tsx +127 -0
  132. package/src/hooks/tests/useScales.test.tsx +383 -0
  133. package/src/hooks/useRightAxis.ts +30 -13
  134. package/src/hooks/useScales.ts +48 -21
  135. package/src/hooks/useTooltip.tsx +3 -1
  136. package/src/index.jsx +5 -2
  137. package/src/scss/main.scss +32 -49
  138. package/src/test/CdcChart.dataTable.test.tsx +122 -0
  139. package/src/types/ChartConfig.ts +24 -1
  140. package/src/types/ChartContext.ts +5 -0
  141. package/src/helpers/getYAxisAutoPadding.ts +0 -53
@@ -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,
@@ -239,6 +246,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
239
246
  const processedIntroText = processedTextFields.introText
240
247
  const processedLegacyFootnotes = processedTextFields.legacyFootnotes
241
248
  const processedDescription = processedTextFields.description
249
+ const chartDataTableIsRendered =
250
+ Boolean(config.table?.show) &&
251
+ config.visualizationType !== 'Spark Line' &&
252
+ ((Boolean(config.xAxis?.dataKey) && config.visualizationType !== 'Sankey') || config.visualizationType === 'Sankey')
242
253
  // Note: Axis labels are processed within updateConfig to ensure they use the correct data
243
254
 
244
255
  // set defaults on titles if blank AND only in editor
@@ -300,6 +311,17 @@ const CdcChart: React.FC<CdcChartProps> = ({
300
311
  }
301
312
  }
302
313
 
314
+ if (loadedConfig?.visualizationType === 'HeatMap' && !loadedConfig?.general?.palette) {
315
+ if (!defaultsWithoutPalette.general) {
316
+ defaultsWithoutPalette.general = {}
317
+ }
318
+ defaultsWithoutPalette.general.palette = {
319
+ isReversed: false,
320
+ version: '2.0',
321
+ name: 'sequential_blue'
322
+ }
323
+ }
324
+
303
325
  let newConfig = { ...defaultsWithoutPalette, ...loadedConfig }
304
326
 
305
327
  // Ensure Horizon Chart has enough palette colors for all layers
@@ -322,9 +344,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
322
344
  })
323
345
  })
324
346
 
325
- if (newConfig.visualizationType === 'Bump Chart') {
326
- newConfig.xAxis.type === 'date-time'
327
- }
347
+ ensureSpecialChartAxisTypes(newConfig)
328
348
  if (!isDashboard) return coveUpdateWorker(newConfig)
329
349
  return newConfig
330
350
  }
@@ -380,6 +400,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
380
400
  const updateConfig = (_config: AllChartsConfig, dataOverride?: any[]) => {
381
401
  const newConfig = cloneConfig(_config)
382
402
  let data = dataOverride || stateData
403
+ const shouldUseHeatMapSideTitlePlacement =
404
+ newConfig.visualizationType === 'HeatMap' && !newConfig.yAxis?.titlePlacement
405
+
406
+ ensureSpecialChartAxisTypes(newConfig)
383
407
 
384
408
  data = handleRankByValue(data, newConfig)
385
409
 
@@ -388,6 +412,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
388
412
 
389
413
  // Backfill missing properties from defaults, respecting legacy values
390
414
  backfillDefaults(newConfig, defaults, LEGACY_CHART_DEFAULTS)
415
+ if (shouldUseHeatMapSideTitlePlacement) {
416
+ newConfig.yAxis.titlePlacement = 'side'
417
+ }
391
418
 
392
419
  // Auto-populate table.defaultSort for date-axis charts if not already set by user
393
420
  const hasDateAxisType = ['date-time', 'date'].includes(newConfig.xAxis?.type)
@@ -457,7 +484,9 @@ const CdcChart: React.FC<CdcChartProps> = ({
457
484
  lineType: series.lineType,
458
485
  originalDataKey: series.dataKey,
459
486
  dynamicCategory: series.dynamicCategory,
460
- tooltip: true
487
+ tooltip: true,
488
+ weight: series.weight,
489
+ axis: series.axis
461
490
  })
462
491
  })
463
492
  // return the series keys
@@ -528,6 +557,32 @@ const CdcChart: React.FC<CdcChartProps> = ({
528
557
  }
529
558
  }
530
559
 
560
+ if (newConfig.visualizationType === 'HeatMap') {
561
+ const heatMapSeriesKeys = newConfig.series.map(series => series.dataKey)
562
+ const heatMapSeriesLabels = newConfig.series.reduce<Record<string, string>>((acc, series) => {
563
+ const heatMapColumnConfig = findColumnConfigByName(newConfig.columns || {}, series.dataKey)?.columnConfig
564
+ const configuredColumnLabel = heatMapColumnConfig?.label
565
+ const hasCustomColumnLabel = configuredColumnLabel && configuredColumnLabel !== series.dataKey
566
+ acc[series.dataKey] = hasCustomColumnLabel ? configuredColumnLabel : series.name || series.dataKey
567
+ return acc
568
+ }, {})
569
+
570
+ newConfig.legend = {
571
+ ...newConfig.legend,
572
+ position: newConfig.legend?.position || 'top',
573
+ style: newConfig.legend?.style || 'gradient',
574
+ subStyle:
575
+ newConfig.legend?.subStyle === 'smooth' ? 'linear blocks' : newConfig.legend?.subStyle || 'linear blocks'
576
+ }
577
+ newConfig.yAxis = {
578
+ ...newConfig.yAxis,
579
+ type: 'categorical'
580
+ }
581
+ newConfig.runtime.seriesKeys = heatMapSeriesKeys
582
+ newConfig.runtime.seriesLabelsAll = heatMapSeriesKeys
583
+ newConfig.runtime.seriesLabels = heatMapSeriesLabels
584
+ }
585
+
531
586
  if (isHorizontalVariant) {
532
587
  // For horizontal charts, axes are swapped, so processedYAxis goes to runtime.xAxis and vice versa
533
588
  const horizontalXAxisSource = cloneDeep((newConfig.yAxis as any)?.yAxis || newConfig.yAxis)
@@ -579,6 +634,19 @@ const CdcChart: React.FC<CdcChartProps> = ({
579
634
  } else {
580
635
  newConfig.runtime.editorErrorMessage = ''
581
636
  }
637
+ } else if (newConfig.visualizationType === 'HeatMap') {
638
+ const missingXColumn = !newConfig.xAxis.dataKey || newConfig.xAxis.dataKey === ''
639
+ const missingSeries = !Array.isArray(newConfig.series) || newConfig.series.length === 0
640
+
641
+ if (missingXColumn && missingSeries) {
642
+ newConfig.runtime.editorErrorMessage = 'Date/Category Axis and Data Series must be set for heatmaps.'
643
+ } else if (missingXColumn) {
644
+ newConfig.runtime.editorErrorMessage = 'Date/Category Axis must be set for heatmaps.'
645
+ } else if (missingSeries) {
646
+ newConfig.runtime.editorErrorMessage = 'Data Series must be set for heatmaps.'
647
+ } else {
648
+ newConfig.runtime.editorErrorMessage = ''
649
+ }
582
650
  } else {
583
651
  // No errors
584
652
  newConfig.runtime.editorErrorMessage = ''
@@ -708,6 +776,16 @@ const CdcChart: React.FC<CdcChartProps> = ({
708
776
  return newConfig
709
777
  }
710
778
 
779
+ const configDataDependency = configObj?.data?.length ? configObj.data : null
780
+ const configExclusionsDependency = [
781
+ configObj?.xAxis?.type,
782
+ configObj?.xAxis?.dataKey,
783
+ configObj?.exclusions?.active,
784
+ configObj?.exclusions?.dateStart,
785
+ configObj?.exclusions?.dateEnd,
786
+ ...(configObj?.exclusions?.keys || [])
787
+ ].join('|')
788
+
711
789
  useEffect(() => {
712
790
  const load = async () => {
713
791
  try {
@@ -729,7 +807,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
729
807
  }
730
808
 
731
809
  load()
732
- }, [configObj?.data?.length ? configObj.data : null])
810
+ }, [configDataDependency, configExclusionsDependency])
733
811
 
734
812
  /**
735
813
  * When cove has a config and container ref publish the cove_loaded event.
@@ -870,17 +948,15 @@ const CdcChart: React.FC<CdcChartProps> = ({
870
948
  }, [config, stateData, getProcessedAxisLabels, dispatch, editorContext, isEditor, isDashboard])
871
949
 
872
950
  // Called on legend click, highlights/unhighlights the data series with the given label
873
- const highlight = (label: Label): void => {
951
+ const highlight = (label: Label | string): void => {
952
+ const newHighlight = getLegendHighlightKey(config.runtime.seriesLabels, label)
953
+
874
954
  if (
875
- seriesHighlight.length + 1 === config.runtime.seriesKeys.length &&
876
- config.visualizationType !== 'Forecasting' &&
877
- !seriesHighlight.includes(label.datum)
955
+ shouldResetSeriesHighlight(seriesHighlight, config.runtime.seriesKeys, newHighlight, config.visualizationType)
878
956
  ) {
879
957
  return handleShowAll()
880
958
  }
881
959
 
882
- const newHighlight = findKey(config.runtime.seriesLabels, v => v === label.datum) || label.datum
883
-
884
960
  const newSeriesHighlight = xor(seriesHighlight, [newHighlight])
885
961
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newSeriesHighlight })
886
962
  }
@@ -1196,9 +1272,18 @@ const CdcChart: React.FC<CdcChartProps> = ({
1196
1272
 
1197
1273
  // Transform and clean data for chart rendering
1198
1274
  const transformedData = getTransformedData({ brushData: state.brushData, filteredData, excludedData, clean })
1275
+ const configYAxisDomainData = (config as ChartConfig).yAxisDomainData
1276
+ const yAxisDomainData = useMemo(() => {
1277
+ if (Array.isArray(configYAxisDomainData) && configYAxisDomainData.length > 0) {
1278
+ return clean(getExcludedData(config, configYAxisDomainData))
1279
+ }
1280
+
1281
+ return clean(excludedData)
1282
+ }, [config, configYAxisDomainData, excludedData])
1199
1283
 
1200
1284
  // Filter annotations to only those visible in current data view
1201
1285
  const visibleAnnotations = getVisibleAnnotations(config.annotations, transformedData, config.xAxis?.dataKey)
1286
+ const isHeatMap = config.visualizationType === 'HeatMap'
1202
1287
  const isTp5Treatment = ENABLE_CHART_MAP_TP5_TREATMENT && config.visual?.tp5Treatment
1203
1288
  const visualSettingClasses = new Set([
1204
1289
  'component--has-border-color-theme',
@@ -1219,6 +1304,12 @@ const CdcChart: React.FC<CdcChartProps> = ({
1219
1304
  bodyClasses.push('component--hide-background-color')
1220
1305
  }
1221
1306
  if (isTp5Treatment && !bodyClasses.includes('no-borders')) bodyClasses.push('no-borders')
1307
+ const bodyWrapClasses = [
1308
+ isTp5Treatment ? 'cdc-callout d-flex flex-column tp5-chart-callout' : '',
1309
+ isHeatMap ? 'cdc-heatmap__body-wrap' : ''
1310
+ ]
1311
+ .filter(Boolean)
1312
+ .join(' ')
1222
1313
  const chartTitle = (
1223
1314
  <Title
1224
1315
  showTitle={config.showTitle}
@@ -1241,7 +1332,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
1241
1332
  return kebabCase(string)
1242
1333
  }
1243
1334
  const getChartWrapperClasses = () => {
1244
- const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
1245
1335
  const classes = ['chart-container', 'visualization-container', 'p-relative']
1246
1336
  const visualSettingClasses = ['component--has-border-color-theme', 'component--has-accent']
1247
1337
  if (legend?.position) {
@@ -1252,11 +1342,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
1252
1342
  }
1253
1343
  }
1254
1344
  if (legend?.hide) classes.push('legend-hidden')
1345
+ if (isHeatMap) classes.push('cdc-heatmap__container')
1255
1346
  if (contentClasses.includes('sparkline')) classes.push('sparkline')
1256
1347
  if (lineDatapointClass) classes.push(lineDatapointClass)
1257
1348
  if (!config.barHasBorder) classes.push('chart-bar--no-border')
1258
- if (config.xAxis.brushActive && dashboardConfig?.type === 'dashboard' && (!isLegendOnBottom || legend.hide))
1259
- classes.push('dashboard-brush')
1260
1349
 
1261
1350
  if (!ENABLE_CHART_VISUAL_SETTINGS) {
1262
1351
  const filtered = classes.filter(className => !visualSettingClasses.includes(className))
@@ -1275,6 +1364,34 @@ const CdcChart: React.FC<CdcChartProps> = ({
1275
1364
  return classes
1276
1365
  }
1277
1366
 
1367
+ const showTopYAxisTitle =
1368
+ config.yAxis?.titlePlacement === 'top' &&
1369
+ !config.smallMultiples?.mode &&
1370
+ !config.hideYAxisLabel &&
1371
+ Boolean(config.runtime?.yAxis?.label)
1372
+ const topYAxisTitleFontSize = getAxisLabelFontSize(vizViewport)
1373
+
1374
+ const renderTopYAxisTitle = () =>
1375
+ showTopYAxisTitle ? (
1376
+ <div className='y-axis-top-title' style={{ fontSize: `${topYAxisTitleFontSize}px` }}>
1377
+ {config.runtime.yAxis.label}
1378
+ </div>
1379
+ ) : null
1380
+
1381
+ const renderLinearChartWithParentSize = (
1382
+ getParentWidth: (parent: { width: number; height: number }) => number = parent => parent.width,
1383
+ wrapperStyle: React.CSSProperties = { width: '100%' }
1384
+ ) => (
1385
+ <>
1386
+ {renderTopYAxisTitle()}
1387
+ <div ref={parentRef} style={wrapperStyle}>
1388
+ <ParentSize>
1389
+ {parent => <LinearChart ref={svgRef} parentWidth={getParentWidth(parent)} parentHeight={parent.height} />}
1390
+ </ParentSize>
1391
+ </div>
1392
+ </>
1393
+ )
1394
+
1278
1395
  if (!isLoading) {
1279
1396
  const tableLink = (
1280
1397
  <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
@@ -1287,11 +1404,11 @@ const CdcChart: React.FC<CdcChartProps> = ({
1287
1404
  {!missingRequiredSections(config) && !config.newViz && (
1288
1405
  <VisualizationContent
1289
1406
  innerClassName={`type-${makeClassName(config.visualizationType)}`}
1290
- innerProps={{ 'aria-label': handleChartAriaLabels(config), tabIndex: 0 }}
1407
+ innerProps={{ tabIndex: 0 }}
1291
1408
  bodyClassName={bodyClasses.join(' ')}
1292
- bodyWrapClassName={isTp5Treatment ? 'cdc-callout d-flex flex-column tp5-chart-callout' : ''}
1409
+ bodyWrapClassName={bodyWrapClasses}
1293
1410
  filters={
1294
- config.filters?.length > 0 && !externalFilters && config.visualizationType !== 'Spark Line' ? (
1411
+ hasVisibleVizFilters(config.filters) && !externalFilters && config.visualizationType !== 'Spark Line' ? (
1295
1412
  <Filters
1296
1413
  config={config}
1297
1414
  setFilters={setFilters}
@@ -1311,11 +1428,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1311
1428
  {isDashboard && config.table && config.table.show && config.table.showDataTableLink
1312
1429
  ? tableLink
1313
1430
  : link && link}
1314
- {(config.xAxis.dataKey &&
1315
- config.table.show &&
1316
- config.visualizationType !== 'Spark Line' &&
1317
- config.visualizationType !== 'Sankey') ||
1318
- (config.visualizationType === 'Sankey' && config.table.show)
1431
+ {chartDataTableIsRendered
1319
1432
  ? (() => {
1320
1433
  let dataTableConfig = pivotDynamicSeries(config)
1321
1434
  let dataTableColumns = config.columns
@@ -1341,6 +1454,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1341
1454
  <DataTable
1342
1455
  key={config.table?.defaultSort?.column || ''}
1343
1456
  config={dataTableConfig}
1457
+ dataConfig={config.dataKey ? datasets?.[config.dataKey] : undefined}
1344
1458
  rawData={dataTableRawData}
1345
1459
  runtimeData={dataTableRuntimeData}
1346
1460
  expandDataTable={config.table.expanded}
@@ -1403,12 +1517,13 @@ const CdcChart: React.FC<CdcChartProps> = ({
1403
1517
  }
1404
1518
  footer={
1405
1519
  <FootnotesStandAlone
1406
- config={configObj.footnotes}
1520
+ config={config.footnotes}
1407
1521
  filters={config.filters?.filter(f => f.filterFootnotes)}
1408
1522
  markupVariables={config.markupVariables}
1409
1523
  enableMarkupVariables={config.enableMarkupVariables}
1410
1524
  data={config.data}
1411
1525
  dataMetadata={config.dataMetadata}
1526
+ footerClassName='cove-visualization__footnotes'
1412
1527
  />
1413
1528
  }
1414
1529
  >
@@ -1446,15 +1561,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
1446
1561
  {/* All charts with LinearChart */}
1447
1562
  {filteredData &&
1448
1563
  filteredData.length > 0 &&
1449
- !['Spark Line', 'Line', 'Sankey', 'Pie', 'Radar'].includes(config.visualizationType) && (
1450
- <div ref={parentRef} style={{ width: `100%` }}>
1451
- <ParentSize>
1452
- {parent => (
1453
- <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1454
- )}
1455
- </ParentSize>
1456
- </div>
1457
- )}
1564
+ !['Spark Line', 'Line', 'Sankey', 'Pie', 'Radar', 'HeatMap'].includes(config.visualizationType) &&
1565
+ renderLinearChartWithParentSize()}
1458
1566
 
1459
1567
  {filteredData && filteredData.length > 0 && config.visualizationType === 'Pie' && (
1460
1568
  <ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
@@ -1481,38 +1589,31 @@ const CdcChart: React.FC<CdcChartProps> = ({
1481
1589
  )}
1482
1590
  </ParentSize>
1483
1591
  )}
1592
+ {filteredData && filteredData.length > 0 && config.visualizationType === 'HeatMap' && (
1593
+ <div
1594
+ ref={parentRef}
1595
+ style={{ width: '100%', height: `${calcInitialHeight(config, currentViewport) || 400}px` }}
1596
+ >
1597
+ <ParentSize>
1598
+ {parent => <HeatMap parentWidth={parent.width} parentHeight={parent.height} />}
1599
+ </ParentSize>
1600
+ </div>
1601
+ )}
1484
1602
  {/* Line Chart */}
1485
1603
  {filteredData &&
1486
1604
  filteredData.length > 0 &&
1487
1605
  config.visualizationType === 'Line' &&
1488
- (convertLineToBarGraph ? (
1489
- <div ref={parentRef} style={{ width: `100%` }}>
1490
- <ParentSize>
1491
- {parent => (
1492
- <LinearChart ref={svgRef} parentWidth={parent.width} parentHeight={parent.height} />
1493
- )}
1494
- </ParentSize>
1495
- </div>
1496
- ) : (
1497
- <div ref={parentRef} style={{ width: '100%' }}>
1498
- <ParentSize>
1499
- {parent => {
1500
- const labelMargin = 120
1501
- const widthReduction =
1502
- config.showLineSeriesLabels && (config.legend.position !== 'right' || config.legend.hide)
1503
- ? labelMargin
1504
- : 0
1505
- return (
1506
- <LinearChart
1507
- ref={svgRef}
1508
- parentWidth={parent.width - widthReduction}
1509
- parentHeight={parent.height}
1510
- />
1511
- )
1512
- }}
1513
- </ParentSize>
1514
- </div>
1515
- ))}
1606
+ (convertLineToBarGraph
1607
+ ? renderLinearChartWithParentSize()
1608
+ : renderLinearChartWithParentSize(parent => {
1609
+ const labelMargin = 120
1610
+ const widthReduction =
1611
+ config.showLineSeriesLabels && (config.legend.position !== 'right' || config.legend.hide)
1612
+ ? labelMargin
1613
+ : 0
1614
+
1615
+ return parent.width - widthReduction
1616
+ }))}
1516
1617
  {/* Sparkline */}
1517
1618
  {config.visualizationType === 'Spark Line' && (
1518
1619
  <>
@@ -1549,6 +1650,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1549
1650
  {!config.legend.hide &&
1550
1651
  config.visualizationType !== 'Spark Line' &&
1551
1652
  config.visualizationType !== 'Sankey' &&
1653
+ config.visualizationType !== 'HeatMap' &&
1552
1654
  !(config.visualizationType === 'Warming Stripes' && config.legend?.style === 'gradient') &&
1553
1655
  !(config.visualizationType === 'Warming Stripes' && config.smallMultiples?.mode) && (
1554
1656
  <Legend
@@ -1560,6 +1662,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1560
1662
  {config.visualizationType === 'Warming Stripes' &&
1561
1663
  config.legend?.style === 'gradient' &&
1562
1664
  !config.smallMultiples?.mode && <WarmingStripesGradientLegend />}
1665
+ {config.visualizationType === 'HeatMap' && <HeatMapGradientLegend />}
1563
1666
  </LegendWrapper>
1564
1667
  </div>
1565
1668
  </VisualizationContent>
@@ -1625,7 +1728,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
1625
1728
  twoColorPalette,
1626
1729
  unfilteredData: stateData,
1627
1730
  updateConfig,
1628
- visibleAnnotations
1731
+ visibleAnnotations,
1732
+ yAxisDomainData
1629
1733
  }
1630
1734
 
1631
1735
  return (
@@ -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
+ }