@cdc/chart 4.26.4 → 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 (93) hide show
  1. package/CONFIG.md +349 -29
  2. package/LICENSE +201 -0
  3. package/dist/cdcchart.js +34247 -32805
  4. package/examples/feature/heatmap/average-age-categorical.json +313 -0
  5. package/examples/feature/heatmap/calendar-heatmap.json +134 -0
  6. package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
  7. package/examples/feature/heatmap/sparse-aggregation.json +158 -0
  8. package/examples/private/bump-chart.json +1116 -0
  9. package/examples/private/test-123.json +624 -0
  10. package/package.json +4 -3
  11. package/src/CdcChartComponent.tsx +135 -54
  12. package/src/_stories/Chart.AltText.stories.tsx +136 -0
  13. package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
  14. package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
  15. package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
  16. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
  17. package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
  18. package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
  19. package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
  20. package/src/_stories/_mock/alt_text_metadata.json +38 -0
  21. package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
  22. package/src/_stories/_mock/heatmap.json +156 -0
  23. package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
  24. package/src/_stories/_mock/sankey_mock.json +78 -0
  25. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
  26. package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
  27. package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
  28. package/src/components/Axis/LeftAxis.tsx +14 -11
  29. package/src/components/Axis/LeftAxisGridlines.tsx +14 -11
  30. package/src/components/Brush/BrushSelector.tsx +88 -187
  31. package/src/components/Brush/brushInteraction.ts +22 -0
  32. package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
  33. package/src/components/EditorPanel/EditorPanel.tsx +310 -49
  34. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +13 -0
  35. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
  37. package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
  38. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  39. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
  40. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
  41. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
  42. package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
  43. package/src/components/EditorPanel/useEditorPermissions.ts +42 -22
  44. package/src/components/HeatMap/components/HeatMap.tsx +762 -0
  45. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
  46. package/src/components/HeatMap/heatmap.constants.ts +13 -0
  47. package/src/components/HeatMap/heatmap.css +71 -0
  48. package/src/components/HeatMap/helpers/index.ts +261 -0
  49. package/src/components/HeatMap/index.tsx +5 -0
  50. package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
  51. package/src/components/HeatMap/tests/helpers.test.ts +167 -0
  52. package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
  53. package/src/components/LineChart/helpers.test.ts +345 -0
  54. package/src/components/LineChart/helpers.ts +15 -5
  55. package/src/components/LineChart/index.tsx +7 -1
  56. package/src/components/LinearChart/tests/LinearChart.test.tsx +107 -33
  57. package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
  58. package/src/components/LinearChart.tsx +58 -37
  59. package/src/components/PieChart/PieChart.tsx +4 -1
  60. package/src/components/RadarChart/RadarChart.tsx +3 -1
  61. package/src/components/Regions/components/Regions.tsx +195 -41
  62. package/src/components/Sankey/components/Sankey.tsx +7 -3
  63. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
  64. package/src/components/SmallMultiples/SmallMultipleTile.tsx +30 -3
  65. package/src/components/SmallMultiples/SmallMultiples.css +12 -0
  66. package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
  67. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
  68. package/src/components/Sparkline/components/SparkLine.tsx +4 -9
  69. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
  70. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
  71. package/src/data/initial-state.js +11 -0
  72. package/src/helpers/axisLabelFontSize.ts +8 -0
  73. package/src/helpers/bumpChartTooltip.ts +54 -0
  74. package/src/helpers/calculateLeftYAxisWidth.ts +4 -0
  75. package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
  76. package/src/helpers/getMinMax.ts +2 -3
  77. package/src/helpers/getYAxisAutoPadding.ts +8 -6
  78. package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
  79. package/src/helpers/handleChartAriaLabels.ts +23 -10
  80. package/src/helpers/seriesHighlight.ts +26 -0
  81. package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
  82. package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
  83. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
  84. package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
  85. package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
  86. package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
  87. package/src/helpers/tests/seriesHighlight.test.ts +61 -0
  88. package/src/hooks/tests/useScales.test.tsx +105 -0
  89. package/src/hooks/useScales.ts +19 -9
  90. package/src/index.jsx +5 -2
  91. package/src/scss/main.scss +32 -49
  92. package/src/types/ChartConfig.ts +18 -0
  93. package/src/types/ChartContext.ts +2 -0
