@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.
- package/CONFIG.md +353 -29
- package/LICENSE +201 -0
- package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
- package/dist/cdcchart.js +37885 -35117
- 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/bad-y-axis.json +543 -0
- package/examples/private/bar-chart-negative-value.json +455 -0
- package/examples/private/bump-chart.json +1116 -0
- package/examples/private/test-123.json +624 -0
- package/package.json +4 -3
- package/src/CdcChart.tsx +5 -4
- package/src/CdcChartComponent.tsx +166 -62
- 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.SeriesColorAssignments.stories.tsx +183 -0
- package/src/_stories/Chart.SmallMultiples.smoke.stories.tsx +29 -0
- package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
- package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
- package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -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/annotation_event-line_mock.json +1064 -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.styles.css +22 -1
- package/src/components/Annotations/components/AnnotationDraggable.tsx +220 -120
- package/src/components/Annotations/components/AnnotationDropdown.tsx +5 -2
- package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
- package/src/components/Axis/LeftAxis.tsx +17 -11
- package/src/components/Axis/LeftAxisGridlines.tsx +22 -12
- package/src/components/Axis/RightAxis.tsx +3 -0
- package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
- 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 +677 -185
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +114 -52
- 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.SmallMultiples.test.tsx +98 -0
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +4 -1
- package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
- package/src/components/EditorPanel/components/panels.scss +10 -0
- package/src/components/EditorPanel/editor-panel.scss +47 -0
- package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +61 -0
- package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +74 -0
- package/src/components/EditorPanel/useEditorPermissions.test.tsx +315 -0
- package/src/components/EditorPanel/useEditorPermissions.ts +77 -21
- package/src/components/HeatMap/components/HeatMap.tsx +767 -0
- package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +128 -0
- package/src/components/HeatMap/heatmap.constants.ts +13 -0
- package/src/components/HeatMap/heatmap.css +85 -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 +1072 -0
- package/src/components/HeatMap/tests/helpers.test.ts +167 -0
- package/src/components/Legend/Legend.Component.tsx +2 -2
- package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +19 -22
- package/src/components/LineChart/helpers.test.ts +574 -0
- package/src/components/LineChart/helpers.ts +39 -16
- package/src/components/LineChart/index.tsx +28 -5
- package/src/components/LinearChart/tests/LinearChart.test.tsx +137 -33
- package/src/components/LinearChart/tests/mockConfigContext.ts +1 -0
- package/src/components/LinearChart.tsx +103 -47
- 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/ScatterPlot/ScatterPlot.jsx +3 -3
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +33 -3
- package/src/components/SmallMultiples/SmallMultiples.css +12 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +50 -15
- package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +152 -0
- package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -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 +12 -0
- package/src/helpers/axisLabelFontSize.ts +8 -0
- package/src/helpers/bumpChartTooltip.ts +54 -0
- package/src/helpers/calculateLeftYAxisWidth.ts +13 -3
- package/src/helpers/colorAssignmentHelpers.ts +102 -0
- package/src/helpers/dataHelpers.ts +11 -1
- package/src/helpers/ensureSpecialChartAxisTypes.ts +11 -0
- package/src/helpers/getAxisMaxOverride.ts +16 -0
- package/src/helpers/getCleanTopTickMax.ts +64 -0
- package/src/helpers/getColorScale.ts +18 -9
- package/src/helpers/getHasBoundarySuppression.ts +26 -0
- package/src/helpers/getMinMax.ts +26 -25
- package/src/helpers/getYAxisDomainData.ts +38 -0
- package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
- package/src/helpers/handleChartAriaLabels.ts +23 -10
- package/src/helpers/hasSpacedInlineLabel.ts +10 -0
- package/src/helpers/seriesColumnSettings.ts +5 -4
- package/src/helpers/seriesHighlight.ts +26 -0
- package/src/helpers/smallMultiplesHelpers.ts +5 -0
- package/src/helpers/tests/axisLabelFontSize.test.ts +15 -0
- package/src/helpers/tests/bumpChartTooltip.test.ts +75 -0
- package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
- package/src/helpers/tests/dataHelpers.test.ts +135 -0
- package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +30 -0
- package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
- package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
- package/src/helpers/tests/getColorScale.test.ts +197 -0
- package/src/helpers/tests/getMinMax.test.ts +211 -0
- package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
- package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
- package/src/helpers/tests/handleChartAriaLabels.test.ts +71 -0
- package/src/helpers/tests/hasSpacedInlineLabel.test.ts +75 -0
- package/src/helpers/tests/seriesColumnSettings.test.ts +16 -2
- package/src/helpers/tests/seriesHighlight.test.ts +61 -0
- package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
- package/src/hooks/tests/useRightAxis.test.tsx +127 -0
- package/src/hooks/tests/useScales.test.tsx +383 -0
- package/src/hooks/useRightAxis.ts +30 -13
- package/src/hooks/useScales.ts +48 -21
- package/src/hooks/useTooltip.tsx +3 -1
- package/src/index.jsx +5 -2
- package/src/scss/main.scss +32 -49
- package/src/test/CdcChart.dataTable.test.tsx +122 -0
- package/src/types/ChartConfig.ts +24 -1
- package/src/types/ChartContext.ts +5 -0
- 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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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={{
|
|
1407
|
+
innerProps={{ tabIndex: 0 }}
|
|
1291
1408
|
bodyClassName={bodyClasses.join(' ')}
|
|
1292
|
-
bodyWrapClassName={
|
|
1409
|
+
bodyWrapClassName={bodyWrapClasses}
|
|
1293
1410
|
filters={
|
|
1294
|
-
config.filters
|
|
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
|
-
{
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
))}
|
|
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
|
+
}
|