@cdc/chart 4.25.6-2 → 4.25.7
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.js +52634 -30848
- package/package.json +3 -2
- package/src/CdcChartComponent.tsx +13 -9
- package/src/_stories/Chart.BoxPlot.stories.tsx +35 -0
- package/src/_stories/Chart.stories.tsx +0 -7
- package/src/_stories/Chart.tooltip.stories.tsx +35 -275
- package/src/_stories/_mock/bar-chart-suppressed.json +2 -80
- package/src/_stories/_mock/boxplot_multiseries.json +252 -166
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -1
- package/src/components/AreaChart/components/AreaChart.jsx +4 -8
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +34 -2
- package/src/components/BarChart/components/BarChart.Vertical.tsx +15 -0
- package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +131 -0
- package/src/components/BoxPlot/{BoxPlot.tsx → BoxPlot.Vertical.tsx} +4 -4
- package/src/components/BoxPlot/helpers/index.ts +32 -12
- package/src/components/BrushChart.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +3 -3
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +2 -2
- package/src/components/Forecasting/{Forecasting.jsx → Forecasting.tsx} +32 -12
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +1 -0
- package/src/components/Legend/helpers/index.ts +2 -2
- package/src/components/LinearChart.tsx +63 -15
- package/src/data/initial-state.js +1 -5
- package/src/helpers/filterAndShiftLinearDateTicks.ts +58 -0
- package/src/helpers/getBridgedData.ts +13 -0
- package/src/helpers/tests/getBridgedData.test.ts +64 -0
- package/src/hooks/useScales.ts +42 -42
- package/src/hooks/useTooltip.tsx +3 -2
- package/src/scss/main.scss +2 -8
- package/src/store/chart.actions.ts +2 -2
- package/src/store/chart.reducer.ts +4 -12
- package/src/types/ChartConfig.ts +0 -5
- package/examples/private/0527.json +0 -1
- package/examples/private/DEV-8850-2.json +0 -493
- package/examples/private/DEV-9822.json +0 -574
- package/examples/private/DEV-9840.json +0 -553
- package/examples/private/DEV-9850-3.json +0 -461
- package/examples/private/chart.json +0 -1084
- package/examples/private/ci_formatted.json +0 -202
- package/examples/private/ci_issue.json +0 -3016
- package/examples/private/completed.json +0 -634
- package/examples/private/dem-data-long.csv +0 -20
- package/examples/private/dem-data-long.json +0 -36
- package/examples/private/demographic_data.csv +0 -157
- package/examples/private/demographic_data.json +0 -2654
- package/examples/private/demographic_dynamic.json +0 -443
- package/examples/private/demographic_standard.json +0 -560
- package/examples/private/ehdi.json +0 -29939
- package/examples/private/line-issue.json +0 -497
- package/examples/private/not-loading.json +0 -360
- package/examples/private/test.json +0 -493
- package/examples/private/testing-pie.json +0 -436
- package/src/components/BoxPlot/index.tsx +0 -3
- /package/src/components/Brush/{BrushController..tsx → BrushController.tsx} +0 -0
|
@@ -59,7 +59,9 @@ export const BarChartHorizontal = () => {
|
|
|
59
59
|
|
|
60
60
|
const { HighLightedBarUtils } = useHighlightedBars(config)
|
|
61
61
|
|
|
62
|
-
const hasConfidenceInterval =
|
|
62
|
+
const hasConfidenceInterval = [config.confidenceKeys?.upper, config.confidenceKeys?.lower].every(
|
|
63
|
+
v => v != null && String(v).trim() !== ''
|
|
64
|
+
)
|
|
63
65
|
|
|
64
66
|
const _data = getBarData(config, data, hasConfidenceInterval)
|
|
65
67
|
|
|
@@ -262,6 +264,21 @@ export const BarChartHorizontal = () => {
|
|
|
262
264
|
}
|
|
263
265
|
})}
|
|
264
266
|
|
|
267
|
+
{(absentDataLabel || isSuppressed) && (
|
|
268
|
+
<rect
|
|
269
|
+
x={barX}
|
|
270
|
+
y={0}
|
|
271
|
+
width={yMax}
|
|
272
|
+
height={numbericBarHeight}
|
|
273
|
+
fill='transparent'
|
|
274
|
+
data-tooltip-place='top'
|
|
275
|
+
data-tooltip-offset='{"top":3}'
|
|
276
|
+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
|
|
277
|
+
data-tooltip-html={tooltip}
|
|
278
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
|
|
265
282
|
{config.preliminaryData?.map((pd, index) => {
|
|
266
283
|
// check if user selected column
|
|
267
284
|
const selectedSuppressionColumn = !pd.column || pd.column === bar.key
|
|
@@ -301,7 +318,7 @@ export const BarChartHorizontal = () => {
|
|
|
301
318
|
)
|
|
302
319
|
})}
|
|
303
320
|
|
|
304
|
-
{!config.isLollipopChart && (
|
|
321
|
+
{!config.isLollipopChart && !hasConfidenceInterval && (
|
|
305
322
|
<Text // prettier-ignore
|
|
306
323
|
display={displayBar ? 'block' : 'none'}
|
|
307
324
|
x={bar.y}
|
|
@@ -315,6 +332,21 @@ export const BarChartHorizontal = () => {
|
|
|
315
332
|
{testZeroValue(bar.value) ? '' : barDefaultLabel}
|
|
316
333
|
</Text>
|
|
317
334
|
)}
|
|
335
|
+
|
|
336
|
+
{!config.isLollipopChart && hasConfidenceInterval && (
|
|
337
|
+
<Text // prettier-ignore
|
|
338
|
+
display={displayBar ? 'block' : 'none'}
|
|
339
|
+
x={bar.value < 0 ? bar.y + barWidth : bar.y - barWidth}
|
|
340
|
+
opacity={transparentBar ? 0.5 : 1}
|
|
341
|
+
y={config.barHeight / 2 + config.barHeight * bar.index}
|
|
342
|
+
fill={labelColor}
|
|
343
|
+
dx={-textPadding}
|
|
344
|
+
verticalAnchor='middle'
|
|
345
|
+
textAnchor={bar.value < 0 ? 'end' : 'start'}
|
|
346
|
+
>
|
|
347
|
+
{testZeroValue(bar.value) ? '' : barDefaultLabel}
|
|
348
|
+
</Text>
|
|
349
|
+
)}
|
|
318
350
|
<Text // prettier-ignore
|
|
319
351
|
display={displayBar ? 'block' : 'none'}
|
|
320
352
|
x={bar.y}
|
|
@@ -288,6 +288,21 @@ export const BarChartVertical = () => {
|
|
|
288
288
|
}
|
|
289
289
|
})}
|
|
290
290
|
|
|
291
|
+
{(absentDataLabel || isSuppressed) && (
|
|
292
|
+
<rect
|
|
293
|
+
x={barX}
|
|
294
|
+
y={0}
|
|
295
|
+
width={barWidth}
|
|
296
|
+
height={yMax}
|
|
297
|
+
fill='transparent'
|
|
298
|
+
data-tooltip-place='top'
|
|
299
|
+
data-tooltip-offset='{"top":3}'
|
|
300
|
+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
|
|
301
|
+
data-tooltip-html={tooltip}
|
|
302
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
|
|
291
306
|
{config.preliminaryData.map((pd, index) => {
|
|
292
307
|
// check if user selected column
|
|
293
308
|
const selectedSuppressionColumn = !pd.column || pd.column === bar.key
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { Group } from '@visx/group'
|
|
3
|
+
import { BoxPlot } from '@visx/stats'
|
|
4
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
5
|
+
import ConfigContext from '../../ConfigContext'
|
|
6
|
+
import { handleTooltip, createPlots } from './helpers/index'
|
|
7
|
+
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
8
|
+
import _ from 'lodash'
|
|
9
|
+
|
|
10
|
+
const BoxPlotHorizontal = ({ xScale, yScale, seriesScale }) => {
|
|
11
|
+
const { config, transformedData: data, colorScale, seriesHighlight } = useContext(ConfigContext)
|
|
12
|
+
const yOffset = Number(config.xAxis.size)
|
|
13
|
+
const defaultColor = APP_FONT_COLOR
|
|
14
|
+
const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
|
|
15
|
+
// generate summary stats
|
|
16
|
+
const plots = createPlots(data, config)
|
|
17
|
+
const { plotNonOutlierValues, plotOutlierValues } = config.boxplot
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ErrorBoundary component='BoxPlot Horizontal'>
|
|
21
|
+
<Group left={yOffset} top={0} className='boxplot'>
|
|
22
|
+
{plots.map(plot => {
|
|
23
|
+
const category = plot.columnCategory
|
|
24
|
+
|
|
25
|
+
return config.series.map(item => {
|
|
26
|
+
const y0 = yScale(category)
|
|
27
|
+
const offset = seriesScale(item.dataKey)
|
|
28
|
+
const isTransparent =
|
|
29
|
+
config.legend.behavior === 'highlight' &&
|
|
30
|
+
seriesHighlight.length > 0 &&
|
|
31
|
+
seriesHighlight.indexOf(item.dataKey) === -1
|
|
32
|
+
const displayPlot =
|
|
33
|
+
config.legend.behavior === 'highlight' ||
|
|
34
|
+
seriesHighlight.length === 0 ||
|
|
35
|
+
seriesHighlight.indexOf(item.dataKey) !== -1
|
|
36
|
+
const fillOpacity = isTransparent ? 0.3 : 0.7
|
|
37
|
+
// outlier & non-outlier arrays
|
|
38
|
+
const nonOut = plot.columnNonOutliers?.[item.dataKey] || []
|
|
39
|
+
const out = plot.columnOutliers?.[item.dataKey] || []
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Group key={`${category}-${item.dataKey}`} top={y0 + offset}>
|
|
43
|
+
{displayPlot && (
|
|
44
|
+
<BoxPlot
|
|
45
|
+
min={Number(plot.min[item.dataKey])}
|
|
46
|
+
max={Number(plot.max[item.dataKey])}
|
|
47
|
+
thirdQuartile={plot.q3[item.dataKey]}
|
|
48
|
+
firstQuartile={plot.q1[item.dataKey]}
|
|
49
|
+
median={plot.median[item.dataKey]}
|
|
50
|
+
horizontal={true}
|
|
51
|
+
valueScale={xScale}
|
|
52
|
+
boxWidth={seriesScale.bandwidth()}
|
|
53
|
+
fill={colorScale(item.dataKey)}
|
|
54
|
+
fillOpacity={1}
|
|
55
|
+
stroke={defaultColor}
|
|
56
|
+
boxProps={{
|
|
57
|
+
fill: colorScale(item.dataKey),
|
|
58
|
+
strokeWidth: config.boxplot.borders === 'true' ? 1.5 : 0,
|
|
59
|
+
stroke: defaultColor,
|
|
60
|
+
fillOpacity: fillOpacity
|
|
61
|
+
}}
|
|
62
|
+
minProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
|
|
63
|
+
maxProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
|
|
64
|
+
medianProps={{ stroke: defaultColor, strokeWidth: 1, opacity: fillOpacity }}
|
|
65
|
+
outliers={
|
|
66
|
+
config.boxplot.plotOutlierValues ? _.map(plot.columnOutliers[item.dataKey], item => item) : []
|
|
67
|
+
}
|
|
68
|
+
outlierProps={{
|
|
69
|
+
style: {
|
|
70
|
+
fill: colorScale(item.dataKey),
|
|
71
|
+
opacity: fillOpacity,
|
|
72
|
+
stroke: defaultColor
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
container
|
|
76
|
+
containerProps={{
|
|
77
|
+
'data-tooltip-html': handleTooltip(
|
|
78
|
+
config.boxplot,
|
|
79
|
+
plot.columnCategory,
|
|
80
|
+
item.dataKey,
|
|
81
|
+
_.round(plot.q1[item.dataKey], config.dataFormat.roundTo),
|
|
82
|
+
_.round(plot.q3[item.dataKey], config.dataFormat.roundTo),
|
|
83
|
+
_.round(plot.median[item.dataKey], config.dataFormat.roundTo),
|
|
84
|
+
_.round(plot.iqr[item.dataKey], config.dataFormat.roundTo),
|
|
85
|
+
config.xAxis.label,
|
|
86
|
+
defaultColor
|
|
87
|
+
),
|
|
88
|
+
'data-tooltip-id': tooltip_id,
|
|
89
|
+
tabIndex: -1
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{/* non-outlier points */}
|
|
95
|
+
{plotNonOutlierValues &&
|
|
96
|
+
nonOut.map((value, idx) => (
|
|
97
|
+
<circle
|
|
98
|
+
display={displayPlot ? 'block' : 'none'}
|
|
99
|
+
key={`non-${category}-${item.dataKey}-${idx}`}
|
|
100
|
+
cx={xScale(value)}
|
|
101
|
+
cy={seriesScale.bandwidth() / 2}
|
|
102
|
+
r={4}
|
|
103
|
+
opacity={fillOpacity}
|
|
104
|
+
fill={defaultColor}
|
|
105
|
+
style={{ stroke: defaultColor }}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
|
|
109
|
+
{/* outlier points */}
|
|
110
|
+
{plotOutlierValues &&
|
|
111
|
+
out.map((value, idx) => (
|
|
112
|
+
<circle
|
|
113
|
+
display={displayPlot ? 'block' : 'none'}
|
|
114
|
+
key={`out-${category}-${item.dataKey}-${idx}`}
|
|
115
|
+
cx={xScale(value)}
|
|
116
|
+
cy={seriesScale.bandwidth() / 2}
|
|
117
|
+
r={4}
|
|
118
|
+
opacity={fillOpacity}
|
|
119
|
+
fill={defaultColor}
|
|
120
|
+
style={{ stroke: defaultColor }}
|
|
121
|
+
/>
|
|
122
|
+
))}
|
|
123
|
+
</Group>
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
})}
|
|
127
|
+
</Group>
|
|
128
|
+
</ErrorBoundary>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
export default BoxPlotHorizontal
|
|
@@ -8,7 +8,7 @@ import { handleTooltip, createPlots } from './helpers/index'
|
|
|
8
8
|
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
9
9
|
import _ from 'lodash'
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const BoxPlotVertical = ({ xScale, yScale, seriesScale }) => {
|
|
12
12
|
const { config, colorScale, seriesHighlight, transformedData: data } = useContext(ConfigContext)
|
|
13
13
|
const { boxplot } = config
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
|
|
|
21
21
|
const color_0 = _.get(colorPalettesChart, [config.palette, 0], '#000')
|
|
22
22
|
const plots = createPlots(data, config)
|
|
23
23
|
return (
|
|
24
|
-
<ErrorBoundary component='BoxPlot'>
|
|
24
|
+
<ErrorBoundary component='BoxPlot Vertical'>
|
|
25
25
|
<Group left={Number(config.yAxis.size)} className='boxplot' key={`boxplot-group`}>
|
|
26
26
|
{plots.map((d, i) => {
|
|
27
27
|
const offset = boxWidth - constrainedWidth
|
|
@@ -41,7 +41,7 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
|
|
|
41
41
|
config.legend.behavior === 'highlight' ||
|
|
42
42
|
seriesHighlight.length === 0 ||
|
|
43
43
|
seriesHighlight.indexOf(item.dataKey) !== -1
|
|
44
|
-
const fillOpacity = isTransparent ? 0.3 : 0.
|
|
44
|
+
const fillOpacity = isTransparent ? 0.3 : 0.7
|
|
45
45
|
return (
|
|
46
46
|
<Group key={`boxplotplot-${item.dataKey}-${index}`}>
|
|
47
47
|
{boxplot.plotNonOutlierValues &&
|
|
@@ -131,4 +131,4 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
|
|
|
131
131
|
)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
export default
|
|
134
|
+
export default BoxPlotVertical
|
|
@@ -33,7 +33,7 @@ export const calculateBoxPlotStats = (values: number[]) => {
|
|
|
33
33
|
if (!values || values.length === 0) return {}
|
|
34
34
|
|
|
35
35
|
// Sort the values
|
|
36
|
-
const sortedValues = _.sortBy(values)
|
|
36
|
+
const sortedValues = _.sortBy(values.map(v => Number(v)))
|
|
37
37
|
|
|
38
38
|
// Quartiles
|
|
39
39
|
const firstQuartile = d3.quantile(sortedValues, 0.25) ?? 0
|
|
@@ -45,18 +45,28 @@ export const calculateBoxPlotStats = (values: number[]) => {
|
|
|
45
45
|
// Outlier Bounds
|
|
46
46
|
const lowerBound = firstQuartile - 1.5 * iqr
|
|
47
47
|
const upperBound = thirdQuartile + 1.5 * iqr
|
|
48
|
+
// const lowerFence = q1 - 1.5 * iqr
|
|
49
|
+
// const upperFence = q3 + 1.5 * iqr
|
|
48
50
|
|
|
49
51
|
// Non-Outlier Values
|
|
50
52
|
const nonOutliers = sortedValues.filter(value => value >= lowerBound && value <= upperBound)
|
|
51
|
-
|
|
53
|
+
// **Outliers** =
|
|
54
|
+
const outliers = sortedValues.filter(v => v < lowerBound || v > upperBound)
|
|
55
|
+
const whiskerMax =
|
|
56
|
+
sortedValues
|
|
57
|
+
.slice()
|
|
58
|
+
.reverse()
|
|
59
|
+
.find(v => v <= upperBound) ?? thirdQuartile
|
|
60
|
+
const whiskerMin = nonOutliers.length > 0 ? nonOutliers[0] : firstQuartile
|
|
52
61
|
// Calculate Box Plot Stats
|
|
53
62
|
return {
|
|
54
|
-
min:
|
|
55
|
-
max:
|
|
56
|
-
median: d3.median(sortedValues),
|
|
63
|
+
min: whiskerMin,
|
|
64
|
+
max: whiskerMax,
|
|
65
|
+
median: d3.median(sortedValues),
|
|
57
66
|
firstQuartile,
|
|
58
67
|
thirdQuartile,
|
|
59
|
-
iqr
|
|
68
|
+
iqr,
|
|
69
|
+
outliers
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
72
|
|
|
@@ -109,16 +119,26 @@ export const createPlots = (data, config) => {
|
|
|
109
119
|
|
|
110
120
|
// Calculate outliers and non-outliers for each series key
|
|
111
121
|
Object.keys(keyValues).forEach(key => {
|
|
112
|
-
const
|
|
122
|
+
const raw = keyValues[key] ?? []
|
|
123
|
+
|
|
124
|
+
// 2) normalize → trim, drop empties/non-numbers, coerce to Number
|
|
125
|
+
const cleaned: number[] = raw
|
|
126
|
+
.map(v => (typeof v === 'string' ? v.trim() : v)) // trim strings
|
|
127
|
+
.filter(v => v != null && v !== '' && !isNaN(+v)) // drop null/''/non-nums
|
|
128
|
+
.map(v => +v)
|
|
129
|
+
|
|
130
|
+
if (cleaned.length === 0) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
113
133
|
|
|
114
134
|
// Calculate box plot statistics
|
|
115
|
-
const { firstQuartile, thirdQuartile, min, max, median, iqr } = calculateBoxPlotStats(
|
|
135
|
+
const { firstQuartile, thirdQuartile, min, max, median, iqr, outliers } = calculateBoxPlotStats(cleaned)
|
|
116
136
|
// Calculate outliers and non-outliers
|
|
117
|
-
columnOutliers[key] = calculateOutliers(
|
|
118
|
-
columnNonOutliers[key] = calculateNonOutliers(
|
|
137
|
+
columnOutliers[key] = calculateOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
|
|
138
|
+
columnNonOutliers[key] = calculateNonOutliers(cleaned, firstQuartile, thirdQuartile).map(Number)
|
|
119
139
|
columnMedian[key] = median
|
|
120
|
-
columnMin[key] = min
|
|
121
|
-
columnMax[key] = max
|
|
140
|
+
columnMin[key] = Number(min)
|
|
141
|
+
columnMax[key] = Number(max)
|
|
122
142
|
columnQ1[key] = firstQuartile
|
|
123
143
|
columnQ3[key] = thirdQuartile
|
|
124
144
|
columnIqr[key] = iqr
|
|
@@ -120,7 +120,7 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
|
120
120
|
svg.call(brushHandle, selection, formattedStartDate, formattedEndDate)
|
|
121
121
|
|
|
122
122
|
const payload = {
|
|
123
|
-
active: config.
|
|
123
|
+
active: config.xAxis.brushActive,
|
|
124
124
|
isBrushing: isUserBrushing,
|
|
125
125
|
data: finalData
|
|
126
126
|
}
|
|
@@ -2931,9 +2931,9 @@ const EditorPanel: React.FC<ChartEditorPanelProps> = ({ datasets }) => {
|
|
|
2931
2931
|
/>
|
|
2932
2932
|
{visHasBrushChart() && (
|
|
2933
2933
|
<CheckBox
|
|
2934
|
-
value={config.
|
|
2935
|
-
section='
|
|
2936
|
-
fieldName='
|
|
2934
|
+
value={config.xAxis.brushActive}
|
|
2935
|
+
section='xAxis'
|
|
2936
|
+
fieldName='brushActive'
|
|
2937
2937
|
label='Brush Slider '
|
|
2938
2938
|
updateField={updateFieldDeprecated}
|
|
2939
2939
|
tooltip={
|
|
@@ -131,7 +131,7 @@ const PanelGeneral: FC<PanelProps> = props => {
|
|
|
131
131
|
options={Object.keys(approvedCurveTypes)}
|
|
132
132
|
/>
|
|
133
133
|
)}
|
|
134
|
-
{visualizationType === 'Bar' && (
|
|
134
|
+
{(visualizationType === 'Bar' || visualizationType === 'Box Plot') && (
|
|
135
135
|
<Select
|
|
136
136
|
value={config.orientation || 'vertical'}
|
|
137
137
|
fieldName='orientation'
|
|
@@ -178,7 +178,7 @@ const PanelGeneral: FC<PanelProps> = props => {
|
|
|
178
178
|
options={['standard', 'shallow', 'finger']}
|
|
179
179
|
/>
|
|
180
180
|
)}
|
|
181
|
-
{visualizationType === 'Bar' && config.orientation === 'horizontal' && (
|
|
181
|
+
{(visualizationType === 'Bar' || visualizationType === 'Box Plot') && config.orientation === 'horizontal' && (
|
|
182
182
|
<Select
|
|
183
183
|
value={config.yAxis.labelPlacement || 'Below Bar'}
|
|
184
184
|
section='yAxis'
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useContext } 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
6
|
import { colorPalettesChart, sequentialPalettes } from '@cdc/core/data/colorPalettes'
|
|
7
|
+
import { getBridgedData } from '../../helpers/getBridgedData'
|
|
7
8
|
|
|
8
9
|
// visx & d3
|
|
9
10
|
import { curveMonotoneX } from '@visx/curve'
|
|
@@ -11,7 +12,7 @@ import { Bar, Area, LinePath } from '@visx/shape'
|
|
|
11
12
|
import { Group } from '@visx/group'
|
|
12
13
|
|
|
13
14
|
const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, handleTooltipMouseOff }) => {
|
|
14
|
-
const { transformedData: data, rawData, config, seriesHighlight } = useContext(ConfigContext)
|
|
15
|
+
const { transformedData: data, rawData, config, seriesHighlight, parseDate } = useContext(ConfigContext)
|
|
15
16
|
const { xAxis, yAxis, legend, runtime } = config
|
|
16
17
|
const DEBUG = false
|
|
17
18
|
|
|
@@ -23,12 +24,17 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
23
24
|
if (!group || !group.stages) return false
|
|
24
25
|
return group.stages.map((stage, stageIndex) => {
|
|
25
26
|
const { behavior } = legend
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let displayArea =
|
|
27
|
+
let transparentArea =
|
|
28
|
+
behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(stage.key) === -1
|
|
29
|
+
let displayArea =
|
|
30
|
+
behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(stage.key) !== -1
|
|
31
|
+
const bridgedData = getBridgedData(stage.key, group.stageColumn, rawData)
|
|
29
32
|
|
|
30
33
|
return (
|
|
31
|
-
<Group
|
|
34
|
+
<Group
|
|
35
|
+
className={`forecasting-areas-combo-${index}`}
|
|
36
|
+
key={`forecasting-areas--stage-${replace(stage.key, / /g, '—')}-${index}`}
|
|
37
|
+
>
|
|
32
38
|
{group.confidenceIntervals?.map((ciGroup, ciGroupIndex) => {
|
|
33
39
|
const palette = sequentialPalettes[stage.color] || colorPalettesChart[stage.color] || false
|
|
34
40
|
|
|
@@ -44,13 +50,19 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
44
50
|
|
|
45
51
|
if (ciGroup.high === '' || ciGroup.low === '') return
|
|
46
52
|
return (
|
|
47
|
-
<Group
|
|
53
|
+
<Group
|
|
54
|
+
key={`forecasting-areas--stage-${replace(
|
|
55
|
+
stage.key,
|
|
56
|
+
/ /g,
|
|
57
|
+
'—'
|
|
58
|
+
)}--group-${stageIndex}-${ciGroupIndex}`}
|
|
59
|
+
>
|
|
48
60
|
{/* prettier-ignore */}
|
|
49
61
|
<Area
|
|
50
62
|
curve={curveMonotoneX}
|
|
51
|
-
data={
|
|
63
|
+
data={bridgedData}
|
|
52
64
|
fill={getFill()}
|
|
53
|
-
opacity={transparentArea
|
|
65
|
+
opacity={transparentArea? 0.1 : 0.5 }
|
|
54
66
|
x={d => xScale(Date.parse(d[xAxis.dataKey]))}
|
|
55
67
|
y0={d => yScale(d[ciGroup.low])}
|
|
56
68
|
y1={d => yScale(d[ciGroup.high])}
|
|
@@ -59,10 +71,10 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
59
71
|
{ciGroupIndex === 0 && (
|
|
60
72
|
<>
|
|
61
73
|
{/* prettier-ignore */}
|
|
62
|
-
<LinePath data={
|
|
74
|
+
<LinePath data={bridgedData} x={d => Number(xScale(Date.parse(d[xAxis.dataKey])))} y={d => Number(yScale(d[ciGroup.high]))} curve={curveMonotoneX} stroke={getStroke()} strokeWidth={1} strokeOpacity={1} />
|
|
63
75
|
|
|
64
76
|
{/* prettier-ignore */}
|
|
65
|
-
<LinePath data={
|
|
77
|
+
<LinePath data={bridgedData} x={d => Number(xScale(Date.parse(d[xAxis.dataKey])))} y={d => Number(yScale(d[ciGroup.low]))} curve={curveMonotoneX} stroke={getStroke()} strokeWidth={1} strokeOpacity={1} />
|
|
66
78
|
</>
|
|
67
79
|
)}
|
|
68
80
|
</Group>
|
|
@@ -73,7 +85,15 @@ const Forecasting = ({ xScale, yScale, height, width, handleTooltipMouseOver, ha
|
|
|
73
85
|
})
|
|
74
86
|
})}
|
|
75
87
|
<Group key='tooltip-hover-section'>
|
|
76
|
-
<Bar
|
|
88
|
+
<Bar
|
|
89
|
+
key={'bars'}
|
|
90
|
+
width={Number(width)}
|
|
91
|
+
height={Number(height)}
|
|
92
|
+
fill={DEBUG ? 'red' : 'transparent'}
|
|
93
|
+
fillOpacity={0.05}
|
|
94
|
+
onMouseMove={e => handleTooltipMouseOver(e, data)}
|
|
95
|
+
onMouseOut={handleTooltipMouseOff}
|
|
96
|
+
/>
|
|
77
97
|
</Group>
|
|
78
98
|
</Group>
|
|
79
99
|
</ErrorBoundary>
|
|
@@ -19,9 +19,9 @@ export const getMarginTop = (isLegendBottom, config) => {
|
|
|
19
19
|
if (!isLegendBottom) {
|
|
20
20
|
return '0px'
|
|
21
21
|
}
|
|
22
|
-
if (isLegendBottom && config.
|
|
22
|
+
if (isLegendBottom && config.xAxis.brushActive && !config.legend.hide) {
|
|
23
23
|
const additiolMargin = 25
|
|
24
|
-
return `${DEFAULT_MARGIN_TOP + config.brush
|
|
24
|
+
return `${DEFAULT_MARGIN_TOP + config.brush?.height + additiolMargin}px`
|
|
25
25
|
} else {
|
|
26
26
|
return `${DEFAULT_MARGIN_TOP}px`
|
|
27
27
|
}
|
|
@@ -14,7 +14,8 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
|
14
14
|
import { AreaChartStacked } from './AreaChart'
|
|
15
15
|
import BarChart from './BarChart'
|
|
16
16
|
import ConfigContext from '../ConfigContext'
|
|
17
|
-
import
|
|
17
|
+
import BoxPlotVertical from './BoxPlot/BoxPlot.Vertical'
|
|
18
|
+
import BoxPlotHorizontal from './BoxPlot/BoxPlot.Horizontal'
|
|
18
19
|
import ScatterPlot from './ScatterPlot'
|
|
19
20
|
import DeviationBar from './DeviationBar'
|
|
20
21
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
@@ -25,17 +26,20 @@ import PairedBarChart from './PairedBarChart'
|
|
|
25
26
|
import useIntersectionObserver from '../hooks/useIntersectionObserver'
|
|
26
27
|
import Regions from './Regions'
|
|
27
28
|
import CategoricalYAxis from './Axis/Categorical.Axis'
|
|
29
|
+
import BrushChart from './Brush/BrushController'
|
|
28
30
|
|
|
29
31
|
// Helpers
|
|
30
32
|
import { isLegendWrapViewport, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
31
33
|
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
32
34
|
import { calcInitialHeight, handleAutoPaddingRight } from '../helpers/sizeHelpers'
|
|
35
|
+
import { filterAndShiftLinearDateTicks } from '../helpers/filterAndShiftLinearDateTicks'
|
|
33
36
|
|
|
34
37
|
// Hooks
|
|
35
38
|
import useMinMax from '../hooks/useMinMax'
|
|
36
39
|
import useReduceData from '../hooks/useReduceData'
|
|
37
40
|
import useRightAxis from '../hooks/useRightAxis'
|
|
38
|
-
import useScales, { getTickValues
|
|
41
|
+
import useScales, { getTickValues } from '../hooks/useScales'
|
|
42
|
+
|
|
39
43
|
import getTopAxis from '../helpers/getTopAxis'
|
|
40
44
|
import { useTooltip as useCoveTooltip } from '../hooks/useTooltip'
|
|
41
45
|
import { useEditorPermissions } from './EditorPanel/useEditorPermissions'
|
|
@@ -43,7 +47,6 @@ import Annotation from './Annotations'
|
|
|
43
47
|
import { BlurStrokeText } from '@cdc/core/components/BlurStrokeText'
|
|
44
48
|
import { countNumOfTicks } from '../helpers/countNumOfTicks'
|
|
45
49
|
import HoverLine from './HoverLine/HoverLine'
|
|
46
|
-
import BrushChart from './BrushChart'
|
|
47
50
|
|
|
48
51
|
type LinearChartProps = {
|
|
49
52
|
parentWidth: number
|
|
@@ -94,7 +97,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
94
97
|
tableData,
|
|
95
98
|
transformedData: data,
|
|
96
99
|
seriesHighlight,
|
|
97
|
-
|
|
100
|
+
|
|
98
101
|
} = useContext(ConfigContext)
|
|
99
102
|
|
|
100
103
|
// CONFIG
|
|
@@ -208,10 +211,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
208
211
|
? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime()
|
|
209
212
|
: d[config.runtime.originalXAxis.dataKey]
|
|
210
213
|
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
211
|
-
const xAxisDataMapped =
|
|
212
|
-
config.brush.active && brushConfig.data?.length
|
|
213
|
-
? brushConfig.data.map(d => getXAxisData(d))
|
|
214
|
-
: data.map(d => getXAxisData(d))
|
|
214
|
+
const xAxisDataMapped = data.map(d => getXAxisData(d))
|
|
215
215
|
const section = config.orientation === 'horizontal' || config.visualizationType === 'Forest Plot' ? 'yAxis' : 'xAxis'
|
|
216
216
|
const properties = {
|
|
217
217
|
data,
|
|
@@ -418,8 +418,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
418
418
|
const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
|
|
419
419
|
|
|
420
420
|
// Heights to add
|
|
421
|
-
|
|
422
|
-
const
|
|
421
|
+
|
|
422
|
+
const brushHeight = 25
|
|
423
|
+
const brushHeightWithMargin = config.xAxis.brushActive ? brushHeight + brushHeight : 0
|
|
423
424
|
const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
|
|
424
425
|
const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
|
|
425
426
|
const additionalHeight = axisBottomHeight + brushHeightWithMargin + forestRowsHeight + topLabelOnGridlineHeight
|
|
@@ -447,7 +448,15 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
447
448
|
const legendIsLeftOrRight =
|
|
448
449
|
legend?.position !== 'top' && legend?.position !== 'bottom' && !isLegendWrapViewport(currentViewport)
|
|
449
450
|
legendRef.current.style.transform = legendIsLeftOrRight ? `translateY(${topLabelOnGridlineHeight}px)` : 'none'
|
|
450
|
-
}, [
|
|
451
|
+
}, [
|
|
452
|
+
axisBottomRef.current,
|
|
453
|
+
config,
|
|
454
|
+
bottomLabelStart,
|
|
455
|
+
config.xAxis.brushActive,
|
|
456
|
+
currentViewport,
|
|
457
|
+
topYLabelRef.current,
|
|
458
|
+
initialHeight
|
|
459
|
+
])
|
|
451
460
|
|
|
452
461
|
useEffect(() => {
|
|
453
462
|
if (lastMaxValue.current === maxValue) return
|
|
@@ -745,8 +754,19 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
745
754
|
showTooltip={showTooltip}
|
|
746
755
|
/>
|
|
747
756
|
)}
|
|
748
|
-
{visualizationType === 'Box Plot' && (
|
|
749
|
-
<
|
|
757
|
+
{visualizationType === 'Box Plot' && config.orientation === 'vertical' && (
|
|
758
|
+
<BoxPlotVertical
|
|
759
|
+
seriesScale={seriesScale}
|
|
760
|
+
xMax={xMax}
|
|
761
|
+
yMax={yMax}
|
|
762
|
+
min={min}
|
|
763
|
+
max={max}
|
|
764
|
+
xScale={xScale}
|
|
765
|
+
yScale={yScale}
|
|
766
|
+
/>
|
|
767
|
+
)}
|
|
768
|
+
{visualizationType === 'Box Plot' && config.orientation === 'horizontal' && (
|
|
769
|
+
<BoxPlotHorizontal
|
|
750
770
|
seriesScale={seriesScale}
|
|
751
771
|
xMax={xMax}
|
|
752
772
|
yMax={yMax}
|
|
@@ -864,7 +884,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
864
884
|
/>
|
|
865
885
|
)}
|
|
866
886
|
{/*Brush chart */}
|
|
867
|
-
{config.
|
|
887
|
+
{config.xAxis.brushActive && config.xAxis.type !== 'categorical' && <BrushChart xMax={xMax} yMax={yMax} />}
|
|
868
888
|
{/* Line chart */}
|
|
869
889
|
{/* TODO: Make this just line or combo? */}
|
|
870
890
|
{!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
|
|
@@ -1028,13 +1048,22 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1028
1048
|
stroke='#000'
|
|
1029
1049
|
/>
|
|
1030
1050
|
)}
|
|
1031
|
-
{yScale.domain()[0] < 0 && (
|
|
1051
|
+
{orientation === 'vertical' && yScale.domain()[0] < 0 && (
|
|
1052
|
+
// draw from the Left of the chart …
|
|
1032
1053
|
<Line
|
|
1033
1054
|
from={{ x: props.axisFromPoint.x, y: yScale(0) }}
|
|
1034
1055
|
to={{ x: xMax, y: yScale(0) }}
|
|
1035
1056
|
stroke='#333'
|
|
1036
1057
|
/>
|
|
1037
1058
|
)}
|
|
1059
|
+
{orientation === 'horizontal' && xScale.domain()[0] < 0 && (
|
|
1060
|
+
<Line
|
|
1061
|
+
// draw from the top of the char
|
|
1062
|
+
from={{ x: xScale(0), y: 0 }}
|
|
1063
|
+
to={{ x: xScale(0), y: yMax }}
|
|
1064
|
+
stroke='#333'
|
|
1065
|
+
/>
|
|
1066
|
+
)}
|
|
1038
1067
|
{visualizationType === 'Bar' && orientation === 'horizontal' && xScale.domain()[0] < 0 && (
|
|
1039
1068
|
<Line
|
|
1040
1069
|
from={{ x: xScale(0), y: 0 }}
|
|
@@ -1079,6 +1108,25 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1079
1108
|
)}
|
|
1080
1109
|
|
|
1081
1110
|
{orientation === 'horizontal' &&
|
|
1111
|
+
visualizationType === 'Box Plot' &&
|
|
1112
|
+
config.yAxis.labelPlacement === 'On Date/Category Axis' &&
|
|
1113
|
+
!config.yAxis.hideLabel && (
|
|
1114
|
+
<Text
|
|
1115
|
+
x={tick.to.x}
|
|
1116
|
+
y={yScale(tick.value) + yScale.bandwidth() / 2}
|
|
1117
|
+
transform={`rotate(${
|
|
1118
|
+
config.orientation === 'horizontal' ? config.runtime.yAxis.tickRotation || 0 : 0
|
|
1119
|
+
}, ${tick.to.x}, ${tick.to.y})`}
|
|
1120
|
+
verticalAnchor={'middle'}
|
|
1121
|
+
textAnchor={'end'}
|
|
1122
|
+
fontSize={tickLabelFontSize}
|
|
1123
|
+
>
|
|
1124
|
+
{tick.formattedValue}
|
|
1125
|
+
</Text>
|
|
1126
|
+
)}
|
|
1127
|
+
|
|
1128
|
+
{orientation === 'horizontal' &&
|
|
1129
|
+
visualizationType !== 'Box Plot' &&
|
|
1082
1130
|
visualizationSubType !== 'stacked' &&
|
|
1083
1131
|
config.yAxis.labelPlacement === 'On Date/Category Axis' &&
|
|
1084
1132
|
!config.yAxis.hideLabel && (
|