@cdc/chart 4.25.6 → 4.25.8
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 +53500 -32825
- package/package.json +3 -2
- package/src/CdcChart.tsx +9 -2
- package/src/CdcChartComponent.tsx +30 -12
- 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 +45 -7
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Vertical.tsx +36 -4
- 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/Brush/BrushChart.tsx +65 -10
- package/src/components/Brush/BrushController.tsx +71 -0
- package/src/components/Brush/types.tsx +8 -0
- package/src/components/BrushChart.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +19 -14
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -34
- package/src/components/Forecasting/{Forecasting.jsx → Forecasting.tsx} +32 -12
- package/src/components/Legend/Legend.Component.tsx +16 -1
- package/src/components/Legend/Legend.tsx +3 -1
- package/src/components/Legend/LegendGroup/LegendGroup.tsx +1 -0
- package/src/components/Legend/helpers/index.ts +2 -2
- package/src/components/LineChart/components/LineChart.BumpCircle.tsx +27 -26
- package/src/components/LineChart/helpers.ts +7 -7
- package/src/components/LinearChart.tsx +130 -75
- package/src/data/initial-state.js +12 -15
- package/src/helpers/countNumOfTicks.ts +4 -19
- 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/index.jsx +6 -1
- package/src/scss/main.scss +2 -4
- package/src/store/chart.actions.ts +2 -2
- package/src/store/chart.reducer.ts +4 -12
- package/src/types/ChartConfig.ts +1 -6
- package/src/components/BoxPlot/index.tsx +0 -3
- package/src/components/Brush/BrushController..tsx +0 -39
|
@@ -11,7 +11,7 @@ import { Bar, AreaStack } from '@visx/shape'
|
|
|
11
11
|
import { Group } from '@visx/group'
|
|
12
12
|
import { approvedCurveTypes } from '@cdc/core/helpers/lineChartHelpers'
|
|
13
13
|
|
|
14
|
-
const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff
|
|
14
|
+
const AreaChartStacked = ({ xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff }) => {
|
|
15
15
|
// import data from context
|
|
16
16
|
let { transformedData, config, seriesHighlight, colorScale, rawData } = useContext(ConfigContext)
|
|
17
17
|
const data = config.brush?.active && config.brush.data?.length ? config.brush.data : transformedData
|
|
@@ -9,24 +9,20 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
|
9
9
|
import * as allCurves from '@visx/curve'
|
|
10
10
|
import { AreaClosed, LinePath, Bar } from '@visx/shape'
|
|
11
11
|
import { Group } from '@visx/group'
|
|
12
|
-
import { bisector } from 'd3-array'
|
|
13
12
|
|
|
14
13
|
const AreaChart = props => {
|
|
15
|
-
const { xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff
|
|
14
|
+
const { xScale, yScale, yMax, xMax, handleTooltipMouseOver, handleTooltipMouseOff } = props
|
|
16
15
|
// import data from context
|
|
17
16
|
let {
|
|
18
|
-
transformedData,
|
|
17
|
+
transformedData: data,
|
|
19
18
|
config,
|
|
20
19
|
handleLineType,
|
|
21
20
|
parseDate,
|
|
22
|
-
|
|
23
|
-
formatNumber,
|
|
21
|
+
|
|
24
22
|
seriesHighlight,
|
|
25
23
|
colorScale,
|
|
26
|
-
rawData
|
|
27
|
-
brushConfig
|
|
24
|
+
rawData
|
|
28
25
|
} = useContext(ConfigContext)
|
|
29
|
-
const data = config.brush?.active && brushConfig.data?.length ? brushConfig.data : transformedData
|
|
30
26
|
|
|
31
27
|
if (!data) return
|
|
32
28
|
|
|
@@ -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
|
|
|
@@ -113,9 +115,15 @@ export const BarChartHorizontal = () => {
|
|
|
113
115
|
numbericBarHeight = 25
|
|
114
116
|
}
|
|
115
117
|
let barY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(scaleVal)
|
|
116
|
-
|
|
118
|
+
let defaultBarWidth = Math.abs(xScale(bar.value) - xScale(scaleVal))
|
|
117
119
|
const isPositiveBar = bar.value >= 0 && isNumber(bar.value)
|
|
118
120
|
|
|
121
|
+
const MINIMUM_BAR_HEIGHT = 3
|
|
122
|
+
if (isPositiveBar && barGroup.bars.length === 1 && defaultBarWidth < MINIMUM_BAR_HEIGHT) {
|
|
123
|
+
defaultBarWidth = MINIMUM_BAR_HEIGHT
|
|
124
|
+
barY = yScale(0) - MINIMUM_BAR_HEIGHT
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
const barX = bar.value < 0 ? Math.abs(xScale(bar.value)) : xScale(scaleVal)
|
|
120
128
|
const yAxisValue = formatNumber(bar.value, 'left')
|
|
121
129
|
const xAxisValue =
|
|
@@ -164,7 +172,7 @@ export const BarChartHorizontal = () => {
|
|
|
164
172
|
</li></ul>`
|
|
165
173
|
|
|
166
174
|
// configure colors
|
|
167
|
-
let labelColor =
|
|
175
|
+
let labelColor = APP_FONT_COLOR
|
|
168
176
|
labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
|
|
169
177
|
let barColor =
|
|
170
178
|
config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key]
|
|
@@ -182,7 +190,7 @@ export const BarChartHorizontal = () => {
|
|
|
182
190
|
const borderColor = isHighlightedBar
|
|
183
191
|
? highlightedBarColor
|
|
184
192
|
: config.barHasBorder === 'true'
|
|
185
|
-
?
|
|
193
|
+
? APP_FONT_COLOR
|
|
186
194
|
: 'transparent'
|
|
187
195
|
const borderWidth = isHighlightedBar
|
|
188
196
|
? highlightedBar.borderWidth
|
|
@@ -262,6 +270,21 @@ export const BarChartHorizontal = () => {
|
|
|
262
270
|
}
|
|
263
271
|
})}
|
|
264
272
|
|
|
273
|
+
{(absentDataLabel || isSuppressed) && (
|
|
274
|
+
<rect
|
|
275
|
+
x={barX}
|
|
276
|
+
y={0}
|
|
277
|
+
width={yMax}
|
|
278
|
+
height={numbericBarHeight}
|
|
279
|
+
fill='transparent'
|
|
280
|
+
data-tooltip-place='top'
|
|
281
|
+
data-tooltip-offset='{"top":3}'
|
|
282
|
+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
|
|
283
|
+
data-tooltip-html={tooltip}
|
|
284
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
|
|
265
288
|
{config.preliminaryData?.map((pd, index) => {
|
|
266
289
|
// check if user selected column
|
|
267
290
|
const selectedSuppressionColumn = !pd.column || pd.column === bar.key
|
|
@@ -281,7 +304,7 @@ export const BarChartHorizontal = () => {
|
|
|
281
304
|
: pd.symbol === 'Double Asterisk'
|
|
282
305
|
? barHeight
|
|
283
306
|
: barHeight / 1.5
|
|
284
|
-
const fillColor = pd.displayGray ? '#8b8b8a' :
|
|
307
|
+
const fillColor = pd.displayGray ? '#8b8b8a' : APP_FONT_COLOR
|
|
285
308
|
return (
|
|
286
309
|
<Text // prettier-ignore
|
|
287
310
|
key={index}
|
|
@@ -301,7 +324,7 @@ export const BarChartHorizontal = () => {
|
|
|
301
324
|
)
|
|
302
325
|
})}
|
|
303
326
|
|
|
304
|
-
{!config.isLollipopChart && (
|
|
327
|
+
{!config.isLollipopChart && !hasConfidenceInterval && (
|
|
305
328
|
<Text // prettier-ignore
|
|
306
329
|
display={displayBar ? 'block' : 'none'}
|
|
307
330
|
x={bar.y}
|
|
@@ -315,6 +338,21 @@ export const BarChartHorizontal = () => {
|
|
|
315
338
|
{testZeroValue(bar.value) ? '' : barDefaultLabel}
|
|
316
339
|
</Text>
|
|
317
340
|
)}
|
|
341
|
+
|
|
342
|
+
{!config.isLollipopChart && hasConfidenceInterval && (
|
|
343
|
+
<Text // prettier-ignore
|
|
344
|
+
display={displayBar ? 'block' : 'none'}
|
|
345
|
+
x={bar.value < 0 ? bar.y + barWidth : bar.y - barWidth}
|
|
346
|
+
opacity={transparentBar ? 0.5 : 1}
|
|
347
|
+
y={config.barHeight / 2 + config.barHeight * bar.index}
|
|
348
|
+
fill={labelColor}
|
|
349
|
+
dx={-textPadding}
|
|
350
|
+
verticalAnchor='middle'
|
|
351
|
+
textAnchor={bar.value < 0 ? 'end' : 'start'}
|
|
352
|
+
>
|
|
353
|
+
{testZeroValue(bar.value) ? '' : barDefaultLabel}
|
|
354
|
+
</Text>
|
|
355
|
+
)}
|
|
318
356
|
<Text // prettier-ignore
|
|
319
357
|
display={displayBar ? 'block' : 'none'}
|
|
320
358
|
x={bar.y}
|
|
@@ -334,7 +372,7 @@ export const BarChartHorizontal = () => {
|
|
|
334
372
|
display={displayBar ? 'block' : 'none'}
|
|
335
373
|
x={bar.y}
|
|
336
374
|
y={0}
|
|
337
|
-
fill={
|
|
375
|
+
fill={APP_FONT_COLOR}
|
|
338
376
|
dx={textPaddingLollipop}
|
|
339
377
|
textAnchor={textAnchorLollipop}
|
|
340
378
|
verticalAnchor='middle'
|
|
@@ -137,7 +137,7 @@ const BarChartStackedHorizontal = () => {
|
|
|
137
137
|
<Text
|
|
138
138
|
x={`${bar.x + (config.isLollipopChart ? 15 : 5)}`} // padding
|
|
139
139
|
y={bar.y + bar.height * 1.2}
|
|
140
|
-
fill={
|
|
140
|
+
fill={APP_FONT_COLOR}
|
|
141
141
|
textAnchor='start'
|
|
142
142
|
verticalAnchor='start'
|
|
143
143
|
>
|
|
@@ -16,6 +16,7 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
|
|
|
16
16
|
import isNumber from '@cdc/core/helpers/isNumber'
|
|
17
17
|
import createBarElement from '@cdc/core/components/createBarElement'
|
|
18
18
|
import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
|
|
19
|
+
import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
|
|
19
20
|
// Third party libraries
|
|
20
21
|
import chroma from 'chroma-js'
|
|
21
22
|
// Types
|
|
@@ -45,6 +46,7 @@ export const BarChartVertical = () => {
|
|
|
45
46
|
const {
|
|
46
47
|
colorScale,
|
|
47
48
|
config,
|
|
49
|
+
currentViewport,
|
|
48
50
|
dashboardConfig,
|
|
49
51
|
tableData,
|
|
50
52
|
formatDate,
|
|
@@ -123,8 +125,20 @@ export const BarChartVertical = () => {
|
|
|
123
125
|
seriesHighlight.indexOf(bar.key) !== -1
|
|
124
126
|
|
|
125
127
|
let barGroupWidth = seriesScale.range()[1] - seriesScale.range()[0]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
let defaultBarHeight = Math.abs(yScale(bar.value) - yScale(scaleVal))
|
|
129
|
+
let defaultBarY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(0)
|
|
130
|
+
|
|
131
|
+
const MINIMUM_BAR_HEIGHT = 3
|
|
132
|
+
if (
|
|
133
|
+
bar.value >= 0 &&
|
|
134
|
+
isNumber(bar.value) &&
|
|
135
|
+
barGroup.bars.length === 1 &&
|
|
136
|
+
defaultBarHeight < MINIMUM_BAR_HEIGHT
|
|
137
|
+
) {
|
|
138
|
+
defaultBarHeight = MINIMUM_BAR_HEIGHT
|
|
139
|
+
defaultBarY = yScale(0) - MINIMUM_BAR_HEIGHT
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
let barWidth = config.isLollipopChart ? lollipopBarWidth : seriesScale.bandwidth()
|
|
129
143
|
let barX =
|
|
130
144
|
bar.x +
|
|
@@ -159,7 +173,7 @@ export const BarChartVertical = () => {
|
|
|
159
173
|
yAxisValue
|
|
160
174
|
})
|
|
161
175
|
// configure colors
|
|
162
|
-
let labelColor =
|
|
176
|
+
let labelColor = APP_FONT_COLOR
|
|
163
177
|
labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor) // Set if background is transparent'
|
|
164
178
|
const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
|
|
165
179
|
const isTwoToneLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'two-tone'
|
|
@@ -254,6 +268,8 @@ export const BarChartVertical = () => {
|
|
|
254
268
|
|
|
255
269
|
const BAR_LABEL_PADDING = 10
|
|
256
270
|
|
|
271
|
+
const LABEL_FONT_SIZE = isMobileFontViewport(currentViewport) ? 13 : 16
|
|
272
|
+
|
|
257
273
|
return (
|
|
258
274
|
<Group display={hideGroup} key={`${barGroup.index}--${index}`}>
|
|
259
275
|
<Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
|
|
@@ -288,6 +304,21 @@ export const BarChartVertical = () => {
|
|
|
288
304
|
}
|
|
289
305
|
})}
|
|
290
306
|
|
|
307
|
+
{(absentDataLabel || isSuppressed) && (
|
|
308
|
+
<rect
|
|
309
|
+
x={barX}
|
|
310
|
+
y={0}
|
|
311
|
+
width={barWidth}
|
|
312
|
+
height={yMax}
|
|
313
|
+
fill='transparent'
|
|
314
|
+
data-tooltip-place='top'
|
|
315
|
+
data-tooltip-offset='{"top":3}'
|
|
316
|
+
style={{ pointerEvents: 'all', cursor: 'pointer' }}
|
|
317
|
+
data-tooltip-html={tooltip}
|
|
318
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
321
|
+
|
|
291
322
|
{config.preliminaryData.map((pd, index) => {
|
|
292
323
|
// check if user selected column
|
|
293
324
|
const selectedSuppressionColumn = !pd.column || pd.column === bar.key
|
|
@@ -338,6 +369,7 @@ export const BarChartVertical = () => {
|
|
|
338
369
|
y={barY - BAR_LABEL_PADDING}
|
|
339
370
|
fill={labelColor}
|
|
340
371
|
textAnchor='middle'
|
|
372
|
+
fontSize={LABEL_FONT_SIZE}
|
|
341
373
|
>
|
|
342
374
|
{testZeroValue(bar.value) ? '' : barDefaultLabel}
|
|
343
375
|
</Text>
|
|
@@ -348,7 +380,7 @@ export const BarChartVertical = () => {
|
|
|
348
380
|
y={barY - BAR_LABEL_PADDING}
|
|
349
381
|
fill={labelColor}
|
|
350
382
|
textAnchor='middle'
|
|
351
|
-
fontSize={config.isLollipopChart ? null :
|
|
383
|
+
fontSize={config.isLollipopChart ? null : LABEL_FONT_SIZE}
|
|
352
384
|
>
|
|
353
385
|
{absentDataLabel}
|
|
354
386
|
</Text>
|
|
@@ -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
|
|
@@ -6,28 +6,36 @@ import ConfigContext from '../../ConfigContext'
|
|
|
6
6
|
import { Text } from '@visx/text'
|
|
7
7
|
import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
|
|
8
8
|
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
9
|
-
|
|
10
|
-
export interface ZoomBrushProps {
|
|
9
|
+
export interface BrushChartProps {
|
|
11
10
|
xMax: number
|
|
12
11
|
yMax: number
|
|
13
12
|
brushPosition: { start: { x: number }; end: { x: number } }
|
|
14
13
|
onBrushChange: (bounds: any) => void
|
|
15
14
|
brushKey: number
|
|
15
|
+
brushHandleProps: { startValue: string; endValue: string; endPos: number; startPos: number }
|
|
16
|
+
brushRef: React.RefObject<BrushRef>
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const BrushChart: FC<BrushChartProps> = ({
|
|
20
|
+
xMax,
|
|
21
|
+
yMax,
|
|
22
|
+
brushPosition,
|
|
23
|
+
onBrushChange,
|
|
24
|
+
brushKey,
|
|
25
|
+
brushHandleProps,
|
|
26
|
+
brushRef
|
|
27
|
+
}) => {
|
|
28
|
+
const { tableData, config, dashboardConfig } = useContext(ConfigContext)
|
|
21
29
|
const dataKey = config.xAxis.dataKey
|
|
22
30
|
const borderRadius = 15
|
|
23
31
|
const mappedDates: string[] = tableData.map(row => row[dataKey])
|
|
24
32
|
const brushheight = 25
|
|
25
33
|
const DASHBOARD_MARGIN = 50
|
|
26
34
|
const BRUSH_HEIGHT_MULTIPLIER = 1.5
|
|
27
|
-
|
|
35
|
+
const range = config?.xAxis?.sortByRecentDate ? [xMax, 0] : [0, xMax]
|
|
28
36
|
const xScale = scaleBand<string>({
|
|
29
|
-
domain: mappedDates,
|
|
30
|
-
range:
|
|
37
|
+
domain: config?.xAxis?.sortByRecentDate ? mappedDates.reverse() : mappedDates,
|
|
38
|
+
range: range,
|
|
31
39
|
paddingInner: 0.1,
|
|
32
40
|
paddingOuter: 0.1
|
|
33
41
|
})
|
|
@@ -52,6 +60,16 @@ const ZoomBrush: FC<ZoomBrushProps> = ({ xMax, yMax, brushPosition, onBrushChang
|
|
|
52
60
|
<Group left={config.yAxis.size} top={calculateGroupTop()}>
|
|
53
61
|
<rect fill='#949494' width={xMax} height={25} rx={borderRadius} pointerEvents='none' />
|
|
54
62
|
<Brush
|
|
63
|
+
disableDraggingOverlay={false}
|
|
64
|
+
renderBrushHandle={props => (
|
|
65
|
+
<BrushHandle
|
|
66
|
+
left={Number(config.runtime.yAxis.size)}
|
|
67
|
+
pixelDistance={brushHandleProps.endPos - brushHandleProps.startPos}
|
|
68
|
+
textProps={brushHandleProps}
|
|
69
|
+
isBrushing={brushRef.current?.state.isBrushing}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
55
73
|
innerRef={brushRef}
|
|
56
74
|
key={brushKey}
|
|
57
75
|
xScale={xScale}
|
|
@@ -64,10 +82,47 @@ const ZoomBrush: FC<ZoomBrushProps> = ({ xMax, yMax, brushPosition, onBrushChang
|
|
|
64
82
|
initialBrushPosition={brushPosition}
|
|
65
83
|
selectedBoxStyle={style}
|
|
66
84
|
onChange={onBrushChange}
|
|
67
|
-
|
|
85
|
+
useWindowMoveEvents={true}
|
|
68
86
|
/>
|
|
69
87
|
</Group>
|
|
70
88
|
)
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
export default
|
|
91
|
+
export default BrushChart
|
|
92
|
+
|
|
93
|
+
const BrushHandle = props => {
|
|
94
|
+
const { x, y, isBrushing, className, textProps } = props
|
|
95
|
+
const pathWidth = 8
|
|
96
|
+
|
|
97
|
+
// Flip the SVG path horizontally for the left handle
|
|
98
|
+
const isLeft = className.includes('left')
|
|
99
|
+
const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
|
|
100
|
+
const textAnchor = isLeft ? 'end' : 'start'
|
|
101
|
+
const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
|
|
102
|
+
const textFontSize = APP_FONT_SIZE / 1.4
|
|
103
|
+
const textWidth = getTextWidth(textProps.startValue, `${textFontSize}px`)
|
|
104
|
+
const textPosLeft = x > 0 ? 0 : 55
|
|
105
|
+
const textPosRight = y < textProps.xMax ? 0 : -50
|
|
106
|
+
return (
|
|
107
|
+
<Group left={x + pathWidth / 2} top={-2}>
|
|
108
|
+
<Text
|
|
109
|
+
pointerEvents='visiblePainted'
|
|
110
|
+
dominantBaseline='hanging'
|
|
111
|
+
x={isLeft ? textPosLeft : textPosRight}
|
|
112
|
+
y={25}
|
|
113
|
+
verticalAnchor='start'
|
|
114
|
+
textAnchor={textAnchor}
|
|
115
|
+
fontSize={textFontSize}
|
|
116
|
+
>
|
|
117
|
+
{isLeft ? textProps.startValue : textProps.endValue}
|
|
118
|
+
</Text>
|
|
119
|
+
<path
|
|
120
|
+
cursor='ew-resize'
|
|
121
|
+
d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12'
|
|
122
|
+
fill='#297EF1'
|
|
123
|
+
strokeWidth='1'
|
|
124
|
+
transform={transform}
|
|
125
|
+
/>
|
|
126
|
+
</Group>
|
|
127
|
+
)
|
|
128
|
+
}
|