@cdc/chart 4.25.8 → 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/.claude/settings.local.json +9 -0
- package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +44236 -40355
- package/examples/feature/__data__/planet-example-data.json +0 -30
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/grouped-bar-test.json +400 -0
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/d.json +382 -0
- package/examples/private/example-2.json +49784 -0
- package/examples/private/f2.json +1 -0
- package/examples/private/f4.json +1577 -0
- package/examples/private/forecast.json +1180 -0
- package/examples/private/lollipop.json +468 -0
- package/examples/private/na.json +913 -0
- package/examples/private/new.json +48756 -0
- package/examples/private/pie-chart-legend.json +904 -0
- package/examples/private/test-data.csv +28 -0
- package/examples/suppressed_tooltip.json +480 -0
- package/index.html +2 -133
- package/package.json +25 -7
- package/src/CdcChart.tsx +9 -13
- package/src/CdcChartComponent.tsx +403 -92
- package/src/_stories/Chart.Anchors.stories.tsx +2 -2
- package/src/_stories/Chart.BoxPlot.stories.tsx +1 -1
- package/src/_stories/Chart.CI.stories.tsx +1 -1
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +2 -2
- package/src/_stories/Chart.Filters.stories.tsx +2 -2
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.Patterns.stories.tsx +20 -0
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.ScatterPlot.stories.tsx +1 -1
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/Chart.stories.tsx +8 -5
- package/src/_stories/Chart.tooltip.stories.tsx +1 -1
- package/src/_stories/ChartAnnotation.stories.tsx +7 -4
- package/src/_stories/ChartAxisLabels.stories.tsx +2 -2
- package/src/_stories/ChartAxisTitles.stories.tsx +2 -2
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +59 -60
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +1 -1
- package/src/_stories/ChartPrefixSuffix.stories.tsx +2 -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/_stories/_mock/stacked-pattern-test.json +520 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +1 -0
- package/src/components/Annotations/components/AnnotationDropdown.tsx +1 -1
- 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 +161 -22
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +138 -5
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +215 -73
- package/src/components/BarChart/components/BarChart.Vertical.tsx +155 -22
- package/src/components/BarChart/helpers/index.ts +43 -4
- package/src/components/BarChart/helpers/lollipopColors.ts +27 -0
- package/src/components/BarChart/helpers/useBarChart.ts +25 -3
- package/src/components/BoxPlot/BoxPlot.Vertical.tsx +2 -1
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/DeviationBar.jsx +9 -6
- package/src/components/EditorPanel/EditorPanel.tsx +563 -229
- package/src/components/EditorPanel/EditorPanelContext.ts +3 -0
- 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 +461 -0
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +80 -67
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +188 -139
- package/src/components/EditorPanel/components/Panels/index.tsx +5 -1
- package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +0 -8
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +49 -48
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +175 -27
- package/src/components/ForestPlot/ForestPlot.tsx +11 -7
- package/src/components/ForestPlot/ForestPlotProps.ts +1 -1
- package/src/components/Legend/Legend.Component.tsx +114 -14
- package/src/components/Legend/helpers/createFormatLabels.tsx +230 -171
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LegendWrapper.tsx +1 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/components/LineChart.Circle.tsx +2 -2
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +38 -15
- package/src/components/LinearChart.tsx +96 -84
- package/src/components/PairedBarChart.jsx +6 -4
- package/src/components/PieChart/PieChart.tsx +170 -54
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/components/Sankey.tsx +7 -1
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +32 -4
- 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 +327 -293
- package/src/helpers/buildForecastPaletteMappings.ts +112 -0
- package/src/helpers/buildForecastPaletteOptions.ts +71 -0
- package/src/helpers/getColorScale.ts +82 -8
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getNewRuntime.ts +1 -1
- package/src/helpers/getTransformedData.ts +1 -1
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useChartHoverAnalytics.tsx +44 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useReduceData.ts +105 -70
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +116 -29
- package/src/index.jsx +0 -2
- package/src/scss/main.scss +13 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +5 -1
- package/src/test/CdcChart.test.jsx +8 -3
- package/src/types/ChartConfig.ts +53 -11
- package/src/types/ChartContext.ts +4 -0
- package/vite.config.js +1 -1
- package/vitest.config.ts +16 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/coreStyles_chart.scss +0 -3
- package/src/helpers/configHelpers.ts +0 -28
- package/src/helpers/generateColorsArray.ts +0 -8
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
- package/src/hooks/useColorPalette.js +0 -76
|
@@ -1,21 +1,145 @@
|
|
|
1
|
-
import React, { useContext } from 'react'
|
|
1
|
+
import React, { useContext, useMemo } from 'react'
|
|
2
2
|
import { replace } from 'lodash'
|
|
3
3
|
// cdc
|
|
4
4
|
import ConfigContext from '../../ConfigContext'
|
|
5
5
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
6
|
-
import {
|
|
6
|
+
import { colorPalettesChartV2, sequentialPalettes } from '@cdc/core/data/colorPalettes'
|
|
7
|
+
import { updatePaletteNames } from '@cdc/core/helpers/updatePaletteNames'
|
|
8
|
+
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
7
9
|
import { getBridgedData } from '../../helpers/getBridgedData'
|
|
10
|
+
import { buildForecastPaletteMappings } from '../../helpers/buildForecastPaletteMappings'
|
|
8
11
|
|
|
9
12
|
// visx & d3
|
|
10
13
|
import { curveMonotoneX } from '@visx/curve'
|
|
11
14
|
import { Bar, Area, LinePath } from '@visx/shape'
|
|
12
15
|
import { Group } from '@visx/group'
|
|
13
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
|
+
|
|
14
113
|
const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, handleTooltipMouseOff }) => {
|
|
15
114
|
const { transformedData: data, rawData, config, seriesHighlight, parseDate } = useContext(ConfigContext)
|
|
16
115
|
const { xAxis, yAxis, legend, runtime } = config
|
|
17
116
|
const DEBUG = false
|
|
18
117
|
|
|
118
|
+
// Memoize processed palettes - use version-specific palettes
|
|
119
|
+
const forecastingPalettes = useMemo(() => {
|
|
120
|
+
// Determine palette version from config
|
|
121
|
+
// Forecasting charts use sequentialPalettes for v1, sequential-only palettes for v2
|
|
122
|
+
const paletteVersion = getColorPaletteVersion(config)
|
|
123
|
+
|
|
124
|
+
let forecastPalettes
|
|
125
|
+
if (paletteVersion === 1) {
|
|
126
|
+
// V1: Use original sequential palettes
|
|
127
|
+
forecastPalettes = sequentialPalettes
|
|
128
|
+
} else {
|
|
129
|
+
// V2: Only use sequential palettes (filter out divergent and qualitative)
|
|
130
|
+
const allV2Palettes = colorPalettesChartV2
|
|
131
|
+
forecastPalettes = {}
|
|
132
|
+
Object.keys(allV2Palettes).forEach(key => {
|
|
133
|
+
if (key.startsWith('sequential')) {
|
|
134
|
+
forecastPalettes[key] = allV2Palettes[key]
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const processedPalettes = updatePaletteNames(forecastPalettes)
|
|
140
|
+
return buildForecastPaletteMappings(processedPalettes, paletteVersion)
|
|
141
|
+
}, [config])
|
|
142
|
+
|
|
19
143
|
return (
|
|
20
144
|
data && (
|
|
21
145
|
<ErrorBoundary component='ForecastingChart'>
|
|
@@ -33,50 +157,74 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
33
157
|
return (
|
|
34
158
|
<Group
|
|
35
159
|
className={`forecasting-areas-combo-${index}`}
|
|
36
|
-
key={`forecasting-areas--stage-${replace(stage.key, /
|
|
160
|
+
key={`forecasting-areas--stage-${replace(stage.key, / /g, '—')}-${index}`}
|
|
37
161
|
>
|
|
38
162
|
{group.confidenceIntervals?.map((ciGroup, ciGroupIndex) => {
|
|
39
|
-
const palette =
|
|
163
|
+
const palette = forecastingPalettes[stage.color] || false
|
|
164
|
+
const isReversed = stage.color?.toLowerCase().includes('reverse')
|
|
40
165
|
|
|
41
166
|
const getFill = () => {
|
|
42
|
-
if (displayArea) return palette[2]
|
|
167
|
+
if (displayArea) return palette?.[2] || 'transparent'
|
|
43
168
|
return 'transparent'
|
|
44
169
|
}
|
|
45
170
|
|
|
46
171
|
const getStroke = () => {
|
|
47
|
-
if (displayArea) return
|
|
48
|
-
|
|
172
|
+
if (!displayArea) return 'transparent'
|
|
173
|
+
// Use darker colors: index 1 for reversed (dark at start), index 4 for non-reversed (dark at end)
|
|
174
|
+
return isReversed ? palette?.[1] || 'transparent' : palette?.[4] || 'transparent'
|
|
49
175
|
}
|
|
50
176
|
|
|
51
|
-
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
|
+
|
|
52
182
|
return (
|
|
53
183
|
<Group
|
|
54
184
|
key={`forecasting-areas--stage-${replace(
|
|
55
185
|
stage.key,
|
|
56
|
-
/
|
|
186
|
+
/ /g,
|
|
57
187
|
'—'
|
|
58
188
|
)}--group-${stageIndex}-${ciGroupIndex}`}
|
|
59
189
|
>
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
curve={curveMonotoneX}
|
|
63
|
-
data={bridgedData}
|
|
64
|
-
fill={getFill()}
|
|
65
|
-
opacity={transparentArea? 0.1 : 0.5 }
|
|
66
|
-
x={d => xScale(Date.parse(d[xAxis.dataKey]))}
|
|
67
|
-
y0={d => yScale(d[ciGroup.low])}
|
|
68
|
-
y1={d => yScale(d[ciGroup.high])}
|
|
69
|
-
/>
|
|
70
|
-
|
|
71
|
-
{ciGroupIndex === 0 && (
|
|
72
|
-
<>
|
|
190
|
+
{dataSegments.map((segment, segmentIndex) => (
|
|
191
|
+
<Group key={`segment-${segmentIndex}`}>
|
|
73
192
|
{/* prettier-ignore */}
|
|
74
|
-
<
|
|
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
|
+
/>
|
|
75
202
|
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
))}
|
|
80
228
|
</Group>
|
|
81
229
|
)
|
|
82
230
|
})}
|
|
@@ -8,9 +8,9 @@ import { scaleLinear } from '@visx/scale'
|
|
|
8
8
|
import { curveLinearClosed } from '@visx/curve'
|
|
9
9
|
|
|
10
10
|
// types
|
|
11
|
-
import { type ForestPlotProps } from '
|
|
12
|
-
import { type ChartConfig } from '
|
|
13
|
-
import { type ChartContext } from '
|
|
11
|
+
import { type ForestPlotProps } from './ForestPlotProps'
|
|
12
|
+
import { type ChartConfig } from '../../types/ChartConfig'
|
|
13
|
+
import { type ChartContext } from '../../types/ChartContext'
|
|
14
14
|
|
|
15
15
|
// cdc
|
|
16
16
|
import ConfigContext from '../../ConfigContext'
|
|
@@ -73,7 +73,7 @@ const ForestPlot = ({
|
|
|
73
73
|
|
|
74
74
|
updateConfig(newConfig)
|
|
75
75
|
} catch (e) {
|
|
76
|
-
console.
|
|
76
|
+
console.error(e.message)
|
|
77
77
|
}
|
|
78
78
|
}, [])
|
|
79
79
|
|
|
@@ -184,7 +184,7 @@ const ForestPlot = ({
|
|
|
184
184
|
const isTotalColumn = d[config.xAxis.dataKey] === forestPlot.pooledResult.column
|
|
185
185
|
|
|
186
186
|
return !isTotalColumn ? (
|
|
187
|
-
<Group>
|
|
187
|
+
<Group key={`forest-plot-row-${i}-${d[config.xAxis.dataKey]}`}>
|
|
188
188
|
{/* Confidence Interval Paths */}
|
|
189
189
|
<path
|
|
190
190
|
stroke={lineColor}
|
|
@@ -252,6 +252,7 @@ const ForestPlot = ({
|
|
|
252
252
|
</Group>
|
|
253
253
|
) : (
|
|
254
254
|
<LinePath
|
|
255
|
+
key={`forest-plot-regression-${i}-${d[config.xAxis.dataKey]}`}
|
|
255
256
|
data={regressionPoints}
|
|
256
257
|
x={d => d.x}
|
|
257
258
|
y={d => d.y - APP_FONT_SIZE / 2}
|
|
@@ -314,10 +315,11 @@ const ForestPlot = ({
|
|
|
314
315
|
/>
|
|
315
316
|
|
|
316
317
|
{/* column data */}
|
|
317
|
-
{columnsOnChart.map(column => {
|
|
318
|
+
{columnsOnChart.map((column, colIndex) => {
|
|
318
319
|
return data.map((d, i) => {
|
|
319
320
|
return (
|
|
320
321
|
<Text
|
|
322
|
+
key={`forest-plot-column-${colIndex}-${i}-${d[config.xAxis.dataKey]}`}
|
|
321
323
|
className={`${d[column.name]}`}
|
|
322
324
|
x={column.forestPlotAlignRight ? width : column.forestPlotStartingPoint}
|
|
323
325
|
y={yScale(i)}
|
|
@@ -336,6 +338,7 @@ const ForestPlot = ({
|
|
|
336
338
|
data.map((d, i) => {
|
|
337
339
|
return (
|
|
338
340
|
<Text
|
|
341
|
+
key={`forest-plot-xaxis-${i}-${d[config.xAxis.dataKey]}`}
|
|
339
342
|
className={`${d[config.xAxis.dataKey]}`}
|
|
340
343
|
x={0}
|
|
341
344
|
y={yScale(i)}
|
|
@@ -356,9 +359,10 @@ const ForestPlot = ({
|
|
|
356
359
|
)}
|
|
357
360
|
|
|
358
361
|
{/* column headers */}
|
|
359
|
-
{columnsOnChart.map(column => {
|
|
362
|
+
{columnsOnChart.map((column, colIndex) => {
|
|
360
363
|
return (
|
|
361
364
|
<Text
|
|
365
|
+
key={`forest-plot-header-${colIndex}-${column.label}`}
|
|
362
366
|
className={`${column.label}`}
|
|
363
367
|
x={column.forestPlotAlignRight ? width : column.forestPlotStartingPoint}
|
|
364
368
|
y={0}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import parse from 'html-react-parser'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { LegendOrdinal, LegendItem, LegendLabel } from '@visx/legend'
|
|
4
|
+
import { PatternLines, PatternCircles, PatternWaves } from '@visx/pattern'
|
|
4
5
|
import LegendShape from '@cdc/core/components/LegendShape'
|
|
5
6
|
import Button from '@cdc/core/components/elements/Button'
|
|
6
7
|
import { getLegendClasses } from './helpers/getLegendClasses'
|
|
@@ -18,10 +19,11 @@ import LegendLineShape from './LegendLine.Shape'
|
|
|
18
19
|
import LegendGroup from './LegendGroup'
|
|
19
20
|
import { getSeriesWithData } from '../../helpers/dataHelpers'
|
|
20
21
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
22
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
21
23
|
|
|
22
24
|
const LEGEND_PADDING = 36
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
interface LegendProps {
|
|
25
27
|
colorScale: ColorScale
|
|
26
28
|
config: ChartConfig
|
|
27
29
|
currentViewport: ViewportSize
|
|
@@ -36,7 +38,6 @@ export interface LegendProps {
|
|
|
36
38
|
interactionLabel: string
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
|
|
40
41
|
const Legend: React.FC<LegendProps> = forwardRef(
|
|
41
42
|
(
|
|
42
43
|
{
|
|
@@ -72,6 +73,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
72
73
|
|
|
73
74
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
74
75
|
let highLightedLegendItems = HighLightedBarUtils.findDuplicates(config.highlightedBarValues)
|
|
76
|
+
|
|
75
77
|
if (!legend) return null
|
|
76
78
|
return (
|
|
77
79
|
<aside
|
|
@@ -140,23 +142,40 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
140
142
|
onKeyDown={e => {
|
|
141
143
|
if (e.key === 'Enter') {
|
|
142
144
|
e.preventDefault()
|
|
143
|
-
publishAnalyticsEvent(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
publishAnalyticsEvent({
|
|
146
|
+
vizType: config?.type,
|
|
147
|
+
vizSubType: getVizSubType(config),
|
|
148
|
+
vizTitle: getVizTitle(config),
|
|
149
|
+
eventType: `chart_legend_item_toggled` as any,
|
|
150
|
+
eventAction: 'keydown',
|
|
151
|
+
eventLabel: interactionLabel,
|
|
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}`
|
|
158
|
+
})
|
|
149
159
|
highlight(label)
|
|
150
160
|
}
|
|
151
161
|
}}
|
|
152
162
|
onClick={e => {
|
|
153
163
|
e.preventDefault()
|
|
154
|
-
publishAnalyticsEvent(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
'
|
|
159
|
-
|
|
164
|
+
publishAnalyticsEvent({
|
|
165
|
+
vizType: config?.type,
|
|
166
|
+
vizSubType: getVizSubType(config),
|
|
167
|
+
eventType: `chart_legend_item_toggled` as any,
|
|
168
|
+
eventAction: 'click',
|
|
169
|
+
eventLabel: interactionLabel,
|
|
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}`,
|
|
176
|
+
|
|
177
|
+
vizTitle: getVizTitle(config)
|
|
178
|
+
})
|
|
160
179
|
highlight(label)
|
|
161
180
|
}}
|
|
162
181
|
role='button'
|
|
@@ -221,6 +240,87 @@ const Legend: React.FC<LegendProps> = forwardRef(
|
|
|
221
240
|
</div>
|
|
222
241
|
|
|
223
242
|
<LegendSuppression config={config} isLegendBottom={isLegendBottom} />
|
|
243
|
+
|
|
244
|
+
{/* Pattern Legend Items */}
|
|
245
|
+
{config.legend.patterns && Object.keys(config.legend.patterns).length > 0 && (
|
|
246
|
+
<div
|
|
247
|
+
className={`legend-patterns d-flex ${
|
|
248
|
+
['top', 'bottom'].includes(config.legend.position) ? 'flex-row flex-wrap' : 'flex-column'
|
|
249
|
+
}`}
|
|
250
|
+
>
|
|
251
|
+
{Object.entries(config.legend.patterns).map(([key, pattern]) => {
|
|
252
|
+
const patternId = `legend-pattern-${key}`
|
|
253
|
+
const size = config.legend.patternSize || 8
|
|
254
|
+
const legendSize = 16
|
|
255
|
+
const pColor = (pattern as any)?.color || '#666666'
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<LegendItem
|
|
259
|
+
key={patternId}
|
|
260
|
+
className='legend-item legend-item--pattern d-flex align-items-center'
|
|
261
|
+
tabIndex={0}
|
|
262
|
+
role='button'
|
|
263
|
+
>
|
|
264
|
+
<span className='me-2'>
|
|
265
|
+
<svg width={legendSize} height={legendSize}>
|
|
266
|
+
<defs>
|
|
267
|
+
{pattern.shape === 'circles' && (
|
|
268
|
+
<PatternCircles
|
|
269
|
+
id={patternId}
|
|
270
|
+
height={size}
|
|
271
|
+
width={size}
|
|
272
|
+
fill={pColor}
|
|
273
|
+
radius={1.25}
|
|
274
|
+
/>
|
|
275
|
+
)}
|
|
276
|
+
{pattern.shape === 'lines' && (
|
|
277
|
+
<PatternLines
|
|
278
|
+
id={patternId}
|
|
279
|
+
height={size}
|
|
280
|
+
width={size}
|
|
281
|
+
stroke={pColor}
|
|
282
|
+
strokeWidth={0.75}
|
|
283
|
+
orientation={['horizontal']}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
{pattern.shape === 'diagonalLines' && (
|
|
287
|
+
<PatternLines
|
|
288
|
+
id={patternId}
|
|
289
|
+
height={size}
|
|
290
|
+
width={size}
|
|
291
|
+
stroke={pColor}
|
|
292
|
+
strokeWidth={0.75}
|
|
293
|
+
orientation={['diagonalRightToLeft']}
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
296
|
+
{pattern.shape === 'waves' && (
|
|
297
|
+
<PatternWaves
|
|
298
|
+
id={patternId}
|
|
299
|
+
height={size}
|
|
300
|
+
width={size}
|
|
301
|
+
fill={pColor}
|
|
302
|
+
strokeWidth={0.25}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
</defs>
|
|
306
|
+
<circle
|
|
307
|
+
fill={`url(#${patternId})`}
|
|
308
|
+
r={legendSize / 2}
|
|
309
|
+
cx={legendSize / 2}
|
|
310
|
+
cy={legendSize / 2}
|
|
311
|
+
stroke='#0000004d'
|
|
312
|
+
strokeWidth={1}
|
|
313
|
+
/>
|
|
314
|
+
</svg>
|
|
315
|
+
</span>
|
|
316
|
+
<LegendLabel align='left' className='m-0'>
|
|
317
|
+
{parse(String((pattern as any)?.label || key))}
|
|
318
|
+
</LegendLabel>
|
|
319
|
+
</LegendItem>
|
|
320
|
+
)
|
|
321
|
+
})}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
224
324
|
</>
|
|
225
325
|
)
|
|
226
326
|
}}
|