@cdc/chart 4.23.2 → 4.23.3
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 +41198 -39447
- package/examples/area-chart.json +187 -0
- package/examples/big-small-test-bar.json +328 -0
- package/examples/big-small-test-line.json +328 -0
- package/examples/big-small-test-negative.json +328 -0
- package/examples/box-plot.json +0 -1
- package/examples/example-bar-chart.json +4 -1
- package/examples/example-sparkline.json +76 -0
- package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
- package/examples/gallery/line/line.json +1 -0
- package/examples/horizontal-chart-max-increase.json +38 -0
- package/examples/line-chart-max-increase.json +32 -0
- package/examples/line-chart-nonnumeric.json +5 -5
- package/examples/line-chart.json +6 -6
- package/examples/planet-deviation-config.json +168 -0
- package/examples/planet-deviation-data.json +38 -0
- package/examples/planet-example-config.json +139 -20
- package/examples/planet-example-data-max-increase.json +56 -0
- package/examples/planet-example-data.json +10 -10
- package/examples/scatterplot-continuous.csv +3 -3
- package/examples/scatterplot.json +2 -2
- package/examples/sparkline-chart-nonnumeric.json +3 -3
- package/index.html +26 -9
- package/package.json +6 -3
- package/src/CdcChart.jsx +146 -92
- package/src/components/AreaChart.jsx +198 -0
- package/src/components/BarChart.jsx +58 -34
- package/src/components/BoxPlot.jsx +28 -15
- package/src/components/DataTable.jsx +21 -17
- package/src/components/DeviationBar.jsx +191 -0
- package/src/components/EditorPanel.jsx +473 -168
- package/src/components/Filters.jsx +3 -2
- package/src/components/Legend.jsx +59 -46
- package/src/components/LineChart.jsx +3 -21
- package/src/components/LinearChart.jsx +158 -55
- package/src/components/PairedBarChart.jsx +0 -1
- package/src/components/PieChart.jsx +11 -14
- package/src/components/ScatterPlot.jsx +19 -16
- package/src/components/SparkLine.jsx +87 -85
- package/src/components/useIntersectionObserver.jsx +1 -1
- package/src/data/initial-state.js +20 -4
- package/src/hooks/useColorPalette.js +58 -48
- package/src/hooks/useReduceData.js +3 -4
- package/src/index.jsx +1 -1
- package/src/scss/editor-panel.scss +5 -0
- package/src/test/CdcChart.test.jsx +6 -0
|
@@ -84,7 +84,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
84
84
|
|
|
85
85
|
// return new updated bars/groupes
|
|
86
86
|
return barsArr.map((bar, i) => {
|
|
87
|
-
// set bars Y
|
|
87
|
+
// set bars Y dynamically to handle space between bars
|
|
88
88
|
let y = 0
|
|
89
89
|
bar.index !== 0 && (y = (barHeight + barSpace + labelHeight) * i)
|
|
90
90
|
|
|
@@ -105,13 +105,13 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
105
105
|
}
|
|
106
106
|
})
|
|
107
107
|
}
|
|
108
|
-
}, [config, updateConfig])
|
|
108
|
+
}, [config, updateConfig]) // eslint-disable-line
|
|
109
109
|
|
|
110
110
|
useEffect(() => {
|
|
111
111
|
if (config.isLollipopChart === false && config.barHeight < 25) {
|
|
112
112
|
updateConfig({ ...config, barHeight: 25 })
|
|
113
113
|
}
|
|
114
|
-
}, [config.isLollipopChart])
|
|
114
|
+
}, [config.isLollipopChart]) // eslint-disable-line
|
|
115
115
|
|
|
116
116
|
useEffect(() => {
|
|
117
117
|
if (config.visualizationSubType === 'horizontal') {
|
|
@@ -120,7 +120,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
120
120
|
orientation: 'horizontal'
|
|
121
121
|
})
|
|
122
122
|
}
|
|
123
|
-
}, [])
|
|
123
|
+
}, []) // eslint-disable-line
|
|
124
124
|
|
|
125
125
|
useEffect(() => {
|
|
126
126
|
if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
|
|
@@ -129,7 +129,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
129
129
|
if (isRounded || config.barStyle === 'flat') {
|
|
130
130
|
updateConfig({ ...config, isLollipopChart: false })
|
|
131
131
|
}
|
|
132
|
-
}, [config.barStyle])
|
|
132
|
+
}, [config.barStyle]) // eslint-disable-line
|
|
133
133
|
|
|
134
134
|
return (
|
|
135
135
|
<ErrorBoundary component='BarChart'>
|
|
@@ -147,7 +147,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
147
147
|
let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
|
|
148
148
|
// tooltips
|
|
149
149
|
const xAxisValue = config.runtime.xAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.xAxis.dataKey])) : data[bar.index][config.runtime.xAxis.dataKey]
|
|
150
|
-
const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0)
|
|
150
|
+
const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0, 'left')
|
|
151
151
|
const style = applyRadius(barStack.index, yAxisValue < 0)
|
|
152
152
|
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
153
153
|
const xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
|
|
@@ -161,7 +161,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
161
161
|
${xAxisTooltip}
|
|
162
162
|
</div>`
|
|
163
163
|
return (
|
|
164
|
-
|
|
164
|
+
<Group key={`${barStack.index}--${bar.index}--${orientation}`}>
|
|
165
165
|
<style>
|
|
166
166
|
{`
|
|
167
167
|
#barStack${barStack.index}-${bar.index} rect,
|
|
@@ -173,7 +173,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
173
173
|
</style>
|
|
174
174
|
<Group key={`bar-stack-${barStack.index}-${bar.index}`} id={`barStack${barStack.index}-${bar.index}`} className='stack vertical'>
|
|
175
175
|
<Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barThickness * bar.index + offset} y={bar.y - 5} fill={bar.color} textAnchor='middle'>
|
|
176
|
-
{
|
|
176
|
+
{yAxisValue}
|
|
177
177
|
</Text>
|
|
178
178
|
<foreignObject
|
|
179
179
|
key={`bar-stack-${barStack.index}-${bar.index}`}
|
|
@@ -188,7 +188,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
188
188
|
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
189
189
|
></foreignObject>
|
|
190
190
|
</Group>
|
|
191
|
-
|
|
191
|
+
</Group>
|
|
192
192
|
)
|
|
193
193
|
})
|
|
194
194
|
)
|
|
@@ -208,7 +208,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
208
208
|
config.barHeight = Number(config.barHeight)
|
|
209
209
|
let labelColor = '#000000'
|
|
210
210
|
// tooltips
|
|
211
|
-
const xAxisValue = formatNumber(data[bar.index][bar.key])
|
|
211
|
+
const xAxisValue = formatNumber(data[bar.index][bar.key], 'left')
|
|
212
212
|
const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.originalXAxis.dataKey])) : data[bar.index][config.runtime.originalXAxis.dataKey]
|
|
213
213
|
const style = applyRadius(barStack.index, yAxisValue < 0)
|
|
214
214
|
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
@@ -264,7 +264,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
264
264
|
</Text>
|
|
265
265
|
)}
|
|
266
266
|
|
|
267
|
-
{displayNumbersOnBar && textWidth
|
|
267
|
+
{displayNumbersOnBar && textWidth < bar.width && (
|
|
268
268
|
<Text
|
|
269
269
|
display={displayBar ? 'block' : 'none'}
|
|
270
270
|
x={bar.x + barStack.bars[bar.index].width / 2} // padding
|
|
@@ -280,7 +280,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
280
280
|
}
|
|
281
281
|
}}
|
|
282
282
|
>
|
|
283
|
-
{
|
|
283
|
+
{xAxisValue}
|
|
284
284
|
</Text>
|
|
285
285
|
)}
|
|
286
286
|
</Group>
|
|
@@ -313,6 +313,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
313
313
|
<Group
|
|
314
314
|
className={`bar-group-${barGroup.index}-${barGroup.x0}--${index} ${config.orientation}`}
|
|
315
315
|
key={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
|
|
316
|
+
id={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
|
|
316
317
|
top={config.runtime.horizontal ? barGroup.y : 0}
|
|
317
318
|
left={config.runtime.horizontal ? 0 : (xMax / barGroups.length) * barGroup.index}
|
|
318
319
|
>
|
|
@@ -341,7 +342,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
341
342
|
}
|
|
342
343
|
if (config.legend.colorCode && config.series.length === 1) barColor = palette[barGroup.index]
|
|
343
344
|
|
|
344
|
-
let yAxisValue = formatNumber(bar.value)
|
|
345
|
+
let yAxisValue = formatNumber(bar.value, 'left')
|
|
345
346
|
let xAxisValue = config.runtime[section].type === 'date' ? formatDate(parseDate(data[barGroup.index][config.runtime.originalXAxis.dataKey])) : data[barGroup.index][config.runtime.originalXAxis.dataKey]
|
|
346
347
|
|
|
347
348
|
if (config.runtime.horizontal) {
|
|
@@ -379,7 +380,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
379
380
|
</div>`
|
|
380
381
|
|
|
381
382
|
return (
|
|
382
|
-
|
|
383
|
+
<Group key={`${barGroup.index}--${index}--${orientation}`}>
|
|
383
384
|
{/* This feels gross but inline transition was not working well*/}
|
|
384
385
|
<style>
|
|
385
386
|
{`
|
|
@@ -446,7 +447,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
446
447
|
;
|
|
447
448
|
{orientation === 'vertical' && (
|
|
448
449
|
<Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barWidth * (bar.index + 0.5) + offset} y={barY - 5} fill={barColor} textAnchor='middle'>
|
|
449
|
-
{
|
|
450
|
+
{yAxisValue}
|
|
450
451
|
</Text>
|
|
451
452
|
)}
|
|
452
453
|
;
|
|
@@ -478,7 +479,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
478
479
|
</rect>
|
|
479
480
|
)}
|
|
480
481
|
</Group>
|
|
481
|
-
|
|
482
|
+
</Group>
|
|
482
483
|
)
|
|
483
484
|
})}
|
|
484
485
|
</Group>
|
|
@@ -488,25 +489,48 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
488
489
|
|
|
489
490
|
{Object.keys(config.confidenceKeys).length > 0
|
|
490
491
|
? data.map(d => {
|
|
491
|
-
let xPos
|
|
492
|
-
let upperPos
|
|
493
|
-
let lowerPos
|
|
492
|
+
let xPos, yPos
|
|
493
|
+
let upperPos
|
|
494
|
+
let lowerPos
|
|
494
495
|
let tickWidth = 5
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
496
|
+
// DEV-3264 Make Confidence Intervals work on horizontal bar charts
|
|
497
|
+
if (orientation === 'horizontal') {
|
|
498
|
+
yPos = yScale(getXAxisData(d)) - 0.75 * config.barHeight
|
|
499
|
+
upperPos = xScale(getYAxisData(d, config.confidenceKeys.upper))
|
|
500
|
+
lowerPos = xScale(getYAxisData(d, config.confidenceKeys.lower))
|
|
501
|
+
return (
|
|
502
|
+
<path
|
|
503
|
+
key={`confidence-interval-h-${yPos}-${d[config.runtime.originalXAxis.dataKey]}`}
|
|
504
|
+
stroke='#333'
|
|
505
|
+
strokeWidth='px'
|
|
506
|
+
d={`
|
|
507
|
+
M${lowerPos} ${yPos - tickWidth}
|
|
508
|
+
L${lowerPos} ${yPos + tickWidth}
|
|
509
|
+
M${lowerPos} ${yPos}
|
|
510
|
+
L${upperPos} ${yPos}
|
|
511
|
+
M${upperPos} ${yPos - tickWidth}
|
|
512
|
+
L${upperPos} ${yPos + tickWidth} `}
|
|
513
|
+
/>
|
|
514
|
+
)
|
|
515
|
+
} else {
|
|
516
|
+
xPos = xScale(getXAxisData(d))
|
|
517
|
+
upperPos = yScale(getYAxisData(d, config.confidenceKeys.lower))
|
|
518
|
+
lowerPos = yScale(getYAxisData(d, config.confidenceKeys.upper))
|
|
519
|
+
return (
|
|
520
|
+
<path
|
|
521
|
+
key={`confidence-interval-v-${yPos}-${d[config.runtime.originalXAxis.dataKey]}`}
|
|
522
|
+
stroke='#333'
|
|
523
|
+
strokeWidth='px'
|
|
524
|
+
d={`
|
|
525
|
+
M${xPos - tickWidth} ${upperPos}
|
|
526
|
+
L${xPos + tickWidth} ${upperPos}
|
|
527
|
+
M${xPos} ${upperPos}
|
|
528
|
+
L${xPos} ${lowerPos}
|
|
529
|
+
M${xPos - tickWidth} ${lowerPos}
|
|
530
|
+
L${xPos + tickWidth} ${lowerPos}`}
|
|
531
|
+
/>
|
|
532
|
+
)
|
|
533
|
+
}
|
|
510
534
|
})
|
|
511
535
|
: ''}
|
|
512
536
|
</Group>
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import React, { useContext } from 'react'
|
|
1
|
+
import React, { useContext, useEffect } from 'react'
|
|
2
2
|
import { BoxPlot } from '@visx/stats'
|
|
3
3
|
import { Group } from '@visx/group'
|
|
4
|
-
import { scaleBand, scaleLinear } from '@visx/scale'
|
|
5
4
|
import ConfigContext from '../ConfigContext'
|
|
6
5
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
6
|
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
8
7
|
|
|
9
8
|
const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
10
|
-
const {
|
|
11
|
-
|
|
9
|
+
const { config, setConfig } = useContext(ConfigContext)
|
|
12
10
|
const boxWidth = xScale.bandwidth()
|
|
13
11
|
const constrainedWidth = Math.min(40, boxWidth)
|
|
14
12
|
const color_0 = colorPalettesChart[config?.palette][0] ? colorPalettesChart[config?.palette][0] : '#000'
|
|
@@ -24,6 +22,19 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
24
22
|
${config.boxplot.labels.median}: ${d.columnMedian}
|
|
25
23
|
`
|
|
26
24
|
}
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (config.legend.hide === false) {
|
|
28
|
+
setConfig({
|
|
29
|
+
...config,
|
|
30
|
+
legend: {
|
|
31
|
+
...config.legend,
|
|
32
|
+
hide: true
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}, []) // eslint-disable-line
|
|
37
|
+
|
|
27
38
|
return (
|
|
28
39
|
<ErrorBoundary component='BoxPlot'>
|
|
29
40
|
<Group className='boxplot' key={`boxplot-group`}>
|
|
@@ -31,17 +42,20 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
31
42
|
const offset = boxWidth - constrainedWidth
|
|
32
43
|
const radius = 4
|
|
33
44
|
return (
|
|
34
|
-
|
|
35
|
-
{config.boxplot.plotNonOutlierValues &&
|
|
45
|
+
<Group key={`boxplotplot-${i}`}>
|
|
46
|
+
{config.boxplot.plotNonOutlierValues &&
|
|
47
|
+
d.nonOutlierValues.map((value, index) => {
|
|
48
|
+
return <circle cx={xScale(d.columnCategory) + Number(config.yAxis.size) + boxWidth / 2} cy={yScale(value)} r={radius} fill={'#ccc'} style={{ opacity: 1, fillOpacity: 1, stroke: 'black' }} key={`boxplot-${i}--circle-${index}`} />
|
|
49
|
+
})}
|
|
36
50
|
<BoxPlot
|
|
37
51
|
data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
|
|
38
52
|
key={`box-plot-${i}`}
|
|
39
|
-
min={d.columnMin}
|
|
40
|
-
max={d.columnMax}
|
|
41
|
-
left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
|
|
42
|
-
firstQuartile={d.columnFirstQuartile}
|
|
43
|
-
thirdQuartile={d.columnThirdQuartile}
|
|
44
|
-
median={d.columnMedian}
|
|
53
|
+
min={Number(d.columnMin)}
|
|
54
|
+
max={Number(d.columnMax)}
|
|
55
|
+
left={Number(xScale(d.columnCategory)) + Number(config.yAxis.size) + offset / 2 + 0.5}
|
|
56
|
+
firstQuartile={Number(d.columnFirstQuartile)}
|
|
57
|
+
thirdQuartile={Number(d.columnThirdQuartile)}
|
|
58
|
+
median={Number(d.columnMedian)}
|
|
45
59
|
boxWidth={constrainedWidth}
|
|
46
60
|
fill={color_0}
|
|
47
61
|
fillOpacity={0.5}
|
|
@@ -63,8 +77,7 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
63
77
|
style: {
|
|
64
78
|
stroke: 'black',
|
|
65
79
|
strokeWidth: config.boxplot.borders === 'true' ? 1 : 0
|
|
66
|
-
}
|
|
67
|
-
'data-tooltip-html': 'cool'
|
|
80
|
+
}
|
|
68
81
|
}}
|
|
69
82
|
maxProps={{
|
|
70
83
|
style: {
|
|
@@ -77,7 +90,7 @@ const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
|
77
90
|
'data-tooltip-id': tooltip_id
|
|
78
91
|
}}
|
|
79
92
|
/>
|
|
80
|
-
|
|
93
|
+
</Group>
|
|
81
94
|
)
|
|
82
95
|
})}
|
|
83
96
|
</Group>
|
|
@@ -12,10 +12,7 @@ import ConfigContext from '../ConfigContext'
|
|
|
12
12
|
import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
|
|
13
13
|
|
|
14
14
|
export default function DataTable() {
|
|
15
|
-
const { rawData, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes
|
|
16
|
-
|
|
17
|
-
// Debugging.
|
|
18
|
-
// if (config.visualizationType === 'Box Plot') return null
|
|
15
|
+
const { rawData, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes } = useContext(ConfigContext)
|
|
19
16
|
|
|
20
17
|
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
21
18
|
const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
|
|
@@ -57,7 +54,7 @@ export default function DataTable() {
|
|
|
57
54
|
config.visualizationType === 'Pie'
|
|
58
55
|
? []
|
|
59
56
|
: config.visualizationType === 'Box Plot'
|
|
60
|
-
|
|
57
|
+
? [
|
|
61
58
|
{
|
|
62
59
|
Header: 'Measures',
|
|
63
60
|
Cell: props => {
|
|
@@ -76,7 +73,7 @@ export default function DataTable() {
|
|
|
76
73
|
columnThirdQuartile: labels.q3,
|
|
77
74
|
columnOutliers: labels.outliers,
|
|
78
75
|
values: labels.values,
|
|
79
|
-
|
|
76
|
+
columnTotal: labels.total,
|
|
80
77
|
columnSd: 'Standard Deviation',
|
|
81
78
|
nonOutlierValues: 'Non Outliers'
|
|
82
79
|
}
|
|
@@ -90,7 +87,7 @@ export default function DataTable() {
|
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
89
|
]
|
|
93
|
-
|
|
90
|
+
: [
|
|
94
91
|
{
|
|
95
92
|
Header: '',
|
|
96
93
|
Cell: ({ row }) => {
|
|
@@ -105,8 +102,8 @@ export default function DataTable() {
|
|
|
105
102
|
? colorScale(seriesLabel)
|
|
106
103
|
: // dynamic legend
|
|
107
104
|
config.legend.dynamicLegend
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
? colorPalettes[config.palette][row.index]
|
|
106
|
+
: // fallback
|
|
110
107
|
'#000'
|
|
111
108
|
}
|
|
112
109
|
/>
|
|
@@ -122,12 +119,13 @@ export default function DataTable() {
|
|
|
122
119
|
data.forEach((d, index) => {
|
|
123
120
|
const resolveTableHeader = () => {
|
|
124
121
|
if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
|
|
122
|
+
if (config.runtime[section].type === 'continuous') return numberFormatter(d[config.runtime.originalXAxis.dataKey], 'bottom')
|
|
125
123
|
return d[config.runtime.originalXAxis.dataKey]
|
|
126
124
|
}
|
|
127
125
|
const newCol = {
|
|
128
126
|
Header: resolveTableHeader(),
|
|
129
127
|
Cell: ({ row }) => {
|
|
130
|
-
return <>{numberFormatter(d[row.original])}</>
|
|
128
|
+
return <>{numberFormatter(d[row.original], 'left')}</>
|
|
131
129
|
},
|
|
132
130
|
id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
|
|
133
131
|
canSort: true
|
|
@@ -149,7 +147,7 @@ export default function DataTable() {
|
|
|
149
147
|
if (Number(props.row.id) === 3) return plot.columnMedian
|
|
150
148
|
if (Number(props.row.id) === 4) return plot.columnFirstQuartile
|
|
151
149
|
if (Number(props.row.id) === 5) return plot.columnMin
|
|
152
|
-
if (Number(props.row.id) === 6) return plot.
|
|
150
|
+
if (Number(props.row.id) === 6) return plot.columnTotal
|
|
153
151
|
if (Number(props.row.id) === 7) return plot.columnSd
|
|
154
152
|
if (Number(props.row.id) === 8) return plot.columnMean
|
|
155
153
|
if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
@@ -162,12 +160,12 @@ export default function DataTable() {
|
|
|
162
160
|
canSort: false
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
newTableColumns.push(newCol)
|
|
163
|
+
return newTableColumns.push(newCol)
|
|
166
164
|
})
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
return newTableColumns
|
|
170
|
-
}, [config, colorScale])
|
|
168
|
+
}, [config, colorScale]) // eslint-disable-line
|
|
171
169
|
|
|
172
170
|
// prettier-ignore
|
|
173
171
|
const tableData = useMemo(() => (
|
|
@@ -176,7 +174,7 @@ export default function DataTable() {
|
|
|
176
174
|
: config.visualizationType === 'Box Plot'
|
|
177
175
|
? Object.entries(config.boxplot.tableData[0])
|
|
178
176
|
: config.runtime.seriesKeys),
|
|
179
|
-
[config.runtime.seriesKeys])
|
|
177
|
+
[config.runtime.seriesKeys]) // eslint-disable-line
|
|
180
178
|
|
|
181
179
|
// Change accessibility label depending on expanded status
|
|
182
180
|
useEffect(() => {
|
|
@@ -201,8 +199,13 @@ export default function DataTable() {
|
|
|
201
199
|
}),
|
|
202
200
|
[]
|
|
203
201
|
)
|
|
204
|
-
|
|
205
202
|
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns: tableColumns, data: tableData, defaultColumn }, useSortBy, useBlockLayout, useResizeColumns)
|
|
203
|
+
|
|
204
|
+
// sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
|
|
205
|
+
if (config.xAxis.type === 'continuous' && headerGroups) {
|
|
206
|
+
data.sort((a, b) => a[config.xAxis.dataKey] - b[config.xAxis.dataKey])
|
|
207
|
+
}
|
|
208
|
+
|
|
206
209
|
return (
|
|
207
210
|
<ErrorBoundary component='DataTable'>
|
|
208
211
|
<CoveMediaControls.Section classes={['download-links']}>
|
|
@@ -224,7 +227,7 @@ export default function DataTable() {
|
|
|
224
227
|
}
|
|
225
228
|
}}
|
|
226
229
|
>
|
|
227
|
-
<Icon display={tableExpanded ? 'minus' : 'plus'} base/>
|
|
230
|
+
<Icon display={tableExpanded ? 'minus' : 'plus'} base />
|
|
228
231
|
{config.table.label}
|
|
229
232
|
</div>
|
|
230
233
|
<div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
|
|
@@ -272,7 +275,7 @@ export default function DataTable() {
|
|
|
272
275
|
})}
|
|
273
276
|
</tbody>
|
|
274
277
|
</table>
|
|
275
|
-
{config.regions && config.regions.length > 0 ? (
|
|
278
|
+
{config.regions && config.regions.length > 0 && !config.visualizationType === 'Box Plot' ? (
|
|
276
279
|
<table className='region-table data-table'>
|
|
277
280
|
<caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
|
|
278
281
|
<thead>
|
|
@@ -284,6 +287,7 @@ export default function DataTable() {
|
|
|
284
287
|
</thead>
|
|
285
288
|
<tbody>
|
|
286
289
|
{config.regions.map((region, index) => {
|
|
290
|
+
if (config.visualizationType === 'Box Plot') return false
|
|
287
291
|
if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
|
|
288
292
|
|
|
289
293
|
return (
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Line } from '@visx/shape'
|
|
2
|
+
import { Group } from '@visx/group'
|
|
3
|
+
import { useContext, useEffect } from 'react'
|
|
4
|
+
import ConfigContext from '../ConfigContext'
|
|
5
|
+
import { Text } from '@visx/text'
|
|
6
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
|
+
import chroma from 'chroma-js'
|
|
8
|
+
|
|
9
|
+
// create function to position text based where bar is located left/or right
|
|
10
|
+
function getTextProps(isLollipopChart, textFits, lollipopShapeSize, fill) {
|
|
11
|
+
if (isLollipopChart) {
|
|
12
|
+
return {
|
|
13
|
+
right: {
|
|
14
|
+
textAnchor: 'start',
|
|
15
|
+
dx: lollipopShapeSize + 6,
|
|
16
|
+
fill: '#000000'
|
|
17
|
+
},
|
|
18
|
+
left: {
|
|
19
|
+
textAnchor: 'end',
|
|
20
|
+
dx: -lollipopShapeSize,
|
|
21
|
+
fill: '#000000'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
return {
|
|
26
|
+
right: {
|
|
27
|
+
textAnchor: textFits ? 'end' : 'start',
|
|
28
|
+
dx: textFits ? -6 : 6,
|
|
29
|
+
fill: textFits ? fill : '#000000'
|
|
30
|
+
},
|
|
31
|
+
left: {
|
|
32
|
+
textAnchor: textFits ? 'start' : 'end',
|
|
33
|
+
dx: textFits ? 6 : -6,
|
|
34
|
+
fill: textFits ? fill : '#000000'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DeviationBar({ height, xScale }) {
|
|
41
|
+
const { transformedData: data, config, formatNumber, twoColorPalette, getTextWidth, updateConfig, parseDate, formatDate } = useContext(ConfigContext)
|
|
42
|
+
|
|
43
|
+
if (!config || config?.series?.length !== 1 || config.orientation !== 'horizontal') return
|
|
44
|
+
|
|
45
|
+
const { barStyle, tipRounding, roundingStyle, twoColor } = config
|
|
46
|
+
|
|
47
|
+
const radius = roundingStyle === 'standard' ? '8px' : roundingStyle === 'shallow' ? '5px' : roundingStyle === 'finger' ? '15px' : '0px'
|
|
48
|
+
const fontSize = { small: 16, medium: 18, large: 20 }
|
|
49
|
+
const isRounded = config.barStyle === 'rounded'
|
|
50
|
+
const target = Number(config.xAxis.target)
|
|
51
|
+
const seriesKey = config.series[0].dataKey
|
|
52
|
+
const maxVal = Number(xScale.domain()[1])
|
|
53
|
+
const hasNegativeValues = data.some(d => d[seriesKey] < 0)
|
|
54
|
+
const shouldShowTargetLine = hasNegativeValues || target > 0 || xScale.domain()[0] < 0
|
|
55
|
+
const borderWidth = config.barHasBorder === 'true' ? 1 : 0
|
|
56
|
+
const lollipopBarHeight = config.lollipopSize === 'large' ? 7 : config.lollipopSize === 'medium' ? 6 : 5
|
|
57
|
+
const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
|
|
58
|
+
|
|
59
|
+
const targetX = Math.max(xScale(0), Math.min(xScale(target), xScale(maxVal * 1.05)))
|
|
60
|
+
|
|
61
|
+
const applyRadius = barPosition => {
|
|
62
|
+
if (barPosition === undefined || barPosition === null || barStyle !== 'rounded') return
|
|
63
|
+
let style = {}
|
|
64
|
+
if (barPosition === 'left') {
|
|
65
|
+
style = { borderRadius: `${radius} 0 0 ${radius}` }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (barPosition === 'right') {
|
|
69
|
+
style = { borderRadius: `0 ${radius} ${radius} 0` }
|
|
70
|
+
}
|
|
71
|
+
if (tipRounding === 'full') {
|
|
72
|
+
style = { borderRadius: radius }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return style
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const targetLabel = {
|
|
79
|
+
calculate: function () {
|
|
80
|
+
const firstBarValue = data[0][seriesKey]
|
|
81
|
+
const barPosition = firstBarValue < target ? 'left' : 'right'
|
|
82
|
+
const label = `${config.xAxis.targetLabel} ${formatNumber(config.xAxis.target || 0, 'left')}`
|
|
83
|
+
const labelWidth = getTextWidth(label, `bold ${fontSize[config.fontSize]}px sans-serif`)
|
|
84
|
+
let labelY = config.isLollipopChart ? lollipopBarHeight / 2 : Number(config.barHeight) / 2
|
|
85
|
+
let paddingX = 0
|
|
86
|
+
let labelX = 0
|
|
87
|
+
let showLabel = false
|
|
88
|
+
|
|
89
|
+
if (barPosition === 'right') {
|
|
90
|
+
paddingX = -10
|
|
91
|
+
showLabel = labelWidth - paddingX < targetX
|
|
92
|
+
labelX = targetX - labelWidth
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (barPosition === 'left') {
|
|
96
|
+
paddingX = 10
|
|
97
|
+
showLabel = xScale(maxVal) - targetX > labelWidth + paddingX
|
|
98
|
+
labelX = targetX
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.text = label
|
|
102
|
+
this.y = labelY
|
|
103
|
+
this.x = labelX
|
|
104
|
+
this.padding = paddingX
|
|
105
|
+
this.showLabel = config.xAxis.showTargetLabel ? showLabel : false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
targetLabel.calculate()
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
|
|
112
|
+
updateConfig({ ...config, isLollipopChart: true })
|
|
113
|
+
}
|
|
114
|
+
if (isRounded || config.barStyle === 'flat') {
|
|
115
|
+
updateConfig({ ...config, isLollipopChart: false })
|
|
116
|
+
}
|
|
117
|
+
}, [config.barStyle])
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<ErrorBoundary component='Deviation Bar'>
|
|
121
|
+
<Group left={Number(config.xAxis.size)}>
|
|
122
|
+
{data.map((d, index) => {
|
|
123
|
+
const barValue = Number(d[seriesKey])
|
|
124
|
+
const barHeight = config.isLollipopChart ? lollipopBarHeight : Number(config.barHeight)
|
|
125
|
+
const barSpace = Number(config.barSpace) || 15
|
|
126
|
+
const barWidth = Math.abs(xScale(barValue) - targetX)
|
|
127
|
+
const barBaseX = xScale(barValue)
|
|
128
|
+
const barX = barValue > target ? targetX : barBaseX
|
|
129
|
+
const barPosition = barValue < target ? 'left' : 'right'
|
|
130
|
+
|
|
131
|
+
// update bar Y to give dynamic Y when user applyes BarSpace
|
|
132
|
+
let barY = 0
|
|
133
|
+
barY = index !== 0 ? (barSpace + barHeight + borderWidth) * index : barY
|
|
134
|
+
const totalheight = (barSpace + barHeight + borderWidth) * data.length
|
|
135
|
+
config.heights.horizontal = totalheight
|
|
136
|
+
|
|
137
|
+
// text,labels postiions
|
|
138
|
+
const textWidth = getTextWidth(formatNumber(barValue, 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
|
|
139
|
+
const textFits = textWidth < barWidth - 6
|
|
140
|
+
const textX = barBaseX
|
|
141
|
+
const textY = barY + barHeight / 2
|
|
142
|
+
|
|
143
|
+
// lollipop shapes
|
|
144
|
+
const circleX = barBaseX
|
|
145
|
+
const circleY = barY + barHeight / 2
|
|
146
|
+
const squareX = barBaseX
|
|
147
|
+
const squareY = barY - barHeight / 2
|
|
148
|
+
const borderRadius = applyRadius(barPosition)
|
|
149
|
+
// colors
|
|
150
|
+
const [leftColor, rightColor] = twoColorPalette[twoColor.palette]
|
|
151
|
+
const barColor = { left: leftColor, right: rightColor }
|
|
152
|
+
const isBarColorDark = chroma.contrast('#000000', barColor[barPosition]) < 4.9
|
|
153
|
+
const fill = isBarColorDark ? '#FFFFFF' : '#000000'
|
|
154
|
+
|
|
155
|
+
let textProps = getTextProps(config.isLollipopChart, textFits, lollipopShapeSize, fill)
|
|
156
|
+
|
|
157
|
+
// tooltips
|
|
158
|
+
const xAxisValue = formatNumber(barValue, 'left')
|
|
159
|
+
const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[index][config.runtime.originalXAxis.dataKey])) : data[index][config.runtime.originalXAxis.dataKey]
|
|
160
|
+
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
161
|
+
let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
|
|
162
|
+
const tooltip = `<div>
|
|
163
|
+
${yAxisTooltip}<br />
|
|
164
|
+
${xAxisTooltip}
|
|
165
|
+
</div>`
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Group key={`deviation-bar-${config.orientation}-${seriesKey}-${index}`}>
|
|
169
|
+
<foreignObject x={barX} y={barY} width={barWidth} height={barHeight} style={{ border: `${borderWidth}px solid #333`, backgroundColor: barColor[barPosition], ...borderRadius }} data-tooltip-html={tooltip} data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} />
|
|
170
|
+
{config.yAxis.displayNumbersOnBar && (
|
|
171
|
+
<Text verticalAnchor='middle' x={textX} y={textY} {...textProps[barPosition]}>
|
|
172
|
+
{formatNumber(d[seriesKey], 'left')}
|
|
173
|
+
</Text>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{config.isLollipopChart && config.lollipopShape === 'circle' && <circle cx={circleX} cy={circleY} r={lollipopShapeSize / 2} fill={barColor[barPosition]} style={{ filter: 'unset', opacity: 1 }} />}
|
|
177
|
+
{config.isLollipopChart && config.lollipopShape === 'square' && <rect x={squareX} y={squareY} width={lollipopShapeSize} height={lollipopShapeSize} fill={barColor[barPosition]} style={{ opacity: 1, filter: 'unset' }}></rect>}
|
|
178
|
+
</Group>
|
|
179
|
+
)
|
|
180
|
+
})}
|
|
181
|
+
{targetLabel.showLabel && (
|
|
182
|
+
<Text fontWeight='bold' dx={targetLabel.padding} verticalAnchor='middle' x={targetLabel.x} y={targetLabel.y}>
|
|
183
|
+
{targetLabel.text}
|
|
184
|
+
</Text>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{shouldShowTargetLine && <Line from={{ x: targetX, y: 0 }} to={{ x: targetX, y: height }} stroke='#333' strokeWidth={2} />}
|
|
188
|
+
</Group>
|
|
189
|
+
</ErrorBoundary>
|
|
190
|
+
)
|
|
191
|
+
}
|