@cdc/chart 4.25.10 → 4.25.11
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/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +36258 -34658
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/na.json +913 -0
- package/examples/private/test-data.csv +28 -0
- package/index.html +2 -121
- package/package.json +4 -4
- package/src/CdcChart.tsx +8 -11
- package/src/CdcChartComponent.tsx +256 -87
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Patterns.stories.tsx +2 -1
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/ChartAnnotation.stories.tsx +6 -3
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +1 -2
- package/src/_stories/_mock/combo.json +451 -0
- package/src/_stories/_mock/editor-test-configs.json +376 -0
- package/src/_stories/_mock/editor-test-datasets.json +477 -0
- package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
- package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
- package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
- package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
- package/src/_stories/_mock/pie_config.json +257 -62
- package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
- package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
- package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
- package/src/components/Annotations/components/findNearestDatum.ts +6 -41
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +199 -190
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +139 -21
- package/src/components/Legend/Legend.Component.tsx +16 -9
- package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +36 -13
- package/src/components/LinearChart.tsx +75 -80
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
- package/src/components/SmallMultiples/index.ts +2 -0
- package/src/data/initial-state.js +13 -1
- package/src/helpers/buildForecastPaletteOptions.ts +0 -38
- package/src/helpers/getColorScale.ts +10 -0
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +60 -15
- package/src/scss/main.scss +1 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +4 -0
- package/src/types/ChartConfig.ts +24 -6
- package/src/types/ChartContext.ts +3 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
|
@@ -20,6 +20,7 @@ import InputToggle from '@cdc/core/components/inputs/InputToggle'
|
|
|
20
20
|
import { useColorPalette } from '@cdc/core/hooks/useColorPalette'
|
|
21
21
|
import { getCurrentPaletteName } from '@cdc/core/helpers/palettes/utils'
|
|
22
22
|
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
23
|
+
import { isCoveDeveloperMode } from '@cdc/core/helpers/queryStringUtils'
|
|
23
24
|
import { ChartContext } from './../../../../types/ChartContext.js'
|
|
24
25
|
|
|
25
26
|
import { useEditorPermissions } from '../../useEditorPermissions.js'
|
|
@@ -28,6 +29,10 @@ import ConfigContext from '../../../../ConfigContext.js'
|
|
|
28
29
|
import { PanelProps } from '../PanelProps'
|
|
29
30
|
import { LineChartConfig } from '../../../../types/ChartConfig'
|
|
30
31
|
import { PaletteSelector, DeveloperPaletteRollback } from '@cdc/core/components/PaletteSelector'
|
|
32
|
+
import { HeaderThemeSelector } from '@cdc/core/components/HeaderThemeSelector'
|
|
33
|
+
import { CustomColorsEditor } from '@cdc/core/components/CustomColorsEditor'
|
|
34
|
+
import { getColorScale } from '../../../../helpers/getColorScale'
|
|
35
|
+
import '@cdc/core/styles/v2/components/editor.scss'
|
|
31
36
|
import './panelVisual.styles.css'
|
|
32
37
|
|
|
33
38
|
const PanelVisual: FC<PanelProps> = props => {
|
|
@@ -40,7 +45,6 @@ const PanelVisual: FC<PanelProps> = props => {
|
|
|
40
45
|
visHasBarBorders,
|
|
41
46
|
visCanAnimate,
|
|
42
47
|
visSupportsNonSequentialPallete,
|
|
43
|
-
headerColors,
|
|
44
48
|
visSupportsTooltipOpacity,
|
|
45
49
|
visSupportsTooltipLines,
|
|
46
50
|
visSupportsBarSpace,
|
|
@@ -86,7 +90,7 @@ const PanelVisual: FC<PanelProps> = props => {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
return (
|
|
89
|
-
<AccordionItem>
|
|
93
|
+
<AccordionItem className='panel-visual'>
|
|
90
94
|
<AccordionItemHeading>
|
|
91
95
|
<AccordionItemButton>Visual</AccordionItemButton>
|
|
92
96
|
</AccordionItemHeading>
|
|
@@ -247,24 +251,7 @@ const PanelVisual: FC<PanelProps> = props => {
|
|
|
247
251
|
/>
|
|
248
252
|
</>
|
|
249
253
|
)}
|
|
250
|
-
{
|
|
251
|
-
<label className='header'>
|
|
252
|
-
<span className='edit-label'>Header Theme</span>
|
|
253
|
-
<ul className='color-palette'>
|
|
254
|
-
{headerColors.map(palette => (
|
|
255
|
-
<button
|
|
256
|
-
title={palette}
|
|
257
|
-
key={palette}
|
|
258
|
-
onClick={e => {
|
|
259
|
-
e.preventDefault()
|
|
260
|
-
updateConfig({ ...config, theme: palette })
|
|
261
|
-
}}
|
|
262
|
-
className={config.theme === palette ? 'selected ' + palette : palette}
|
|
263
|
-
></button>
|
|
264
|
-
))}
|
|
265
|
-
</ul>
|
|
266
|
-
</label>
|
|
267
|
-
{/* eslint-enable */}
|
|
254
|
+
<HeaderThemeSelector selectedTheme={config.theme} onThemeSelect={theme => updateConfig({ ...config, theme })} />
|
|
268
255
|
{(visSupportsNonSequentialPallete() || visSupportsNonSequentialPallete()) && (
|
|
269
256
|
<>
|
|
270
257
|
<label>
|
|
@@ -344,6 +331,74 @@ const PanelVisual: FC<PanelProps> = props => {
|
|
|
344
331
|
/>
|
|
345
332
|
</>
|
|
346
333
|
)}
|
|
334
|
+
|
|
335
|
+
{isCoveDeveloperMode() && (visSupportsSequentialPallete() || visSupportsNonSequentialPallete()) && (
|
|
336
|
+
<>
|
|
337
|
+
<div className='mt-3'>
|
|
338
|
+
<label className='checkbox'>
|
|
339
|
+
<input
|
|
340
|
+
type='checkbox'
|
|
341
|
+
checked={
|
|
342
|
+
!!(config.general?.palette?.customColorsOrdered || config.general?.palette?.customColors)
|
|
343
|
+
}
|
|
344
|
+
onChange={e => {
|
|
345
|
+
const _state = cloneConfig(config)
|
|
346
|
+
// Ensure palette object exists
|
|
347
|
+
if (!_state.general.palette) {
|
|
348
|
+
_state.general.palette = {}
|
|
349
|
+
}
|
|
350
|
+
if (e.target.checked) {
|
|
351
|
+
// Extract colors from current color scale if runtime data available
|
|
352
|
+
if (config.runtime?.seriesLabelsAll && config.runtime.seriesLabelsAll.length > 0) {
|
|
353
|
+
const colorScale = getColorScale(config)
|
|
354
|
+
const extractedColors = config.runtime.seriesLabelsAll.map((label: string) =>
|
|
355
|
+
colorScale(label)
|
|
356
|
+
)
|
|
357
|
+
_state.general.palette.customColorsOrdered =
|
|
358
|
+
extractedColors.length > 0
|
|
359
|
+
? extractedColors
|
|
360
|
+
: ['#3366cc', '#5588dd', '#77aaee', '#99ccff']
|
|
361
|
+
} else {
|
|
362
|
+
// Fallback to default colors if runtime not available
|
|
363
|
+
_state.general.palette.customColorsOrdered = ['#3366cc', '#5588dd', '#77aaee', '#99ccff']
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
// Remove custom colors and revert to default palette
|
|
367
|
+
delete _state.general.palette.customColorsOrdered
|
|
368
|
+
delete _state.general.palette.customColors
|
|
369
|
+
// Set default palette if none exists
|
|
370
|
+
if (!_state.general.palette.name) {
|
|
371
|
+
_state.general.palette.name = 'qualitative_standard'
|
|
372
|
+
_state.general.palette.version = '2.0'
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
updateConfig(_state)
|
|
376
|
+
}}
|
|
377
|
+
/>
|
|
378
|
+
Use Custom Colors
|
|
379
|
+
</label>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{(config.general?.palette?.customColorsOrdered || config.general?.palette?.customColors) && (
|
|
383
|
+
<div className='mt-2'>
|
|
384
|
+
<CustomColorsEditor
|
|
385
|
+
colors={config.general.palette.customColorsOrdered || config.general.palette.customColors}
|
|
386
|
+
onChange={newColors => {
|
|
387
|
+
const _state = cloneConfig(config)
|
|
388
|
+
if (!_state.general.palette) {
|
|
389
|
+
_state.general.palette = {}
|
|
390
|
+
}
|
|
391
|
+
_state.general.palette.customColorsOrdered = newColors
|
|
392
|
+
updateConfig(_state)
|
|
393
|
+
}}
|
|
394
|
+
label='Custom Color Order'
|
|
395
|
+
minColors={1}
|
|
396
|
+
maxColors={20}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
</>
|
|
401
|
+
)}
|
|
347
402
|
</>
|
|
348
403
|
)}
|
|
349
404
|
{config.visualizationType === 'Sankey' && (
|
|
@@ -443,7 +498,6 @@ const PanelVisual: FC<PanelProps> = props => {
|
|
|
443
498
|
value={config.dataCutoff}
|
|
444
499
|
type='number'
|
|
445
500
|
fieldName='dataCutoff'
|
|
446
|
-
className='number-narrow'
|
|
447
501
|
label='Data Cutoff'
|
|
448
502
|
updateField={updateField}
|
|
449
503
|
tooltip={
|
|
@@ -7,6 +7,7 @@ import Visual from './Panel.Visual'
|
|
|
7
7
|
import Sankey from './Panel.Sankey'
|
|
8
8
|
import Annotate from './Panel.Annotate'
|
|
9
9
|
import PatternSettings from './Panel.PatternSettings'
|
|
10
|
+
import SmallMultiples from './Panel.SmallMultiples'
|
|
10
11
|
|
|
11
12
|
const Panels = {
|
|
12
13
|
ForestPlot: ForestPlotSettings,
|
|
@@ -17,7 +18,8 @@ const Panels = {
|
|
|
17
18
|
Visual,
|
|
18
19
|
Sankey,
|
|
19
20
|
Annotate,
|
|
20
|
-
PatternSettings
|
|
21
|
+
PatternSettings,
|
|
22
|
+
SmallMultiples
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export default Panels
|
|
@@ -7,26 +7,6 @@
|
|
|
7
7
|
text-align: left;
|
|
8
8
|
|
|
9
9
|
span {
|
|
10
|
-
display: inline-block;
|
|
11
|
-
float: right;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.edit-block {
|
|
16
|
-
margin-top: 0;
|
|
17
|
-
padding: 1em;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.viewport-overrides {
|
|
22
|
-
button {
|
|
23
|
-
width: 100%;
|
|
24
|
-
padding: 1em;
|
|
25
|
-
margin-top: 1em;
|
|
26
|
-
text-align: left;
|
|
27
|
-
|
|
28
|
-
span {
|
|
29
|
-
display: inline-block;
|
|
30
10
|
float: right;
|
|
31
11
|
}
|
|
32
12
|
}
|
|
@@ -22,20 +22,6 @@ export const useEditorPermissions = () => {
|
|
|
22
22
|
'Scatter Plot',
|
|
23
23
|
'Spark Line',
|
|
24
24
|
'Sankey'
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
const headerColors = [
|
|
28
|
-
'theme-blue',
|
|
29
|
-
'theme-purple',
|
|
30
|
-
'theme-brown',
|
|
31
|
-
'theme-teal',
|
|
32
|
-
'theme-pink',
|
|
33
|
-
'theme-orange',
|
|
34
|
-
'theme-slate',
|
|
35
|
-
'theme-indigo',
|
|
36
|
-
'theme-cyan',
|
|
37
|
-
'theme-green',
|
|
38
|
-
'theme-amber'
|
|
39
25
|
]
|
|
40
26
|
|
|
41
27
|
const visSupportsDateCategoryAxis = () => {
|
|
@@ -383,6 +369,12 @@ export const useEditorPermissions = () => {
|
|
|
383
369
|
)
|
|
384
370
|
}
|
|
385
371
|
|
|
372
|
+
const visSupportsSmallMultiples = () => {
|
|
373
|
+
const enabledCharts = ['Line', 'Bar', 'Area Chart', 'Combo', 'Box Plot', 'Scatter Plot']
|
|
374
|
+
if (enabledCharts.includes(visualizationType)) return true
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
|
|
386
378
|
const visSupportsYPadding = () => {
|
|
387
379
|
return !config.yAxis.inlineLabel || !config.yAxis.inlineLabel?.includes(' ')
|
|
388
380
|
}
|
|
@@ -411,7 +403,6 @@ export const useEditorPermissions = () => {
|
|
|
411
403
|
|
|
412
404
|
return {
|
|
413
405
|
enabledChartTypes,
|
|
414
|
-
headerColors,
|
|
415
406
|
visCanAnimate,
|
|
416
407
|
visHasAnchors,
|
|
417
408
|
visHasBarBorders,
|
|
@@ -460,6 +451,7 @@ export const useEditorPermissions = () => {
|
|
|
460
451
|
visSupportsValueAxisMax,
|
|
461
452
|
visSupportsValueAxisMin,
|
|
462
453
|
visSupportsDynamicSeries,
|
|
454
|
+
visSupportsSmallMultiples,
|
|
463
455
|
visSupportsYPadding,
|
|
464
456
|
visHasSingleSeriesTooltip,
|
|
465
457
|
visHasCategoricalAxis
|
|
@@ -14,6 +14,102 @@ import { curveMonotoneX } from '@visx/curve'
|
|
|
14
14
|
import { Bar, Area, LinePath } from '@visx/shape'
|
|
15
15
|
import { Group } from '@visx/group'
|
|
16
16
|
|
|
17
|
+
// Helper function to check if a value is numeric/calculable
|
|
18
|
+
const isCalculable = (value: any): boolean => {
|
|
19
|
+
if (value === null || value === undefined || value === '' || value === 'NA') return false
|
|
20
|
+
const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : Number(value)
|
|
21
|
+
return !isNaN(num) && isFinite(num)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Helper function to filter and sort forecast data, splitting into segments at gaps
|
|
25
|
+
const prepareForecastData = (
|
|
26
|
+
data: Record<string, any>[],
|
|
27
|
+
xAxisKey: string,
|
|
28
|
+
highKey: string,
|
|
29
|
+
lowKey: string
|
|
30
|
+
): Record<string, any>[][] => {
|
|
31
|
+
if (!data || data.length === 0) return []
|
|
32
|
+
|
|
33
|
+
// Filter out invalid data points (where confidence intervals are not calculable)
|
|
34
|
+
const validData = data.filter(d => {
|
|
35
|
+
const high = d[highKey]
|
|
36
|
+
const low = d[lowKey]
|
|
37
|
+
return isCalculable(high) && isCalculable(low) && d[xAxisKey]
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (validData.length === 0) return []
|
|
41
|
+
|
|
42
|
+
// Sort by date
|
|
43
|
+
const sortedData = [...validData].sort((a, b) => {
|
|
44
|
+
const dateA = Date.parse(a[xAxisKey])
|
|
45
|
+
const dateB = Date.parse(b[xAxisKey])
|
|
46
|
+
if (isNaN(dateA) || isNaN(dateB)) return 0
|
|
47
|
+
return dateA - dateB
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Split into segments when there are gaps
|
|
51
|
+
// Calculate intervals between consecutive points to detect gaps
|
|
52
|
+
const intervals: number[] = []
|
|
53
|
+
for (let i = 1; i < sortedData.length; i++) {
|
|
54
|
+
const currentDate = Date.parse(sortedData[i][xAxisKey])
|
|
55
|
+
const prevDate = Date.parse(sortedData[i - 1][xAxisKey])
|
|
56
|
+
if (!isNaN(currentDate) && !isNaN(prevDate)) {
|
|
57
|
+
intervals.push(currentDate - prevDate)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Calculate median interval (more robust than average for detecting gaps)
|
|
62
|
+
const medianInterval =
|
|
63
|
+
intervals.length > 0
|
|
64
|
+
? [...intervals].sort((a, b) => a - b)[Math.floor(intervals.length / 2)]
|
|
65
|
+
: 7 * 24 * 60 * 60 * 1000 // Default to 7 days if no intervals
|
|
66
|
+
|
|
67
|
+
// Threshold: gap is more than 2x the median interval, or more than 30 days
|
|
68
|
+
const gapThreshold = Math.max(medianInterval * 2, 30 * 24 * 60 * 60 * 1000)
|
|
69
|
+
|
|
70
|
+
const segments: Record<string, any>[][] = []
|
|
71
|
+
let currentSegment: Record<string, any>[] = []
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < sortedData.length; i++) {
|
|
74
|
+
const current = sortedData[i]
|
|
75
|
+
const prev = sortedData[i - 1]
|
|
76
|
+
|
|
77
|
+
if (i === 0) {
|
|
78
|
+
// First data point starts a new segment
|
|
79
|
+
currentSegment = [current]
|
|
80
|
+
} else {
|
|
81
|
+
const currentDate = Date.parse(current[xAxisKey])
|
|
82
|
+
const prevDate = Date.parse(prev[xAxisKey])
|
|
83
|
+
|
|
84
|
+
if (isNaN(currentDate) || isNaN(prevDate)) {
|
|
85
|
+
// If dates are invalid, continue current segment
|
|
86
|
+
currentSegment.push(current)
|
|
87
|
+
} else {
|
|
88
|
+
const interval = currentDate - prevDate
|
|
89
|
+
const hasGap = interval > gapThreshold
|
|
90
|
+
|
|
91
|
+
if (hasGap) {
|
|
92
|
+
// Save current segment and start a new one
|
|
93
|
+
if (currentSegment.length > 0) {
|
|
94
|
+
segments.push(currentSegment)
|
|
95
|
+
}
|
|
96
|
+
currentSegment = [current]
|
|
97
|
+
} else {
|
|
98
|
+
// Continue current segment
|
|
99
|
+
currentSegment.push(current)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add the last segment
|
|
106
|
+
if (currentSegment.length > 0) {
|
|
107
|
+
segments.push(currentSegment)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return segments.length > 0 ? segments : [sortedData]
|
|
111
|
+
}
|
|
112
|
+
|
|
17
113
|
const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, handleTooltipMouseOff }) => {
|
|
18
114
|
const { transformedData: data, rawData, config, seriesHighlight, parseDate } = useContext(ConfigContext)
|
|
19
115
|
const { xAxis, yAxis, legend, runtime } = config
|
|
@@ -61,7 +157,7 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
61
157
|
return (
|
|
62
158
|
<Group
|
|
63
159
|
className={`forecasting-areas-combo-${index}`}
|
|
64
|
-
key={`forecasting-areas--stage-${replace(stage.key, /
|
|
160
|
+
key={`forecasting-areas--stage-${replace(stage.key, / /g, '—')}-${index}`}
|
|
65
161
|
>
|
|
66
162
|
{group.confidenceIntervals?.map((ciGroup, ciGroupIndex) => {
|
|
67
163
|
const palette = forecastingPalettes[stage.color] || false
|
|
@@ -78,35 +174,57 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
78
174
|
return isReversed ? palette?.[1] || 'transparent' : palette?.[4] || 'transparent'
|
|
79
175
|
}
|
|
80
176
|
|
|
81
|
-
if (ciGroup.high === '' || ciGroup.low === '') return
|
|
177
|
+
if (ciGroup.high === '' || ciGroup.low === '') return null
|
|
178
|
+
|
|
179
|
+
// Prepare data: filter invalid values, sort by date, and split into segments at gaps
|
|
180
|
+
const dataSegments = prepareForecastData(bridgedData, xAxis.dataKey, ciGroup.high, ciGroup.low)
|
|
181
|
+
|
|
82
182
|
return (
|
|
83
183
|
<Group
|
|
84
184
|
key={`forecasting-areas--stage-${replace(
|
|
85
185
|
stage.key,
|
|
86
|
-
/
|
|
186
|
+
/ /g,
|
|
87
187
|
'—'
|
|
88
188
|
)}--group-${stageIndex}-${ciGroupIndex}`}
|
|
89
189
|
>
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
curve={curveMonotoneX}
|
|
93
|
-
data={bridgedData}
|
|
94
|
-
fill={getFill()}
|
|
95
|
-
opacity={transparentArea? 0.1 : 0.5 }
|
|
96
|
-
x={d => xScale(Date.parse(d[xAxis.dataKey]))}
|
|
97
|
-
y0={d => yScale(d[ciGroup.low])}
|
|
98
|
-
y1={d => yScale(d[ciGroup.high])}
|
|
99
|
-
/>
|
|
100
|
-
|
|
101
|
-
{ciGroupIndex === 0 && (
|
|
102
|
-
<>
|
|
190
|
+
{dataSegments.map((segment, segmentIndex) => (
|
|
191
|
+
<Group key={`segment-${segmentIndex}`}>
|
|
103
192
|
{/* prettier-ignore */}
|
|
104
|
-
<
|
|
193
|
+
<Area
|
|
194
|
+
curve={curveMonotoneX}
|
|
195
|
+
data={segment}
|
|
196
|
+
fill={getFill()}
|
|
197
|
+
opacity={transparentArea ? 0.1 : 0.5}
|
|
198
|
+
x={d => xScale(Date.parse(d[xAxis.dataKey]))}
|
|
199
|
+
y0={d => yScale(d[ciGroup.low])}
|
|
200
|
+
y1={d => yScale(d[ciGroup.high])}
|
|
201
|
+
/>
|
|
105
202
|
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
203
|
+
{ciGroupIndex === 0 && (
|
|
204
|
+
<>
|
|
205
|
+
<LinePath
|
|
206
|
+
data={segment}
|
|
207
|
+
x={d => Number(xScale(Date.parse(d[xAxis.dataKey])))}
|
|
208
|
+
y={d => Number(yScale(d[ciGroup.high]))}
|
|
209
|
+
curve={curveMonotoneX}
|
|
210
|
+
stroke={getStroke()}
|
|
211
|
+
strokeWidth={1}
|
|
212
|
+
strokeOpacity={1}
|
|
213
|
+
/>
|
|
214
|
+
|
|
215
|
+
<LinePath
|
|
216
|
+
data={segment}
|
|
217
|
+
x={d => Number(xScale(Date.parse(d[xAxis.dataKey])))}
|
|
218
|
+
y={d => Number(yScale(d[ciGroup.low]))}
|
|
219
|
+
curve={curveMonotoneX}
|
|
220
|
+
stroke={getStroke()}
|
|
221
|
+
strokeWidth={1}
|
|
222
|
+
strokeOpacity={1}
|
|
223
|
+
/>
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</Group>
|
|
227
|
+
))}
|
|
110
228
|
</Group>
|
|
111
229
|
)
|
|
112
230
|
})}
|
|
@@ -23,7 +23,7 @@ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
|
23
23
|
|
|
24
24
|
const LEGEND_PADDING = 36
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface LegendProps {
|
|
27
27
|
colorScale: ColorScale
|
|
28
28
|
config: ChartConfig
|
|
29
29
|
currentViewport: ViewportSize
|
|
@@ -149,9 +149,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
149
149
|
eventType: `chart_legend_item_toggled` as any,
|
|
150
150
|
eventAction: 'keydown',
|
|
151
151
|
eventLabel: interactionLabel,
|
|
152
|
-
specifics:
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
specifics:
|
|
153
|
+
config.visualizationType === 'Bar'
|
|
154
|
+
? `label: ${label.text}, orientation: ${
|
|
155
|
+
config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
|
|
156
|
+
}, mode: ${legend.behavior}`
|
|
157
|
+
: `label: ${label.text}, mode: ${legend.behavior}`
|
|
155
158
|
})
|
|
156
159
|
highlight(label)
|
|
157
160
|
}
|
|
@@ -164,9 +167,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
164
167
|
eventType: `chart_legend_item_toggled` as any,
|
|
165
168
|
eventAction: 'click',
|
|
166
169
|
eventLabel: interactionLabel,
|
|
167
|
-
specifics:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
specifics:
|
|
171
|
+
config.visualizationType === 'Bar'
|
|
172
|
+
? `label: ${label.text}, orientation: ${
|
|
173
|
+
config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
|
|
174
|
+
}, mode: ${legend.behavior}`
|
|
175
|
+
: `label: ${label.text}, mode: ${legend.behavior}`,
|
|
170
176
|
|
|
171
177
|
vizTitle: getVizTitle(config)
|
|
172
178
|
})
|
|
@@ -238,8 +244,9 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
238
244
|
{/* Pattern Legend Items */}
|
|
239
245
|
{config.legend.patterns && Object.keys(config.legend.patterns).length > 0 && (
|
|
240
246
|
<div
|
|
241
|
-
className={`legend-patterns d-flex ${
|
|
242
|
-
|
|
247
|
+
className={`legend-patterns d-flex ${
|
|
248
|
+
['top', 'bottom'].includes(config.legend.position) ? 'flex-row flex-wrap' : 'flex-column'
|
|
249
|
+
}`}
|
|
243
250
|
>
|
|
244
251
|
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
245
252
|
const patternId = `legend-pattern-${key}`
|