@cdc/chart 4.23.1 → 4.23.2
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 +54532 -696
- package/examples/Barchart_with_negative.json +34 -0
- package/examples/box-plot.json +2 -2
- package/examples/dynamic-legends.json +1 -1
- package/examples/example-bar-chart-nonnumeric.json +36 -0
- package/examples/example-bar-chart.json +33 -0
- package/examples/example-combo-bar-nonnumeric.json +105 -0
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +1 -1
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +2 -2
- package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
- package/examples/line-chart-nonnumeric.json +32 -0
- package/examples/line-chart.json +21 -63
- package/examples/newdata.json +1 -1
- package/examples/planet-combo-example-config.json +143 -20
- package/examples/planet-example-data-nonnumeric.json +56 -0
- package/examples/planet-example-data.json +2 -2
- package/examples/planet-pie-example-config-nonnumeric.json +30 -0
- package/examples/scatterplot-continuous.csv +17 -0
- package/examples/scatterplot.json +136 -0
- package/examples/sparkline-chart-nonnumeric.json +76 -0
- package/examples/stacked-vertical-bar-example-negative.json +154 -0
- package/examples/stacked-vertical-bar-example-nonnumerics.json +154 -0
- package/index.html +74 -0
- package/package.json +29 -23
- package/src/{CdcChart.tsx → CdcChart.jsx} +74 -56
- package/src/components/{BarChart.tsx → BarChart.jsx} +99 -91
- package/src/components/BoxPlot.jsx +88 -0
- package/src/components/{DataTable.tsx → DataTable.jsx} +102 -25
- package/src/components/{EditorPanel.js → EditorPanel.jsx} +228 -14
- package/src/components/{Filters.js → Filters.jsx} +6 -12
- package/src/components/{Legend.js → Legend.jsx} +120 -108
- package/src/components/{LineChart.tsx → LineChart.jsx} +26 -12
- package/src/components/{LinearChart.tsx → LinearChart.jsx} +67 -47
- package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +45 -78
- package/src/components/{PieChart.tsx → PieChart.jsx} +17 -32
- package/src/components/ScatterPlot.jsx +48 -0
- package/src/components/{SparkLine.js → SparkLine.jsx} +49 -18
- package/src/components/{useIntersectionObserver.tsx → useIntersectionObserver.jsx} +1 -1
- package/src/data/initial-state.js +33 -3
- package/src/hooks/{useColorPalette.ts → useColorPalette.js} +10 -28
- package/src/hooks/{useReduceData.ts → useReduceData.js} +25 -14
- package/src/hooks/useRightAxis.js +3 -1
- package/src/index.jsx +16 -0
- package/src/scss/DataTable.scss +22 -0
- package/src/scss/main.scss +30 -10
- package/vite.config.js +4 -0
- package/dist/495.js +0 -3
- package/dist/703.js +0 -1
- package/src/components/BoxPlot.js +0 -92
- package/src/index.html +0 -67
- package/src/index.tsx +0 -18
- /package/src/{context.tsx → ConfigContext.jsx} +0 -0
|
@@ -4,11 +4,15 @@ import { BarGroup, BarStack } from '@visx/shape'
|
|
|
4
4
|
import { Text } from '@visx/text'
|
|
5
5
|
import chroma from 'chroma-js'
|
|
6
6
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
|
-
import
|
|
7
|
+
import ConfigContext from '../ConfigContext'
|
|
8
8
|
import { BarStackHorizontal } from '@visx/shape'
|
|
9
9
|
|
|
10
10
|
export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getXAxisData, getYAxisData, animatedChart, visible }) {
|
|
11
|
-
const { transformedData: data, colorScale, seriesHighlight, config, formatNumber, updateConfig, colorPalettes, formatDate, parseDate } = useContext
|
|
11
|
+
const { transformedData: data, colorScale, seriesHighlight, config, formatNumber, updateConfig, colorPalettes, formatDate, isNumber, cleanData, getTextWidth, parseDate } = useContext(ConfigContext)
|
|
12
|
+
// Just do this once up front otherwise we end up
|
|
13
|
+
// calling clean several times on same set of data (TT)
|
|
14
|
+
const cleanedData = cleanData(data, config.xAxis.dataKey)
|
|
15
|
+
|
|
12
16
|
const { orientation, visualizationSubType } = config
|
|
13
17
|
const isHorizontal = orientation === 'horizontal'
|
|
14
18
|
|
|
@@ -25,15 +29,20 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
25
29
|
const radius = config.roundingStyle === 'standard' ? '8px' : config.roundingStyle === 'shallow' ? '5px' : config.roundingStyle === 'finger' ? '15px' : '0px'
|
|
26
30
|
const stackCount = config.runtime.seriesKeys.length
|
|
27
31
|
const barBorderWidth = 1
|
|
28
|
-
const fontSize = { small:
|
|
32
|
+
const fontSize = { small: 16, medium: 18, large: 20 }
|
|
29
33
|
const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
|
|
30
34
|
|
|
31
|
-
const applyRadius = (index
|
|
35
|
+
const applyRadius = (index, isNegative) => {
|
|
32
36
|
if (index === undefined || index === null || !isRounded) return
|
|
33
37
|
let style = {}
|
|
34
38
|
|
|
35
39
|
if ((isStacked && index + 1 === stackCount) || !isStacked) {
|
|
36
|
-
|
|
40
|
+
if (isNegative) {
|
|
41
|
+
// reverse borderRadius to bottom
|
|
42
|
+
style = isHorizontal ? { borderRadius: `0 ${radius} ${radius} 0` } : { borderRadius: `0 0 ${radius} ${radius}` }
|
|
43
|
+
} else {
|
|
44
|
+
style = isHorizontal ? { borderRadius: `0 ${radius} ${radius} 0` } : { borderRadius: `${radius} ${radius} 0 0` }
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
if (tipRounding === 'full' && isStacked && index === 0 && stackCount > 1) {
|
|
39
48
|
style = isHorizontal ? { borderRadius: `${radius} 0 0 ${radius}` } : { borderRadius: `0 0 ${radius} ${radius}` }
|
|
@@ -41,7 +50,6 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
41
50
|
if (tipRounding === 'full' && ((isStacked && index === 0 && stackCount === 1) || !isStacked)) {
|
|
42
51
|
style = { borderRadius: radius }
|
|
43
52
|
}
|
|
44
|
-
|
|
45
53
|
return style
|
|
46
54
|
}
|
|
47
55
|
// }
|
|
@@ -84,16 +92,6 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
84
92
|
})
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
function getTextWidth(text, font) {
|
|
88
|
-
// function calculates the width of given text and its font-size
|
|
89
|
-
const canvas = document.createElement('canvas')
|
|
90
|
-
const context = canvas.getContext('2d')
|
|
91
|
-
|
|
92
|
-
context.font = font || getComputedStyle(document.body).font
|
|
93
|
-
|
|
94
|
-
return Math.ceil(context.measureText(text).width)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
95
|
// Using State
|
|
98
96
|
const [textWidth, setTextWidth] = useState(null)
|
|
99
97
|
|
|
@@ -135,10 +133,10 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
135
133
|
|
|
136
134
|
return (
|
|
137
135
|
<ErrorBoundary component='BarChart'>
|
|
138
|
-
<Group left={config.runtime.yAxis.size}>
|
|
136
|
+
<Group left={parseFloat(config.runtime.yAxis.size)}>
|
|
139
137
|
{/* Stacked Vertical */}
|
|
140
138
|
{config.visualizationSubType === 'stacked' && !isHorizontal && (
|
|
141
|
-
<BarStack data={
|
|
139
|
+
<BarStack data={cleanedData} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} x={d => d[config.runtime.xAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale}>
|
|
142
140
|
{barStacks =>
|
|
143
141
|
barStacks.reverse().map(barStack =>
|
|
144
142
|
barStack.bars.map(bar => {
|
|
@@ -147,10 +145,10 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
147
145
|
let barThickness = xMax / barStack.bars.length
|
|
148
146
|
let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
|
|
149
147
|
let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
|
|
150
|
-
const style = applyRadius(barStack.index)
|
|
151
148
|
// tooltips
|
|
152
149
|
const xAxisValue = config.runtime.xAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.xAxis.dataKey])) : data[bar.index][config.runtime.xAxis.dataKey]
|
|
153
150
|
const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0)
|
|
151
|
+
const style = applyRadius(barStack.index, yAxisValue < 0)
|
|
154
152
|
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
155
153
|
const xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
|
|
156
154
|
if (!hasMultipleSeries) {
|
|
@@ -162,14 +160,13 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
162
160
|
${yAxisTooltip}<br />
|
|
163
161
|
${xAxisTooltip}
|
|
164
162
|
</div>`
|
|
165
|
-
|
|
166
163
|
return (
|
|
167
164
|
<>
|
|
168
165
|
<style>
|
|
169
166
|
{`
|
|
170
167
|
#barStack${barStack.index}-${bar.index} rect,
|
|
171
168
|
#barStack${barStack.index}-${bar.index} foreignObject{
|
|
172
|
-
animation-delay: ${barStack.index}
|
|
169
|
+
animation-delay: ${barStack.index * 0.5}s;
|
|
173
170
|
transform-origin: ${barThicknessAdjusted / 2}px ${bar.y + bar.height}px
|
|
174
171
|
}
|
|
175
172
|
`}
|
|
@@ -187,8 +184,8 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
187
184
|
style={{ background: bar.color, border: `${config.barHasBorder === 'true' ? barBorderWidth : 0}px solid #333`, ...style }}
|
|
188
185
|
opacity={transparentBar ? 0.5 : 1}
|
|
189
186
|
display={displayBar ? 'block' : 'none'}
|
|
190
|
-
data-
|
|
191
|
-
data-
|
|
187
|
+
data-tooltip-html={tooltip}
|
|
188
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
192
189
|
></foreignObject>
|
|
193
190
|
</Group>
|
|
194
191
|
</>
|
|
@@ -202,18 +199,18 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
202
199
|
{/* Stacked Horizontal */}
|
|
203
200
|
{config.visualizationSubType === 'stacked' && isHorizontal && (
|
|
204
201
|
<>
|
|
205
|
-
<BarStackHorizontal data={
|
|
202
|
+
<BarStackHorizontal data={cleanedData} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} height={yMax} y={d => d[config.runtime.yAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale} offset='none'>
|
|
206
203
|
{barStacks =>
|
|
207
204
|
barStacks.map(barStack =>
|
|
208
205
|
updateBars(barStack.bars).map((bar, index) => {
|
|
209
206
|
let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
|
|
210
207
|
let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
|
|
211
208
|
config.barHeight = Number(config.barHeight)
|
|
212
|
-
const style = applyRadius(barStack.index)
|
|
213
209
|
let labelColor = '#000000'
|
|
214
210
|
// tooltips
|
|
215
211
|
const xAxisValue = formatNumber(data[bar.index][bar.key])
|
|
216
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
|
+
const style = applyRadius(barStack.index, yAxisValue < 0)
|
|
217
214
|
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
218
215
|
let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
|
|
219
216
|
if (!hasMultipleSeries) {
|
|
@@ -230,53 +227,64 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
230
227
|
}
|
|
231
228
|
|
|
232
229
|
return (
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
textAnchor='start'
|
|
254
|
-
verticalAnchor='start'
|
|
255
|
-
>
|
|
256
|
-
{yAxisValue}
|
|
257
|
-
</Text>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{displayNumbersOnBar && textWidth + 50 < bar.width && (
|
|
261
|
-
<Text
|
|
230
|
+
<>
|
|
231
|
+
<style>
|
|
232
|
+
{`
|
|
233
|
+
#barStack${barStack.index}-${bar.index} rect,
|
|
234
|
+
#barStack${barStack.index}-${bar.index} foreignObject{
|
|
235
|
+
animation-delay: ${barStack.index * 0.5}s;
|
|
236
|
+
transform-origin: ${bar.x}px
|
|
237
|
+
}
|
|
238
|
+
`}
|
|
239
|
+
</style>
|
|
240
|
+
<Group key={index} id={`barStack${barStack.index}-${bar.index}`} className='stack horizontal'>
|
|
241
|
+
<foreignObject
|
|
242
|
+
key={`barstack-horizontal-${barStack.index}-${bar.index}-${index}`}
|
|
243
|
+
className={`animated-chart group ${animatedChart ? 'animated' : ''}`}
|
|
244
|
+
x={bar.x}
|
|
245
|
+
y={bar.y}
|
|
246
|
+
width={bar.width}
|
|
247
|
+
height={bar.height}
|
|
248
|
+
style={{ background: bar.color, border: `${config.barHasBorder === 'true' ? barBorderWidth : 0}px solid #333`, ...style }}
|
|
249
|
+
opacity={transparentBar ? 0.5 : 1}
|
|
262
250
|
display={displayBar ? 'block' : 'none'}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
251
|
+
data-tooltip-html={tooltip}
|
|
252
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
253
|
+
></foreignObject>
|
|
254
|
+
|
|
255
|
+
{orientation === 'horizontal' && visualizationSubType === 'stacked' && isLabelBelowBar && barStack.index === 0 && !config.yAxis.hideLabel && (
|
|
256
|
+
<Text
|
|
257
|
+
x={`${bar.x + (config.isLollipopChart ? 15 : 5)}`} // padding
|
|
258
|
+
y={bar.y + bar.height * 1.2}
|
|
259
|
+
fill={'#000000'}
|
|
260
|
+
textAnchor='start'
|
|
261
|
+
verticalAnchor='start'
|
|
262
|
+
>
|
|
263
|
+
{yAxisValue}
|
|
264
|
+
</Text>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{displayNumbersOnBar && textWidth + 50 < bar.width && (
|
|
268
|
+
<Text
|
|
269
|
+
display={displayBar ? 'block' : 'none'}
|
|
270
|
+
x={bar.x + barStack.bars[bar.index].width / 2} // padding
|
|
271
|
+
y={bar.y + bar.height / 2}
|
|
272
|
+
fill={labelColor}
|
|
273
|
+
textAnchor='middle'
|
|
274
|
+
verticalAnchor='middle'
|
|
275
|
+
innerRef={e => {
|
|
276
|
+
if (e) {
|
|
277
|
+
// use font sizes and padding to set the bar height
|
|
278
|
+
let elem = e.getBBox()
|
|
279
|
+
setTextWidth(elem.width)
|
|
280
|
+
}
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{formatNumber(data[bar.index][bar.key])}
|
|
284
|
+
</Text>
|
|
285
|
+
)}
|
|
286
|
+
</Group>
|
|
287
|
+
</>
|
|
280
288
|
)
|
|
281
289
|
})
|
|
282
290
|
)
|
|
@@ -289,10 +297,10 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
289
297
|
{config.visualizationSubType !== 'stacked' && (
|
|
290
298
|
<Group>
|
|
291
299
|
<BarGroup
|
|
292
|
-
data={
|
|
300
|
+
data={cleanedData}
|
|
293
301
|
keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
|
|
294
302
|
height={yMax}
|
|
295
|
-
x0={
|
|
303
|
+
x0={d => d[config.runtime.originalXAxis.dataKey]}
|
|
296
304
|
x0Scale={config.runtime.horizontal ? yScale : xScale}
|
|
297
305
|
x1Scale={seriesScale}
|
|
298
306
|
yScale={config.runtime.horizontal ? xScale : yScale}
|
|
@@ -311,8 +319,8 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
311
319
|
{barGroup.bars.map((bar, index) => {
|
|
312
320
|
let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
|
|
313
321
|
let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
|
|
314
|
-
let barHeight = orientation === 'horizontal' ? config.barHeight : Math.abs(yScale(bar.value) - yScale(0))
|
|
315
|
-
let barY = bar.value >= 0 ? bar.y : yScale(0)
|
|
322
|
+
let barHeight = orientation === 'horizontal' ? config.barHeight : isNumber(Math.abs(yScale(bar.value) - yScale(0))) ? Math.abs(yScale(bar.value) - yScale(0)) : 0
|
|
323
|
+
let barY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(0)
|
|
316
324
|
let barGroupWidth = ((config.runtime.horizontal ? yMax : xMax) / barGroups.length) * (config.barThickness || 0.8)
|
|
317
325
|
let offset = (((config.runtime.horizontal ? yMax : xMax) / barGroups.length) * (1 - (config.barThickness || 0.8))) / 2
|
|
318
326
|
|
|
@@ -342,18 +350,18 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
342
350
|
xAxisValue = tempValue
|
|
343
351
|
barWidth = config.barHeight
|
|
344
352
|
}
|
|
353
|
+
// check if bar text/value string fits into each bars.
|
|
354
|
+
let textWidth = getTextWidth(xAxisValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
|
|
355
|
+
let textFits = textWidth < bar.y - 5 // minus padding 5
|
|
356
|
+
|
|
345
357
|
let labelColor = '#000000'
|
|
346
358
|
|
|
347
359
|
// Set label color
|
|
348
360
|
if (chroma.contrast(labelColor, barColor) < 4.9) {
|
|
349
|
-
labelColor = '#FFFFFF'
|
|
361
|
+
if (textFits) labelColor = '#FFFFFF'
|
|
350
362
|
}
|
|
351
363
|
|
|
352
|
-
const style = applyRadius(index)
|
|
353
|
-
|
|
354
|
-
// check if bar text/value string fits into each bars.
|
|
355
|
-
let textWidth = getTextWidth(xAxisValue, config.fontSize)
|
|
356
|
-
let doesTextFit = (textWidth / bar.y) * 100 < 48
|
|
364
|
+
const style = applyRadius(index, yAxisValue < 0)
|
|
357
365
|
|
|
358
366
|
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
|
|
359
367
|
let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
|
|
@@ -396,18 +404,18 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
396
404
|
}}
|
|
397
405
|
opacity={transparentBar ? 0.5 : 1}
|
|
398
406
|
display={displayBar ? 'block' : 'none'}
|
|
399
|
-
data-
|
|
400
|
-
data-
|
|
407
|
+
data-tooltip-html={tooltip}
|
|
408
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
401
409
|
></foreignObject>
|
|
402
410
|
{orientation === 'horizontal' && !config.isLollipopChart && displayNumbersOnBar && (
|
|
403
|
-
<Text
|
|
411
|
+
<Text // prettier-ignore
|
|
404
412
|
display={displayBar ? 'block' : 'none'}
|
|
405
413
|
x={bar.y}
|
|
406
414
|
y={config.barHeight / 2 + config.barHeight * bar.index}
|
|
407
415
|
fill={labelColor}
|
|
408
|
-
dx={
|
|
416
|
+
dx={textFits ? -5 : 5}
|
|
409
417
|
verticalAnchor='middle'
|
|
410
|
-
textAnchor={
|
|
418
|
+
textAnchor={textFits ? 'end' : 'start'}
|
|
411
419
|
>
|
|
412
420
|
{xAxisValue}
|
|
413
421
|
</Text>
|
|
@@ -438,7 +446,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
438
446
|
;
|
|
439
447
|
{orientation === 'vertical' && (
|
|
440
448
|
<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'>
|
|
441
|
-
{bar.value}
|
|
449
|
+
{formatNumber(bar.value)}
|
|
442
450
|
</Text>
|
|
443
451
|
)}
|
|
444
452
|
;
|
|
@@ -449,8 +457,8 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
449
457
|
r={lollipopShapeSize / 2}
|
|
450
458
|
fill={barColor}
|
|
451
459
|
key={`circle--${bar.index}`}
|
|
452
|
-
data-
|
|
453
|
-
data-
|
|
460
|
+
data-tooltip-html={tooltip}
|
|
461
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
454
462
|
style={{ filter: 'unset', opacity: 1 }}
|
|
455
463
|
/>
|
|
456
464
|
)}
|
|
@@ -462,8 +470,8 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
|
|
|
462
470
|
height={lollipopShapeSize}
|
|
463
471
|
fill={barColor}
|
|
464
472
|
key={`circle--${bar.index}`}
|
|
465
|
-
data-
|
|
466
|
-
data-
|
|
473
|
+
data-tooltip-html={tooltip}
|
|
474
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
467
475
|
style={{ opacity: 1, filter: 'unset' }}
|
|
468
476
|
>
|
|
469
477
|
<animate attributeName='height' values={`0, ${lollipopShapeSize}`} dur='2.5s' />
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import { BoxPlot } from '@visx/stats'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
import { scaleBand, scaleLinear } from '@visx/scale'
|
|
5
|
+
import ConfigContext from '../ConfigContext'
|
|
6
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
|
+
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
8
|
+
|
|
9
|
+
const CoveBoxPlot = ({ xScale, yScale }) => {
|
|
10
|
+
const { transformedData: data, config } = useContext(ConfigContext)
|
|
11
|
+
|
|
12
|
+
const boxWidth = xScale.bandwidth()
|
|
13
|
+
const constrainedWidth = Math.min(40, boxWidth)
|
|
14
|
+
const color_0 = colorPalettesChart[config?.palette][0] ? colorPalettesChart[config?.palette][0] : '#000'
|
|
15
|
+
|
|
16
|
+
// tooltips
|
|
17
|
+
const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
|
|
18
|
+
const handleTooltip = d => {
|
|
19
|
+
return `
|
|
20
|
+
<strong>${d.columnCategory}</strong></br>
|
|
21
|
+
${config.boxplot.labels.q1}: ${d.columnFirstQuartile}<br/>
|
|
22
|
+
${config.boxplot.labels.q3}: ${d.columnThirdQuartile}<br/>
|
|
23
|
+
${config.boxplot.labels.iqr}: ${d.columnIqr}<br/>
|
|
24
|
+
${config.boxplot.labels.median}: ${d.columnMedian}
|
|
25
|
+
`
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
<ErrorBoundary component='BoxPlot'>
|
|
29
|
+
<Group className='boxplot' key={`boxplot-group`}>
|
|
30
|
+
{config.boxplot.plots.map((d, i) => {
|
|
31
|
+
const offset = boxWidth - constrainedWidth
|
|
32
|
+
const radius = 4
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
{config.boxplot.plotNonOutlierValues && d.nonOutlierValues.map(value => <circle cx={xScale(d.columnCategory) + config.yAxis.size + boxWidth / 2} cy={yScale(value)} r={radius} fill={'#ccc'} style={{ opacity: 1, fillOpacity: 1, stroke: 'black' }} />)}
|
|
36
|
+
<BoxPlot
|
|
37
|
+
data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
|
|
38
|
+
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}
|
|
45
|
+
boxWidth={constrainedWidth}
|
|
46
|
+
fill={color_0}
|
|
47
|
+
fillOpacity={0.5}
|
|
48
|
+
stroke='black'
|
|
49
|
+
valueScale={yScale}
|
|
50
|
+
outliers={config.boxplot.plotOutlierValues ? d.columnOutliers : []}
|
|
51
|
+
outlierProps={{
|
|
52
|
+
style: {
|
|
53
|
+
fill: `${color_0}`,
|
|
54
|
+
opacity: 1
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
medianProps={{
|
|
58
|
+
style: {
|
|
59
|
+
stroke: 'black'
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
boxProps={{
|
|
63
|
+
style: {
|
|
64
|
+
stroke: 'black',
|
|
65
|
+
strokeWidth: config.boxplot.borders === 'true' ? 1 : 0
|
|
66
|
+
},
|
|
67
|
+
'data-tooltip-html': 'cool'
|
|
68
|
+
}}
|
|
69
|
+
maxProps={{
|
|
70
|
+
style: {
|
|
71
|
+
stroke: 'black'
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
container
|
|
75
|
+
containerProps={{
|
|
76
|
+
'data-tooltip-html': handleTooltip(d),
|
|
77
|
+
'data-tooltip-id': tooltip_id
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</>
|
|
81
|
+
)
|
|
82
|
+
})}
|
|
83
|
+
</Group>
|
|
84
|
+
</ErrorBoundary>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default CoveBoxPlot
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
import React, { useContext, useEffect, useState, useMemo
|
|
1
|
+
import React, { useContext, useEffect, useState, useMemo } from 'react'
|
|
2
2
|
import { useTable, useSortBy, useResizeColumns, useBlockLayout } from 'react-table'
|
|
3
3
|
import Papa from 'papaparse'
|
|
4
4
|
import { Base64 } from 'js-base64'
|
|
5
5
|
|
|
6
6
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
7
7
|
import LegendCircle from '@cdc/core/components/LegendCircle'
|
|
8
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
8
9
|
|
|
9
|
-
import
|
|
10
|
+
import ConfigContext from '../ConfigContext'
|
|
10
11
|
|
|
11
|
-
import CoveMediaControls from '@cdc/core/
|
|
12
|
+
import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
|
|
12
13
|
|
|
13
14
|
export default function DataTable() {
|
|
14
|
-
const { rawData, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, imageId } = useContext
|
|
15
|
+
const { rawData, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, imageId } = useContext(ConfigContext)
|
|
15
16
|
|
|
16
17
|
// Debugging.
|
|
17
18
|
// if (config.visualizationType === 'Box Plot') return null
|
|
18
19
|
|
|
19
20
|
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
20
|
-
const [tableExpanded, setTableExpanded] = useState
|
|
21
|
+
const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
|
|
21
22
|
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
22
23
|
|
|
23
|
-
const DownloadButton = ({ data }
|
|
24
|
+
const DownloadButton = ({ data }, type) => {
|
|
24
25
|
const fileName = `${config.title.substring(0, 50)}.csv`
|
|
25
26
|
|
|
26
27
|
const csvData = Papa.unparse(data)
|
|
@@ -55,52 +56,127 @@ export default function DataTable() {
|
|
|
55
56
|
const newTableColumns =
|
|
56
57
|
config.visualizationType === 'Pie'
|
|
57
58
|
? []
|
|
58
|
-
:
|
|
59
|
+
: config.visualizationType === 'Box Plot'
|
|
60
|
+
? [
|
|
61
|
+
{
|
|
62
|
+
Header: 'Measures',
|
|
63
|
+
Cell: props => {
|
|
64
|
+
const resolveName = () => {
|
|
65
|
+
let {
|
|
66
|
+
boxplot: { labels }
|
|
67
|
+
} = config
|
|
68
|
+
const columnLookup = {
|
|
69
|
+
columnMean: labels.mean,
|
|
70
|
+
columnMax: labels.maximum,
|
|
71
|
+
columnMin: labels.minimum,
|
|
72
|
+
columnIqr: labels.iqr,
|
|
73
|
+
columnCategory: 'Category',
|
|
74
|
+
columnMedian: labels.median,
|
|
75
|
+
columnFirstQuartile: labels.q1,
|
|
76
|
+
columnThirdQuartile: labels.q3,
|
|
77
|
+
columnOutliers: labels.outliers,
|
|
78
|
+
values: labels.values,
|
|
79
|
+
columnCount: labels.count,
|
|
80
|
+
columnSd: 'Standard Deviation',
|
|
81
|
+
nonOutlierValues: 'Non Outliers'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let resolvedName = columnLookup[props.row.original[0]]
|
|
85
|
+
|
|
86
|
+
return resolvedName
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return resolveName()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
: [
|
|
59
94
|
{
|
|
60
95
|
Header: '',
|
|
61
96
|
Cell: ({ row }) => {
|
|
62
97
|
const seriesLabel = config.runtime.seriesLabels ? config.runtime.seriesLabels[row.original] : row.original
|
|
63
98
|
return (
|
|
64
|
-
|
|
99
|
+
<>
|
|
65
100
|
{config.visualizationType !== 'Pie' && (
|
|
66
101
|
<LegendCircle
|
|
67
102
|
fill={
|
|
68
|
-
// non
|
|
103
|
+
// non-dynamic leged
|
|
69
104
|
!config.legend.dynamicLegend
|
|
70
105
|
? colorScale(seriesLabel)
|
|
71
106
|
: // dynamic legend
|
|
72
107
|
config.legend.dynamicLegend
|
|
73
|
-
|
|
74
|
-
|
|
108
|
+
? colorPalettes[config.palette][row.index]
|
|
109
|
+
: // fallback
|
|
75
110
|
'#000'
|
|
76
111
|
}
|
|
77
112
|
/>
|
|
78
113
|
)}
|
|
79
114
|
<span>{seriesLabel}</span>
|
|
80
|
-
|
|
115
|
+
</>
|
|
81
116
|
)
|
|
82
117
|
},
|
|
83
118
|
id: 'series-label'
|
|
84
119
|
}
|
|
85
120
|
]
|
|
121
|
+
if (config.visualizationType !== 'Box Plot') {
|
|
122
|
+
data.forEach((d, index) => {
|
|
123
|
+
const resolveTableHeader = () => {
|
|
124
|
+
if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
|
|
125
|
+
return d[config.runtime.originalXAxis.dataKey]
|
|
126
|
+
}
|
|
127
|
+
const newCol = {
|
|
128
|
+
Header: resolveTableHeader(),
|
|
129
|
+
Cell: ({ row }) => {
|
|
130
|
+
return <>{numberFormatter(d[row.original])}</>
|
|
131
|
+
},
|
|
132
|
+
id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
|
|
133
|
+
canSort: true
|
|
134
|
+
}
|
|
86
135
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Cell: ({ row }) => {
|
|
91
|
-
return <>{numberFormatter(d[row.original])}</>
|
|
92
|
-
},
|
|
93
|
-
id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
|
|
94
|
-
canSort: true
|
|
95
|
-
}
|
|
136
|
+
newTableColumns.push(newCol)
|
|
137
|
+
})
|
|
138
|
+
}
|
|
96
139
|
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
if (config.visualizationType === 'Box Plot') {
|
|
141
|
+
config.boxplot.tableData.map((plot, index) => {
|
|
142
|
+
const newCol = {
|
|
143
|
+
Header: plot.columnCategory,
|
|
144
|
+
Cell: props => {
|
|
145
|
+
let resolveCell = () => {
|
|
146
|
+
if (Number(props.row.id) === 0) return true
|
|
147
|
+
if (Number(props.row.id) === 1) return plot.columnMax
|
|
148
|
+
if (Number(props.row.id) === 2) return plot.columnThirdQuartile
|
|
149
|
+
if (Number(props.row.id) === 3) return plot.columnMedian
|
|
150
|
+
if (Number(props.row.id) === 4) return plot.columnFirstQuartile
|
|
151
|
+
if (Number(props.row.id) === 5) return plot.columnMin
|
|
152
|
+
if (Number(props.row.id) === 6) return plot.columnCount
|
|
153
|
+
if (Number(props.row.id) === 7) return plot.columnSd
|
|
154
|
+
if (Number(props.row.id) === 8) return plot.columnMean
|
|
155
|
+
if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
|
|
156
|
+
if (Number(props.row.id) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
|
|
157
|
+
return <p>-</p>
|
|
158
|
+
}
|
|
159
|
+
return resolveCell()
|
|
160
|
+
},
|
|
161
|
+
id: `${index}`,
|
|
162
|
+
canSort: false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
newTableColumns.push(newCol)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
99
168
|
|
|
100
169
|
return newTableColumns
|
|
101
170
|
}, [config, colorScale])
|
|
102
171
|
|
|
103
|
-
|
|
172
|
+
// prettier-ignore
|
|
173
|
+
const tableData = useMemo(() => (
|
|
174
|
+
config.visualizationType === 'Pie'
|
|
175
|
+
? [config.yAxis.dataKey]
|
|
176
|
+
: config.visualizationType === 'Box Plot'
|
|
177
|
+
? Object.entries(config.boxplot.tableData[0])
|
|
178
|
+
: config.runtime.seriesKeys),
|
|
179
|
+
[config.runtime.seriesKeys])
|
|
104
180
|
|
|
105
181
|
// Change accessibility label depending on expanded status
|
|
106
182
|
useEffect(() => {
|
|
@@ -148,6 +224,7 @@ export default function DataTable() {
|
|
|
148
224
|
}
|
|
149
225
|
}}
|
|
150
226
|
>
|
|
227
|
+
<Icon display={tableExpanded ? 'minus' : 'plus'} base/>
|
|
151
228
|
{config.table.label}
|
|
152
229
|
</div>
|
|
153
230
|
<div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
|
|
@@ -182,7 +259,7 @@ export default function DataTable() {
|
|
|
182
259
|
{rows.map((row, index) => {
|
|
183
260
|
prepareRow(row)
|
|
184
261
|
return (
|
|
185
|
-
<tr {...row.getRowProps()} key={`tbody__tr-${index}`}>
|
|
262
|
+
<tr {...row.getRowProps()} key={`tbody__tr-${index}`} className={`row-${String(config.visualizationType).replace(' ', '-')}--${index}`}>
|
|
186
263
|
{row.cells.map((cell, index) => {
|
|
187
264
|
return (
|
|
188
265
|
<td tabIndex='0' {...cell.getCellProps()} key={`tbody__tr__td-${index}`} role='gridcell'>
|