@@ -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)
@@ -579,6 +627,19 @@ const CdcChart: React.FC<CdcChartProps> = ({
579
627
  } else {
580
628
  newConfig.runtime.editorErrorMessage = ''
581
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
+ }
582
643
  } else {
583
644
  // No errors
584
645
  newConfig.runtime.editorErrorMessage = ''
@@ -870,17 +931,15 @@ const CdcChart: React.FC<CdcChartProps> = ({
870
931
  }, [config, stateData, getProcessedAxisLabels, dispatch, editorContext, isEditor, isDashboard])
871
932
 
872
933
  // Called on legend click, highlights/unhighlights the data series with the given label
873
- const highlight = (label: Label): void => {
934
+ const highlight = (label: Label | string): void => {
935
+ const newHighlight = getLegendHighlightKey(config.runtime.seriesLabels, label)
936
+
874
937
  if (
875
- seriesHighlight.length + 1 === config.runtime.seriesKeys.length &&
876
- config.visualizationType !== 'Forecasting' &&
877
- !seriesHighlight.includes(label.datum)
938
+ shouldResetSeriesHighlight(seriesHighlight, config.runtime.seriesKeys, newHighlight, config.visualizationType)
878
939
  ) {
879
940
  return handleShowAll()
880
941
  }
881
942
 
882
- const newHighlight = findKey(config.runtime.seriesLabels, v => v === label.datum) || label.datum
883
-
884
943
  const newSeriesHighlight = xor(seriesHighlight, [newHighlight])
885
944
  dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: newSeriesHighlight })
886
945
  }
@@ -1199,6 +1258,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1199
1258
 
1200
1259
  // Filter annotations to only those visible in current data view
1201
1260
  const visibleAnnotations = getVisibleAnnotations(config.annotations, transformedData, config.xAxis?.dataKey)
1261
+ const isHeatMap = config.visualizationType === 'HeatMap'
1202
1262
  const isTp5Treatment = ENABLE_CHART_MAP_TP5_TREATMENT && config.visual?.tp5Treatment
