@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.
- package/CONFIG.md +349 -29
- package/LICENSE +201 -0
- package/dist/cdcchart.js +34247 -32805
- package/examples/feature/heatmap/average-age-categorical.json +313 -0
- package/examples/feature/heatmap/calendar-heatmap.json +134 -0
- package/examples/feature/heatmap/cell-values-bottom-axis.json +181 -0
- package/examples/feature/heatmap/sparse-aggregation.json +158 -0
- package/examples/private/bump-chart.json +1116 -0
- package/examples/private/test-123.json +624 -0
- package/package.json +4 -3
- package/src/CdcChartComponent.tsx +135 -54
- package/src/_stories/Chart.AltText.stories.tsx +136 -0
- package/src/_stories/Chart.HeatMap.stories.tsx +187 -0
- package/src/_stories/Chart.Sankey.smoke.stories.tsx +62 -0
- package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
- package/src/_stories/ChartAxisTitles.smoke.stories.tsx +13 -0
- package/src/_stories/ChartBar.Editor.stories.tsx +63 -0
- package/src/_stories/ChartBrush.Interaction.stories.tsx +112 -0
- package/src/_stories/ChartBrush.smoke.stories.tsx +13 -4
- package/src/_stories/_mock/alt_text_metadata.json +38 -0
- package/src/_stories/_mock/heatmap-average-age-categorical.json +315 -0
- package/src/_stories/_mock/heatmap.json +156 -0
- package/src/_stories/_mock/reported_pertussis_cases.json +1643 -0
- package/src/_stories/_mock/sankey_mock.json +78 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +1 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -1
- package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
- package/src/components/Axis/LeftAxis.tsx +14 -11
- package/src/components/Axis/LeftAxisGridlines.tsx +14 -11
- package/src/components/Brush/BrushSelector.tsx +88 -187
- package/src/components/Brush/brushInteraction.ts +22 -0
- package/src/components/Brush/tests/brushInteraction.test.ts +38 -0
- package/src/components/EditorPanel/EditorPanel.tsx +310 -49
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +13 -0
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +105 -13
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +1 -0
- package/src/components/EditorPanel/components/Panels/Panel.Regions.tsx +5 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +1 -1
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +57 -0
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +56 -0
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +70 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +42 -22
- package/src/components/HeatMap/components/HeatMap.tsx +762 -0
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +114 -0
- package/src/components/HeatMap/heatmap.constants.ts +13 -0
- package/src/components/HeatMap/heatmap.css +71 -0
- package/src/components/HeatMap/helpers/index.ts +261 -0
- package/src/components/HeatMap/index.tsx +5 -0
- package/src/components/HeatMap/tests/HeatMap.test.tsx +1040 -0
- package/src/components/HeatMap/tests/helpers.test.ts +167 -0
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
- package/src/components/LineChart/helpers.test.ts +345 -0
- package/src/components/LineChart/helpers.ts +15 -5
- package/src/components/LineChart/index.tsx +7 -1
- package/src/components/LinearChart/tests/LinearChart.test.tsx +107 -33
- package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
- package/src/components/LinearChart.tsx +58 -37
- package/src/components/PieChart/PieChart.tsx +4 -1
- package/src/components/RadarChart/RadarChart.tsx +3 -1
- package/src/components/Regions/components/Regions.tsx +195 -41
- package/src/components/Sankey/components/Sankey.tsx +7 -3
- package/src/components/Sankey/helpers/getSankeyTooltip.tsx +7 -6
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +30 -3
- package/src/components/SmallMultiples/SmallMultiples.css +12 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +15 -9
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
- package/src/components/Sparkline/components/SparkLine.tsx +4 -9
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +1 -1
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +2 -2
- package/src/data/initial-state.js +11 -0
- package/src/helpers/axisLabelFontSize.ts +8 -0
- package/src/helpers/bumpChartTooltip.ts +54 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +4 -0
- package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
- package/src/helpers/getMinMax.ts +2 -3
- package/src/helpers/getYAxisAutoPadding.ts +8 -6
- package/src/helpers/getYAxisAutoPaddingMode.ts +17 -0
- package/src/helpers/handleChartAriaLabels.ts +23 -10
- package/src/helpers/seriesHighlight.ts +26 -0
- package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
- package/src/helpers/tests/getYAxisAutoPadding.test.ts +37 -0
- package/src/helpers/tests/getYAxisAutoPaddingMode.test.ts +90 -0
- package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
- package/src/helpers/tests/seriesHighlight.test.ts +61 -0
- package/src/hooks/tests/useScales.test.tsx +105 -0
- package/src/hooks/useScales.ts +19 -9
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +32 -49
- package/src/types/ChartConfig.ts +18 -0
- 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
|
-
|
|
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
|
|
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={{
|
|
1382
|
+
innerProps={{ tabIndex: 0 }}
|
|
1291
1383
|
bodyClassName={bodyClasses.join(' ')}
|
|
1292
|
-
bodyWrapClassName={
|
|
1384
|
+
bodyWrapClassName={bodyWrapClasses}
|
|
1293
1385
|
filters={
|
|
1294
|
-
config.filters
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
+
}
|