@cdc/chart 4.22.11 → 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.
Files changed (65) hide show
  1. package/dist/cdcchart.js +54569 -16
  2. package/examples/Barchart_with_negative.json +34 -0
  3. package/examples/box-plot-data.json +71 -0
  4. package/examples/box-plot.csv +5 -0
  5. package/examples/box-plot.json +124 -0
  6. package/examples/dynamic-legends.json +1 -1
  7. package/examples/example-bar-chart-nonnumeric.json +36 -0
  8. package/examples/example-bar-chart.json +33 -0
  9. package/examples/example-combo-bar-nonnumeric.json +105 -0
  10. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +3 -1
  11. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +1 -1
  12. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +86 -17
  13. package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
  14. package/examples/line-chart-nonnumeric.json +32 -0
  15. package/examples/line-chart.json +21 -63
  16. package/examples/new-data.csv +17 -0
  17. package/examples/newdata.json +90 -0
  18. package/examples/planet-combo-example-config.json +143 -20
  19. package/examples/planet-example-data-nonnumeric.json +56 -0
  20. package/examples/planet-example-data.json +2 -2
  21. package/examples/planet-pie-example-config-nonnumeric.json +30 -0
  22. package/examples/scatterplot-continuous.csv +17 -0
  23. package/examples/{private/yaxis-test.json → scatterplot.json} +53 -50
  24. package/examples/sparkline-chart-nonnumeric.json +76 -0
  25. package/examples/stacked-vertical-bar-example-negative.json +154 -0
  26. package/examples/stacked-vertical-bar-example-nonnumerics.json +154 -0
  27. package/{src/index.html → index.html} +18 -11
  28. package/package.json +29 -22
  29. package/src/{CdcChart.tsx → CdcChart.jsx} +193 -119
  30. package/src/components/BarChart.jsx +517 -0
  31. package/src/components/BoxPlot.jsx +88 -0
  32. package/src/components/{DataTable.tsx → DataTable.jsx} +125 -32
  33. package/src/components/{EditorPanel.js → EditorPanel.jsx} +376 -115
  34. package/src/components/Filters.jsx +125 -0
  35. package/src/components/Legend.jsx +303 -0
  36. package/src/components/{LineChart.tsx → LineChart.jsx} +87 -22
  37. package/src/components/{LinearChart.tsx → LinearChart.jsx} +172 -113
  38. package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +46 -79
  39. package/src/components/{PieChart.tsx → PieChart.jsx} +29 -34
  40. package/src/components/ScatterPlot.jsx +48 -0
  41. package/src/components/{SparkLine.js → SparkLine.jsx} +49 -18
  42. package/src/components/useIntersectionObserver.jsx +29 -0
  43. package/src/data/initial-state.js +44 -8
  44. package/src/hooks/{useColorPalette.ts → useColorPalette.js} +10 -28
  45. package/src/hooks/{useReduceData.ts → useReduceData.js} +27 -13
  46. package/src/hooks/useRightAxis.js +3 -1
  47. package/src/index.jsx +16 -0
  48. package/src/scss/DataTable.scss +23 -1
  49. package/src/scss/main.scss +83 -32
  50. package/vite.config.js +4 -0
  51. package/examples/private/filters.json +0 -170
  52. package/examples/private/line-test-data.json +0 -22
  53. package/examples/private/line-test-two.json +0 -210
  54. package/examples/private/line-test.json +0 -102
  55. package/examples/private/new.json +0 -48800
  56. package/examples/private/newtest.csv +0 -101
  57. package/examples/private/shawn.json +0 -1106
  58. package/examples/private/test.json +0 -10124
  59. package/examples/private/yaxis-testing.csv +0 -27
  60. package/examples/private/yaxis.json +0 -28
  61. package/src/components/BarChart.tsx +0 -579
  62. package/src/components/Legend.js +0 -284
  63. package/src/components/useIntersectionObserver.tsx +0 -27
  64. package/src/index.tsx +0 -18
  65. /package/src/{context.tsx → ConfigContext.jsx} +0 -0