1203
1263
  const visualSettingClasses = new Set([
1204
1264
  'component--has-border-color-theme',
@@ -1219,6 +1279,12 @@ const CdcChart: React.FC<CdcChartProps> = ({
1219
1279
  bodyClasses.push('component--hide-background-color')
1220
1280
  }
1221
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(' ')
1222
1288
  const chartTitle = (
1223
1289
  <Title
1224
1290
  showTitle={config.showTitle}
@@ -1241,7 +1307,6 @@ const CdcChart: React.FC<CdcChartProps> = ({
1241
1307
  return kebabCase(string)
1242
1308
  }
1243
1309
  const getChartWrapperClasses = () => {
1244
- const isLegendOnBottom = legend?.position === 'bottom' || isLegendWrapViewport(currentViewport)
1245
1310
  const classes = ['chart-container', 'visualization-container', 'p-relative']
1246
1311
  const visualSettingClasses = ['component--has-border-color-theme', 'component--has-accent']
1247
1312
  if (legend?.position) {
@@ -1252,11 +1317,10 @@ const CdcChart: React.FC<CdcChartProps> = ({
1252
1317
  }
1253
1318
  }
1254
1319
  if (legend?.hide) classes.push('legend-hidden')
1320
+ if (isHeatMap) classes.push('cdc-heatmap__container')
1255
1321
  if (contentClasses.includes('sparkline')) classes.push('sparkline')
1256
1322
  if (lineDatapointClass) classes.push(lineDatapointClass)
1257
1323
  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
1324
 
1261
1325
  if (!ENABLE_CHART_VISUAL_SETTINGS) {
1262
1326
  const filtered = classes.filter(className => !visualSettingClasses.includes(className))
@@ -1275,6 +1339,34 @@ const CdcChart: React.FC<CdcChartProps> = ({
1275
1339
  return classes
1276
1340
  }
1277
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
+
1278
1370
  if (!isLoading) {
1279
1371
  const tableLink = (
1280
1372
  <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
@@ -1287,11 +1379,11 @@ const CdcChart: React.FC<CdcChartProps> = ({
1287
1379
  {!missingRequiredSections(config) && !config.newViz && (
1288
1380
  <VisualizationContent
1289
1381
  innerClassName={`type-${makeClassName(config.visualizationType)}`}
1290
- innerProps={{ 'aria-label': handleChartAriaLabels(config), tabIndex: 0 }}
1382
+ innerProps={{ tabIndex: 0 }}
1291
1383
  bodyClassName={bodyClasses.join(' ')}
1292
- bodyWrapClassName={isTp5Treatment ? 'cdc-callout d-flex flex-column tp5-chart-callout' : ''}
1384
+ bodyWrapClassName={bodyWrapClasses}
1293
1385
  filters={
1294
- config.filters?.length > 0 && !externalFilters && config.visualizationType !== 'Spark Line' ? (
1386
+ hasVisibleVizFilters(config.filters) && !externalFilters && config.visualizationType !== 'Spark Line' ? (
1295
1387
  <Filters
1296
1388
  config={config}
1297
1389
  setFilters={setFilters}
@@ -1403,12 +1495,13 @@ const CdcChart: React.FC<CdcChartProps> = ({
1403
1495
  }
1404
1496
  footer={
1405
1497
  <FootnotesStandAlone
1406
- config={configObj.footnotes}
1498
+ config={config.footnotes}
1407
1499
  filters={config.filters?.filter(f => f.filterFootnotes)}
1408
1500
  markupVariables={config.markupVariables}
1409
1501
  enableMarkupVariables={config.enableMarkupVariables}
1410
1502
  data={config.data}
1411
1503
  dataMetadata={config.dataMetadata}
1504
+ footerClassName='cove-visualization__footnotes'
1412
1505
  />
1413
1506
  }
1414
1507
  >
@@ -1446,15 +1539,8 @@ const CdcChart: React.FC<CdcChartProps> = ({
1446
1539
  {/* All charts with LinearChart */}
1447
1540
  {filteredData &&
1448
1541
  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
- )}
1542
+ !['Spark Line', 'Line', 'Sankey', 'Pie', 'Radar', 'HeatMap'].includes(config.visualizationType) &&
1543
+ renderLinearChartWithParentSize()}
1458
1544
 
1459
1545
  {filteredData && filteredData.length > 0 && config.visualizationType === 'Pie' && (
1460
1546
  <ParentSize className='justify-content-center d-flex' style={{ width: `100%` }}>
@@ -1481,38 +1567,31 @@ const CdcChart: React.FC<CdcChartProps> = ({
1481
1567
  )}
1482
1568
  </ParentSize>
1483
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
+ )}
1484
1580
  {/* Line Chart */}
1485
1581
  {filteredData &&
1486
1582
  filteredData.length > 0 &&
1487
1583
  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
- ))}
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
+ }))}
1516
1595
  {/* Sparkline */}
1517
1596
  {config.visualizationType === 'Spark Line' && (
1518
1597
  <>
@@ -1549,6 +1628,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1549
1628
  {!config.legend.hide &&
1550
1629
  config.visualizationType !== 'Spark Line' &&
1551
1630
  config.visualizationType !== 'Sankey' &&
1631
+ config.visualizationType !== 'HeatMap' &&
1552
1632
  !(config.visualizationType === 'Warming Stripes' && config.legend?.style === 'gradient') &&
1553
1633
  !(config.visualizationType === 'Warming Stripes' && config.smallMultiples?.mode) && (
1554
1634
  <Legend
@@ -1560,6 +1640,7 @@ const CdcChart: React.FC<CdcChartProps> = ({
1560
1640
  {config.visualizationType === 'Warming Stripes' &&
1561
1641
  config.legend?.style === 'gradient' &&
1562
1642
  !config.smallMultiples?.mode && <WarmingStripesGradientLegend />}
1643
+ {config.visualizationType === 'HeatMap' && <HeatMapGradientLegend />}
1563
1644
  </LegendWrapper>
1564
1645
  </div>
1565
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
+ }