@@ -0,0 +1,517 @@
1
+ import React, { useContext, useState, useEffect } from 'react'
2
+ import { Group } from '@visx/group'
3
+ import { BarGroup, BarStack } from '@visx/shape'
4
+ import { Text } from '@visx/text'
5
+ import chroma from 'chroma-js'
6
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
7
+ import ConfigContext from '../ConfigContext'
8
+ import { BarStackHorizontal } from '@visx/shape'
9
+
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, 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
+
16
+ const { orientation, visualizationSubType } = config
17
+ const isHorizontal = orientation === 'horizontal'
18
+
19
+ const lollipopBarWidth = config.lollipopSize === 'large' ? 7 : config.lollipopSize === 'medium' ? 6 : 5
20
+ const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
21
+
22
+ const isLabelBelowBar = config.yAxis.labelPlacement === 'Below Bar'
23
+ const displayNumbersOnBar = config.yAxis.displayNumbersOnBar
24
+ const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
25
+
26
+ const isRounded = config.barStyle === 'rounded'
27
+ const isStacked = config.visualizationSubType === 'stacked'
28
+ const tipRounding = config.tipRounding
29
+ const radius = config.roundingStyle === 'standard' ? '8px' : config.roundingStyle === 'shallow' ? '5px' : config.roundingStyle === 'finger' ? '15px' : '0px'
30
+ const stackCount = config.runtime.seriesKeys.length
31
+ const barBorderWidth = 1
32
+ const fontSize = { small: 16, medium: 18, large: 20 }
33
+ const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
34
+
35
+ const applyRadius = (index, isNegative) => {
36
+ if (index === undefined || index === null || !isRounded) return
37
+ let style = {}
38
+
39
+ if ((isStacked && index + 1 === stackCount) || !isStacked) {
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
+ }
46
+ }
47
+ if (tipRounding === 'full' && isStacked && index === 0 && stackCount > 1) {
48
+ style = isHorizontal ? { borderRadius: `${radius} 0 0 ${radius}` } : { borderRadius: `0 0 ${radius} ${radius}` }
49
+ }
50
+ if (tipRounding === 'full' && ((isStacked && index === 0 && stackCount === 1) || !isStacked)) {
51
+ style = { borderRadius: radius }
52
+ }
53
+ return style
54
+ }
55
+ // }
56
+
57
+ const updateBars = defaultBars => {
58
+ // function updates stacked && regular && lollipop horizontal bars
59
+ if (config.visualizationType !== 'Bar' && !isHorizontal) return defaultBars
60
+
61
+ const barsArr = [...defaultBars]
62
+ let barHeight
63
+
64
+ const heights = {
65
+ stacked: config.barHeight,
66
+ lollipop: lollipopBarWidth
67
+ }
68
+
69
+ if (!isStacked) {
70
+ barHeight = heights[config.isLollipopChart ? 'lollipop' : 'stacked'] * stackCount
71
+ } else {
72
+ barHeight = heights.stacked
73
+ }
74
+
75
+ const labelHeight = isLabelBelowBar ? fontSize[config.fontSize] * 1.2 : 0
76
+ let barSpace = Number(config.barSpace)
77
+
78
+ // calculate height of container based height, space and fontSize of labels
79
+ let totalHeight = barsArr.length * (barHeight + labelHeight + barSpace)
80
+
81
+ if (isHorizontal) {
82
+ config.heights.horizontal = totalHeight
83
+ }
84
+
85
+ // return new updated bars/groupes
86
+ return barsArr.map((bar, i) => {
87
+ // set bars Y dynamycly to handle space between bars
88
+ let y = 0
89
+ bar.index !== 0 && (y = (barHeight + barSpace + labelHeight) * i)
90
+
91
+ return { ...bar, y: y, height: barHeight }
92
+ })
93
+ }
94
+
95
+ // Using State
96
+ const [textWidth, setTextWidth] = useState(null)
97
+
98
+ useEffect(() => {
99
+ if (orientation === 'horizontal' && !config.yAxis.labelPlacement) {
100
+ updateConfig({
101
+ ...config,
102
+ yAxis: {
103
+ ...config,
104
+ labelPlacement: 'Below Bar'
105
+ }
106
+ })
107
+ }
108
+ }, [config, updateConfig])
109
+
110
+ useEffect(() => {
111
+ if (config.isLollipopChart === false && config.barHeight < 25) {
112
+ updateConfig({ ...config, barHeight: 25 })
113
+ }
114
+ }, [config.isLollipopChart])
115
+
116
+ useEffect(() => {
117
+ if (config.visualizationSubType === 'horizontal') {
118
+ updateConfig({
119
+ ...config,
120
+ orientation: 'horizontal'
121
+ })
122
+ }
123
+ }, [])
124
+
125
+ useEffect(() => {
126
+ if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
127
+ updateConfig({ ...config, isLollipopChart: true })
128
+ }
129
+ if (isRounded || config.barStyle === 'flat') {
130
+ updateConfig({ ...config, isLollipopChart: false })
131
+ }
132
+ }, [config.barStyle])
133
+
134
+ return (
135
+ <ErrorBoundary component='BarChart'>
136
+ <Group left={parseFloat(config.runtime.yAxis.size)}>
137
+ {/* Stacked Vertical */}
138
+ {config.visualizationSubType === 'stacked' && !isHorizontal && (
139
+ <BarStack data={cleanedData} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} x={d => d[config.runtime.xAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale}>
140
+ {barStacks =>
141
+ barStacks.reverse().map(barStack =>
142
+ barStack.bars.map(bar => {
143
+ let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
144
+ let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
145
+ let barThickness = xMax / barStack.bars.length
146
+ let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
147
+ let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
148
+ // tooltips
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)
151
+ const style = applyRadius(barStack.index, yAxisValue < 0)
152
+ let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
153
+ const xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
154
+ if (!hasMultipleSeries) {
155
+ yAxisTooltip = config.isLegendValue ? `${bar.key}: ${yAxisValue}` : config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
156
+ }
157
+
158
+ const tooltip = `<div>
159
+ ${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && hasMultipleSeries ? `${config.runtime.seriesLabels[bar.key] || ''}<br/>` : ''}
160
+ ${yAxisTooltip}<br />
161
+ ${xAxisTooltip}
162
+ </div>`
163
+ return (
164
+ <>
165
+ <style>
166
+ {`
167
+ #barStack${barStack.index}-${bar.index} rect,
168
+ #barStack${barStack.index}-${bar.index} foreignObject{
169
+ animation-delay: ${barStack.index * 0.5}s;
170
+ transform-origin: ${barThicknessAdjusted / 2}px ${bar.y + bar.height}px
171
+ }
172
+ `}
173
+ </style>
174
+ <Group key={`bar-stack-${barStack.index}-${bar.index}`} id={`barStack${barStack.index}-${bar.index}`} className='stack vertical'>
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
+ {formatNumber(bar.bar ? bar.bar.data[bar.key] : 0)}
177
+ </Text>
178
+ <foreignObject
179
+ key={`bar-stack-${barStack.index}-${bar.index}`}
180
+ x={barThickness * bar.index + offset}
181
+ y={bar.y}
182
+ width={barThicknessAdjusted}
183
+ height={bar.height}
184
+ style={{ background: bar.color, border: `${config.barHasBorder === 'true' ? barBorderWidth : 0}px solid #333`, ...style }}
185
+ opacity={transparentBar ? 0.5 : 1}
186
+ display={displayBar ? 'block' : 'none'}
187
+ data-tooltip-html={tooltip}
188
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
189
+ ></foreignObject>
190
+ </Group>
191
+ </>
192
+ )
193
+ })
194
+ )
195
+ }
196
+ </BarStack>
197
+ )}
198
+
199
+ {/* Stacked Horizontal */}
200
+ {config.visualizationSubType === 'stacked' && isHorizontal && (
201
+ <>
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'>
203
+ {barStacks =>
204
+ barStacks.map(barStack =>
205
+ updateBars(barStack.bars).map((bar, index) => {
206
+ let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
207
+ let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
208
+ config.barHeight = Number(config.barHeight)
209
+ let labelColor = '#000000'
210
+ // tooltips
211
+ const xAxisValue = formatNumber(data[bar.index][bar.key])
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)
214
+ let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
215
+ let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
216
+ if (!hasMultipleSeries) {
217
+ xAxisTooltip = config.isLegendValue ? `${bar.key}: ${xAxisValue}` : config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisTooltip
218
+ }
219
+ const tooltip = `<div>
220
+ ${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && hasMultipleSeries ? `${config.runtime.seriesLabels[bar.key] || ''}<br/>` : ''}
221
+ ${yAxisTooltip}<br />
222
+ ${xAxisTooltip}
223
+ </div>`
224
+
225
+ if (chroma.contrast(labelColor, bar.color) < 4.9) {
226
+ labelColor = '#FFFFFF'
227
+ }
228
+
229
+ return (
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}
250
+ display={displayBar ? 'block' : 'none'}
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
+ </>
288
+ )
289
+ })
290
+ )
291
+ }
292
+ </BarStackHorizontal>
293
+ </>
294
+ )}
295
+
296
+ {/* Bar Groups: Not Stacked */}
297
+ {config.visualizationSubType !== 'stacked' && (
298
+ <Group>
299
+ <BarGroup
300
+ data={cleanedData}
301
+ keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
302
+ height={yMax}
303
+ x0={d => d[config.runtime.originalXAxis.dataKey]}
304
+ x0Scale={config.runtime.horizontal ? yScale : xScale}
305
+ x1Scale={seriesScale}
306
+ yScale={config.runtime.horizontal ? xScale : yScale}
307
+ color={() => {
308
+ return ''
309
+ }}
310
+ >
311
+ {barGroups => {
312
+ return updateBars(barGroups).map((barGroup, index) => (
313
+ <Group
314
+ className={`bar-group-${barGroup.index}-${barGroup.x0}--${index} ${config.orientation}`}
315
+ key={`bar-group-${barGroup.index}-${barGroup.x0}--${index}`}
316
+ top={config.runtime.horizontal ? barGroup.y : 0}
317
+ left={config.runtime.horizontal ? 0 : (xMax / barGroups.length) * barGroup.index}
318
+ >
319
+ {barGroup.bars.map((bar, index) => {
320
+ let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
321
+ let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
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)
324
+ let barGroupWidth = ((config.runtime.horizontal ? yMax : xMax) / barGroups.length) * (config.barThickness || 0.8)
325
+ let offset = (((config.runtime.horizontal ? yMax : xMax) / barGroups.length) * (1 - (config.barThickness || 0.8))) / 2
326
+
327
+ // ! Unsure if this should go back.
328
+ if (config.isLollipopChart) {
329
+ offset = (config.runtime.horizontal ? yMax : xMax) / barGroups.length / 2 - lollipopBarWidth / 2
330
+ }
331
+ const set = new Set()
332
+ data.forEach(d => set.add(d[config.legend.colorCode]))
333
+ const uniqValues = Array.from(set)
334
+
335
+ let palette = colorPalettes[config.palette].slice(0, uniqValues.length)
336
+
337
+ let barWidth = config.isLollipopChart ? lollipopBarWidth : barGroupWidth / barGroup.bars.length
338
+ let barColor = config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key] ? colorScale(config.runtime.seriesLabels[bar.key]) : colorScale(bar.key)
339
+ while (palette.length < barGroups.length) {
340
+ palette = palette.concat(palette)
341
+ }
342
+ if (config.legend.colorCode && config.series.length === 1) barColor = palette[barGroup.index]
343
+
344
+ let yAxisValue = formatNumber(bar.value)
345
+ 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
+ if (config.runtime.horizontal) {
348
+ let tempValue = yAxisValue
349
+ yAxisValue = xAxisValue
350
+ xAxisValue = tempValue
351
+ barWidth = config.barHeight
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
+
357
+ let labelColor = '#000000'
358
+
359
+ // Set label color
360
+ if (chroma.contrast(labelColor, barColor) < 4.9) {
361
+ if (textFits) labelColor = '#FFFFFF'
362
+ }
363
+
364
+ const style = applyRadius(index, yAxisValue < 0)
365
+
366
+ let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
367
+ let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
368
+ if (!hasMultipleSeries && config.runtime.horizontal) {
369
+ xAxisTooltip = config.isLegendValue ? `${bar.key}: ${xAxisValue}` : config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
370
+ }
371
+ if (!hasMultipleSeries && !config.runtime.horizontal) {
372
+ yAxisTooltip = config.isLegendValue ? `${bar.key}: ${yAxisValue}` : config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
373
+ }
374
+
375
+ const tooltip = `<div>
376
+ ${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && hasMultipleSeries ? `${config.runtime.seriesLabels[bar.key] || ''}<br/>` : ''}
377
+ ${yAxisTooltip}<br />
378
+ ${xAxisTooltip}
379
+ </div>`
380
+
381
+ return (
382
+ <>
383
+ {/* This feels gross but inline transition was not working well*/}
384
+ <style>
385
+ {`
386
+ .linear #barGroup${barGroup.index},
387
+ .Combo #barGroup${barGroup.index} {
388
+ transform-origin: 0 ${barY + barHeight}px;
389
+ }
390
+ `}
391
+ </style>
392
+ <Group key={`bar-sub-group-${barGroup.index}-${barGroup.x0}-${barY}--${index}`}>
393
+ <foreignObject
394
+ id={`barGroup${barGroup.index}`}
395
+ key={`bar-group-bar-${barGroup.index}-${bar.index}-${bar.value}-${bar.key}`}
396
+ x={config.runtime.horizontal ? 0 : barWidth * bar.index + offset}
397
+ y={config.runtime.horizontal ? barWidth * bar.index : barY}
398
+ width={config.runtime.horizontal ? bar.y : barWidth}
399
+ height={isHorizontal && !config.isLollipopChart ? barWidth : isHorizontal && config.isLollipopChart ? lollipopBarWidth : barHeight}
400
+ style={{
401
+ background: config.isLollipopChart && config.lollipopColorStyle === 'regular' ? barColor : config.isLollipopChart && config.lollipopColorStyle === 'two-tone' ? chroma(barColor).brighten(1) : barColor,
402
+ border: `${config.isLollipopChart ? 0 : config.barHasBorder === 'true' ? barBorderWidth : 0}px solid #333`,
403
+ ...style
404
+ }}
405
+ opacity={transparentBar ? 0.5 : 1}
406
+ display={displayBar ? 'block' : 'none'}
407
+ data-tooltip-html={tooltip}
408
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
409
+ ></foreignObject>
410
+ {orientation === 'horizontal' && !config.isLollipopChart && displayNumbersOnBar && (
411
+ <Text // prettier-ignore
412
+ display={displayBar ? 'block' : 'none'}
413
+ x={bar.y}
414
+ y={config.barHeight / 2 + config.barHeight * bar.index}
415
+ fill={labelColor}
416
+ dx={textFits ? -5 : 5}
417
+ verticalAnchor='middle'
418
+ textAnchor={textFits ? 'end' : 'start'}
419
+ >
420
+ {xAxisValue}
421
+ </Text>
422
+ )}
423
+ ;
424
+ {orientation === 'horizontal' && config.isLollipopChart && displayNumbersOnBar && (
425
+ <Text
426
+ display={displayBar ? 'block' : 'none'}
427
+ x={`${bar.y + (config.isLollipopChart ? 15 : 5) + (config.isLollipopChart && barGroup.bars.length === bar.index ? offset : 0)}`} // padding
428
+ y={0}
429
+ fill={'#000000'}
430
+ textAnchor='start'
431
+ verticalAnchor='middle'
432
+ fontWeight={'normal'}
433
+ >
434
+ {xAxisValue}
435
+ </Text>
436
+ )}
437
+ {orientation === 'horizontal' && isLabelBelowBar && !config.yAxis.hideLabel && (
438
+ <Text x={config.yAxis.hideAxis ? 0 : 5} y={barGroup.height} dy={4} verticalAnchor={'start'} textAnchor={'start'}>
439
+ {config.runtime.yAxis.type === 'date'
440
+ ? formatDate(parseDate(data[barGroup.index][config.runtime.originalXAxis.dataKey]))
441
+ : isHorizontal
442
+ ? data[barGroup.index][config.runtime.originalXAxis.dataKey]
443
+ : formatNumber(data[barGroup.index][config.runtime.originalXAxis.dataKey])}
444
+ </Text>
445
+ )}
446
+ ;
447
+ {orientation === 'vertical' && (
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'>
449
+ {formatNumber(bar.value)}
450
+ </Text>
451
+ )}
452
+ ;
453
+ {config.isLollipopChart && config.lollipopShape === 'circle' && (
454
+ <circle
455
+ cx={orientation === 'horizontal' ? bar.y : barWidth * (barGroup.bars.length - bar.index - 1) + (isLabelBelowBar && orientation === 'horizontal' ? 0 : offset) + lollipopShapeSize / 3.5}
456
+ cy={orientation === 'horizontal' ? 0 + lollipopBarWidth / 2 : bar.y}
457
+ r={lollipopShapeSize / 2}
458
+ fill={barColor}
459
+ key={`circle--${bar.index}`}
460
+ data-tooltip-html={tooltip}
461
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
462
+ style={{ filter: 'unset', opacity: 1 }}
463
+ />
464
+ )}
465
+ {config.isLollipopChart && config.lollipopShape === 'square' && (
466
+ <rect
467
+ x={orientation === 'horizontal' && bar.y > 10 ? bar.y - lollipopShapeSize / 2 : orientation === 'horizontal' && bar.y < 10 ? 0 : orientation !== 'horizontal' ? offset - lollipopBarWidth / 2 : barWidth * (barGroup.bars.length - bar.index - 1) + offset - 5.25}
468
+ y={orientation === 'horizontal' ? 0 - lollipopBarWidth / 2 : barY}
469
+ width={lollipopShapeSize}
470
+ height={lollipopShapeSize}
471
+ fill={barColor}
472
+ key={`circle--${bar.index}`}
473
+ data-tooltip-html={tooltip}
474
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
475
+ style={{ opacity: 1, filter: 'unset' }}
476
+ >
477
+ <animate attributeName='height' values={`0, ${lollipopShapeSize}`} dur='2.5s' />
478
+ </rect>
479
+ )}
480
+ </Group>
481
+ </>
482
+ )
483
+ })}
484
+ </Group>
485
+ ))
486
+ }}
487
+ </BarGroup>
488
+
489
+ {Object.keys(config.confidenceKeys).length > 0
490
+ ? data.map(d => {
491
+ let xPos = xScale(getXAxisData(d))
492
+ let upperPos = yScale(getYAxisData(d, config.confidenceKeys.lower))
493
+ let lowerPos = yScale(getYAxisData(d, config.confidenceKeys.upper))
494
+ let tickWidth = 5
495
+
496
+ return (
497
+ <path
498
+ key={`confidence-interval-${d[config.runtime.originalXAxis.dataKey]}`}
499
+ stroke='#333'
500
+ strokeWidth='2px'
501
+ d={`
502
+ M${xPos - tickWidth} ${upperPos}
503
+ L${xPos + tickWidth} ${upperPos}
504
+ M${xPos} ${upperPos}
505
+ L${xPos} ${lowerPos}
506
+ M${xPos - tickWidth} ${lowerPos}
507
+ L${xPos + tickWidth} ${lowerPos}`}
508
+ />
509
+ )
510
+ })
511
+ : ''}
512
+ </Group>
513
+ )}
514
+ </Group>
515
+ </ErrorBoundary>
516
+ )
517
+ }
@@ -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