@cdc/chart 4.23.1 → 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 +56289 -702
- package/examples/Barchart_with_negative.json +34 -0
- 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 +1 -2
- package/examples/dynamic-legends.json +1 -1
- package/examples/example-bar-chart-nonnumeric.json +36 -0
- package/examples/example-bar-chart.json +36 -0
- package/examples/example-combo-bar-nonnumeric.json +105 -0
- 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-categorical.json +1 -1
- 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/bar-chart-vertical/vertical-bar-chart.json +2 -2
- package/examples/gallery/line/line.json +1 -0
- package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
- package/examples/horizontal-chart-max-increase.json +38 -0
- package/examples/line-chart-max-increase.json +32 -0
- 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-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-nonnumeric.json +56 -0
- package/examples/planet-example-data.json +9 -9
- 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 +91 -0
- package/package.json +33 -24
- package/src/{CdcChart.tsx → CdcChart.jsx} +196 -124
- package/src/components/AreaChart.jsx +198 -0
- package/src/components/{BarChart.tsx → BarChart.jsx} +154 -122
- package/src/components/BoxPlot.jsx +101 -0
- package/src/components/{DataTable.tsx → DataTable.jsx} +109 -28
- package/src/components/DeviationBar.jsx +191 -0
- package/src/components/{EditorPanel.js → EditorPanel.jsx} +676 -157
- package/src/components/{Filters.js → Filters.jsx} +6 -11
- package/src/components/Legend.jsx +316 -0
- package/src/components/{LineChart.tsx → LineChart.jsx} +22 -26
- package/src/components/{LinearChart.tsx → LinearChart.jsx} +214 -91
- package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +44 -78
- package/src/components/{PieChart.tsx → PieChart.jsx} +26 -44
- package/src/components/ScatterPlot.jsx +51 -0
- package/src/components/SparkLine.jsx +218 -0
- package/src/components/{useIntersectionObserver.tsx → useIntersectionObserver.jsx} +2 -2
- package/src/data/initial-state.js +51 -5
- package/src/hooks/useColorPalette.js +68 -0
- package/src/hooks/{useReduceData.ts → useReduceData.js} +26 -16
- package/src/hooks/useRightAxis.js +3 -1
- package/src/index.jsx +16 -0
- package/src/scss/DataTable.scss +22 -0
- package/src/scss/editor-panel.scss +5 -0
- package/src/scss/main.scss +30 -10
- package/src/test/CdcChart.test.jsx +6 -0
- 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/components/Legend.js +0 -291
- package/src/components/SparkLine.js +0 -185
- package/src/hooks/useColorPalette.ts +0 -76
- package/src/index.html +0 -67
- package/src/index.tsx +0 -18
- /package/src/{context.tsx → ConfigContext.jsx} +0 -0
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import React, { useContext } from 'react'
|
|
2
2
|
import { Group } from '@visx/group'
|
|
3
3
|
import { Bar } from '@visx/shape'
|
|
4
|
-
import { scaleLinear
|
|
4
|
+
import { scaleLinear } from '@visx/scale'
|
|
5
5
|
import { Text } from '@visx/text'
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import ConfigContext from '../ConfigContext'
|
|
8
8
|
import chroma from 'chroma-js'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
height: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
16
|
-
const { config, colorScale, transformedData, formatNumber, seriesHighlight } = useContext<any>(Context)
|
|
10
|
+
const PairedBarChart = ({ width, height, originalWidth }) => {
|
|
11
|
+
const { config, colorScale, transformedData: data, formatNumber, seriesHighlight, getTextWidth } = useContext(ConfigContext)
|
|
17
12
|
|
|
18
13
|
if (!config || config?.series?.length < 2) return
|
|
19
14
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
15
|
+
const borderWidth = config.barHasBorder === 'true' ? 1 : 0
|
|
16
|
+
const halfWidth = width / 2
|
|
17
|
+
const fontSize = { small: 16, medium: 18, large: 20 }
|
|
18
|
+
const offset = 1.02 // Offset of the left bar from the Axis
|
|
24
19
|
|
|
25
20
|
const groupOne = {
|
|
26
21
|
parentKey: config.dataDescription.seriesKey,
|
|
@@ -45,15 +40,10 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
const xScale = scaleLinear({
|
|
48
|
-
domain: [0, Math.max(groupOne.max, groupTwo.max)],
|
|
43
|
+
domain: [0, Math.max(groupOne.max * offset, groupTwo.max * 1.1)],
|
|
49
44
|
range: [0, halfWidth]
|
|
50
45
|
})
|
|
51
46
|
|
|
52
|
-
const yScale = scaleBand({
|
|
53
|
-
range: [0, adjustedHeight],
|
|
54
|
-
domain: data.map(d => d[config.dataDescription.xKey])
|
|
55
|
-
})
|
|
56
|
-
|
|
57
47
|
// Set label color
|
|
58
48
|
let labelColor = '#000000'
|
|
59
49
|
|
|
@@ -65,11 +55,13 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
65
55
|
groupTwo.labelColor = '#FFFFFF'
|
|
66
56
|
}
|
|
67
57
|
|
|
58
|
+
const label = config.yAxis.label ? `${config.yAxis.label}: ` : ''
|
|
59
|
+
|
|
68
60
|
const dataTipOne = d => {
|
|
69
61
|
return `<p>
|
|
70
62
|
${config.dataDescription.seriesKey}: ${groupOne.dataKey}<br/>
|
|
71
63
|
${config.xAxis.dataKey}: ${d[config.xAxis.dataKey]}<br/>
|
|
72
|
-
${
|
|
64
|
+
${label}${formatNumber(d[groupOne.dataKey])}
|
|
73
65
|
</p>`
|
|
74
66
|
}
|
|
75
67
|
|
|
@@ -77,14 +69,10 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
77
69
|
return `<p>
|
|
78
70
|
${config.dataDescription.seriesKey}: ${groupTwo.dataKey}<br/>
|
|
79
71
|
${config.xAxis.dataKey}: ${d[config.xAxis.dataKey]}<br/>
|
|
80
|
-
${
|
|
72
|
+
${label}${formatNumber(d[groupTwo.dataKey])}
|
|
81
73
|
</p>`
|
|
82
74
|
}
|
|
83
75
|
|
|
84
|
-
const isLabelBelowBar = config.yAxis.labelPlacement === 'Below Bar'
|
|
85
|
-
const isLabelOnYAxis = config.yAxis.labelPlacement === 'On Date/Category Axis'
|
|
86
|
-
const isLabelMissing = !config.yAxis.labelPlacement
|
|
87
|
-
|
|
88
76
|
return (
|
|
89
77
|
width > 0 && (
|
|
90
78
|
<>
|
|
@@ -96,7 +84,7 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
96
84
|
}
|
|
97
85
|
`}
|
|
98
86
|
</style>
|
|
99
|
-
<svg id='cdc-visualization__paired-bar-chart' width={
|
|
87
|
+
<svg id='cdc-visualization__paired-bar-chart' width={originalWidth} height={height} viewBox={`0 0 ${width + Number(config.runtime.yAxis.size)} ${height}`} role='img' tabIndex={0}>
|
|
100
88
|
<Group top={0} left={Number(config.xAxis.size)}>
|
|
101
89
|
{data
|
|
102
90
|
.filter(item => config.series[0].dataKey === groupOne.dataKey)
|
|
@@ -105,26 +93,14 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
105
93
|
let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(config.series[0].dataKey) !== -1
|
|
106
94
|
let barWidth = xScale(d[config.series[0].dataKey])
|
|
107
95
|
let barHeight = Number(config.barHeight) ? Number(config.barHeight) : 25
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else {
|
|
117
|
-
config.barPadding = barPadding
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
config.barPadding = barPadding / 2
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
config.height = Number(barHeight) * data.length + config.barPadding * data.length
|
|
125
|
-
|
|
126
|
-
let y = yScale([d[config.dataDescription.xKey]]) + config.barHeight / 1.5
|
|
127
|
-
y = Number(config.barPadding) > 20 ? (y += Number(config.barPadding / 3.5) - config.barHeight / 2) : (y += 0)
|
|
96
|
+
// update bar Y to give dynamic Y when user applyes BarSpace
|
|
97
|
+
let y = 0
|
|
98
|
+
y = index !== 0 ? (Number(config.barSpace) + barHeight + borderWidth) * index : y
|
|
99
|
+
const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
|
|
100
|
+
config.heights.horizontal = totalheight
|
|
101
|
+
// check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
|
|
102
|
+
const textWidth = getTextWidth(formatNumber(d[groupOne.dataKey]), `normal ${fontSize[config.fontSize]}px sans-serif`)
|
|
103
|
+
const textFits = textWidth < barWidth - 5 // minus padding dx(5)
|
|
128
104
|
|
|
129
105
|
return (
|
|
130
106
|
<>
|
|
@@ -138,15 +114,15 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
138
114
|
width={xScale(d[config.series[0].dataKey])}
|
|
139
115
|
height={barHeight}
|
|
140
116
|
fill={groupOne.color}
|
|
141
|
-
data-
|
|
142
|
-
data-
|
|
117
|
+
data-tooltip-html={dataTipOne(d)}
|
|
118
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
143
119
|
stroke='#333'
|
|
144
|
-
strokeWidth={
|
|
120
|
+
strokeWidth={borderWidth}
|
|
145
121
|
opacity={transparentBar ? 0.5 : 1}
|
|
146
122
|
display={displayBar ? 'block' : 'none'}
|
|
147
123
|
/>
|
|
148
124
|
{config.yAxis.displayNumbersOnBar && displayBar && (
|
|
149
|
-
<Text textAnchor={
|
|
125
|
+
<Text textAnchor={textFits ? 'start' : 'end'} dx={textFits ? 5 : -5} verticalAnchor='middle' x={halfWidth - barWidth} y={y + config.barHeight / 2} fill={textFits ? groupOne.labelColor : '#000'}>
|
|
150
126
|
{formatNumber(d[groupOne.dataKey])}
|
|
151
127
|
</Text>
|
|
152
128
|
)}
|
|
@@ -156,38 +132,28 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
156
132
|
})}
|
|
157
133
|
{data
|
|
158
134
|
.filter(item => config.series[1].dataKey === groupTwo.dataKey)
|
|
159
|
-
.map(d => {
|
|
135
|
+
.map((d, index) => {
|
|
160
136
|
let barWidth = xScale(d[config.series[1].dataKey])
|
|
161
137
|
let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(config.series[1].dataKey) === -1
|
|
162
138
|
let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(config.series[1].dataKey) !== -1
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
let
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (isLabelBelowBar || isLabelMissing || isLabelOnYAxis) {
|
|
173
|
-
if (barHeight < 40) {
|
|
174
|
-
config.barPadding = 40
|
|
175
|
-
} else {
|
|
176
|
-
config.barPadding = barPadding
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
config.barPadding = barPadding / 2
|
|
180
|
-
}
|
|
181
|
-
}
|
|
139
|
+
let barHeight = config.barHeight ? Number(config.barHeight) : 25
|
|
140
|
+
// update bar Y to give dynamic Y when user applyes BarSpace
|
|
141
|
+
let y = 0
|
|
142
|
+
y = index !== 0 ? (Number(config.barSpace) + barHeight + borderWidth) * index : y
|
|
143
|
+
const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
|
|
144
|
+
config.heights.horizontal = totalheight
|
|
145
|
+
// check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
|
|
146
|
+
const textWidth = getTextWidth(formatNumber(d[groupTwo.dataKey]), `normal ${fontSize[config.fontSize]}px sans-serif`)
|
|
147
|
+
const isTextFits = textWidth < barWidth - 5 // minus padding dx(5)
|
|
182
148
|
|
|
183
149
|
return (
|
|
184
150
|
<>
|
|
185
151
|
<style>
|
|
186
152
|
{`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
153
|
+
.bar-${groupTwo.dataKey}-${d[config.xAxis.dataKey]} {
|
|
154
|
+
transform-origin: ${halfWidth}px ${y}px
|
|
155
|
+
}
|
|
156
|
+
`}
|
|
191
157
|
</style>
|
|
192
158
|
<Group key={`group-${groupTwo.dataKey}-${d[config.dataDescription.xKey]}`} className='horizontal'>
|
|
193
159
|
<Bar
|
|
@@ -199,15 +165,15 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
|
|
|
199
165
|
width={xScale(d[config.series[1].dataKey])}
|
|
200
166
|
height={barHeight}
|
|
201
167
|
fill={groupTwo.color}
|
|
202
|
-
data-
|
|
203
|
-
data-
|
|
204
|
-
strokeWidth={
|
|
168
|
+
data-tooltip-html={dataTipTwo(d)}
|
|
169
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
170
|
+
strokeWidth={borderWidth}
|
|
205
171
|
stroke='#333'
|
|
206
172
|
opacity={transparentBar ? 0.5 : 1}
|
|
207
173
|
display={displayBar ? 'block' : 'none'}
|
|
208
174
|
/>
|
|
209
175
|
{config.yAxis.displayNumbersOnBar && displayBar && (
|
|
210
|
-
<Text textAnchor={
|
|
176
|
+
<Text textAnchor={isTextFits ? 'end' : 'start'} dx={isTextFits ? -5 : 5} verticalAnchor='middle' x={halfWidth + barWidth} y={y + config.barHeight / 2} fill={isTextFits ? groupTwo.labelColor : '#000'}>
|
|
211
177
|
{formatNumber(d[groupTwo.dataKey])}
|
|
212
178
|
</Text>
|
|
213
179
|
)}
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import React, { useContext, useState, useEffect, useRef } from 'react'
|
|
2
2
|
import { animated, useTransition, interpolate } from 'react-spring'
|
|
3
|
-
import ReactTooltip from 'react-tooltip'
|
|
3
|
+
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
4
4
|
|
|
5
|
-
import Pie
|
|
5
|
+
import Pie from '@visx/shape/lib/shapes/Pie'
|
|
6
6
|
import chroma from 'chroma-js'
|
|
7
7
|
import { Group } from '@visx/group'
|
|
8
8
|
import { Text } from '@visx/text'
|
|
9
9
|
import useIntersectionObserver from './useIntersectionObserver'
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import ConfigContext from '../ConfigContext'
|
|
12
12
|
|
|
13
13
|
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
type PieStyles = { startAngle: number; endAngle: number }
|
|
17
|
-
|
|
18
|
-
const enterUpdateTransition = ({ startAngle, endAngle }: PieArcDatum<any>) => ({
|
|
15
|
+
const enterUpdateTransition = ({ startAngle, endAngle }) => ({
|
|
19
16
|
startAngle,
|
|
20
17
|
endAngle
|
|
21
18
|
})
|
|
22
19
|
|
|
23
20
|
export default function PieChart() {
|
|
24
|
-
const { transformedData: data, config, dimensions, seriesHighlight, colorScale, formatNumber, currentViewport, handleChartAriaLabels } = useContext
|
|
21
|
+
const { transformedData: data, config, dimensions, seriesHighlight, colorScale, formatNumber, currentViewport, handleChartAriaLabels, cleanData } = useContext(ConfigContext)
|
|
22
|
+
|
|
23
|
+
const cleanedData = cleanData(data, config.xAxis.dataKey)
|
|
25
24
|
|
|
26
|
-
const [filteredData, setFilteredData] = useState
|
|
27
|
-
const [animatedPie, setAnimatePie] = useState
|
|
25
|
+
const [filteredData, setFilteredData] = useState(undefined)
|
|
26
|
+
const [animatedPie, setAnimatePie] = useState(false)
|
|
28
27
|
|
|
29
28
|
const triggerRef = useRef()
|
|
30
29
|
const dataRef = useIntersectionObserver(triggerRef, {
|
|
@@ -32,11 +31,11 @@ export default function PieChart() {
|
|
|
32
31
|
})
|
|
33
32
|
|
|
34
33
|
// Make sure the chart is visible if in the editor
|
|
34
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
35
35
|
useEffect(() => {
|
|
36
36
|
const element = document.querySelector('.isEditor')
|
|
37
37
|
if (element) {
|
|
38
38
|
// parent element is visible
|
|
39
|
-
console.log('setAnimation')
|
|
40
39
|
setAnimatePie(prevState => true)
|
|
41
40
|
}
|
|
42
41
|
})
|
|
@@ -47,30 +46,19 @@ export default function PieChart() {
|
|
|
47
46
|
setAnimatePie(true)
|
|
48
47
|
}, 500)
|
|
49
48
|
}
|
|
50
|
-
}, [dataRef?.isIntersecting, config.animate])
|
|
49
|
+
}, [dataRef?.isIntersecting, config.animate]) // eslint-disable-line
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const transitions = useTransition<PieArcDatum<Datum>, PieStyles>(
|
|
60
|
-
arcs,
|
|
61
|
-
getKey,
|
|
62
|
-
// @ts-ignore react-spring doesn't like this overload
|
|
63
|
-
{
|
|
64
|
-
from: enterUpdateTransition,
|
|
65
|
-
enter: enterUpdateTransition,
|
|
66
|
-
update: enterUpdateTransition,
|
|
67
|
-
leave: enterUpdateTransition
|
|
68
|
-
}
|
|
69
|
-
)
|
|
51
|
+
function AnimatedPie({ arcs, path, getKey }) {
|
|
52
|
+
const transitions = useTransition(arcs, getKey, {
|
|
53
|
+
from: enterUpdateTransition,
|
|
54
|
+
enter: enterUpdateTransition,
|
|
55
|
+
update: enterUpdateTransition,
|
|
56
|
+
leave: enterUpdateTransition
|
|
57
|
+
})
|
|
70
58
|
|
|
71
59
|
return (
|
|
72
60
|
<>
|
|
73
|
-
{transitions.map(({ item: arc, props, key }
|
|
61
|
+
{transitions.map(({ item: arc, props, key }) => {
|
|
74
62
|
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${formatNumber(arc.data[config.runtime.yAxis.dataKey])}` : formatNumber(arc.data[config.runtime.yAxis.dataKey])
|
|
75
63
|
let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${arc.data[config.runtime.xAxis.dataKey]}` : arc.data[config.runtime.xAxis.dataKey]
|
|
76
64
|
|
|
@@ -79,11 +67,9 @@ export default function PieChart() {
|
|
|
79
67
|
${xAxisTooltip}<br />
|
|
80
68
|
Percent: ${Math.round((((arc.endAngle - arc.startAngle) * 180) / Math.PI / 360) * 100) + '%'}
|
|
81
69
|
`
|
|
82
|
-
|
|
83
70
|
return (
|
|
84
71
|
<Group key={key} style={{ opacity: config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(arc.data[config.runtime.xAxis.dataKey]) === -1 ? 0.5 : 1 }}>
|
|
85
72
|
<animated.path
|
|
86
|
-
// compute interpolated path d attribute from intermediate angle values
|
|
87
73
|
d={interpolate([props.startAngle, props.endAngle], (startAngle, endAngle) =>
|
|
88
74
|
path({
|
|
89
75
|
...arc,
|
|
@@ -92,13 +78,13 @@ export default function PieChart() {
|
|
|
92
78
|
})
|
|
93
79
|
)}
|
|
94
80
|
fill={colorScale(arc.data[config.runtime.xAxis.dataKey])}
|
|
95
|
-
data-
|
|
96
|
-
data-
|
|
81
|
+
data-tooltip-html={tooltip}
|
|
82
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
97
83
|
/>
|
|
98
84
|
</Group>
|
|
99
85
|
)
|
|
100
86
|
})}
|
|
101
|
-
{transitions.map(({ item: arc, key }
|
|
87
|
+
{transitions.map(({ item: arc, key }) => {
|
|
102
88
|
const [centroidX, centroidY] = path.centroid(arc)
|
|
103
89
|
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1
|
|
104
90
|
|
|
@@ -148,23 +134,19 @@ export default function PieChart() {
|
|
|
148
134
|
} else {
|
|
149
135
|
setFilteredData(undefined)
|
|
150
136
|
}
|
|
151
|
-
}, [seriesHighlight])
|
|
152
|
-
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
ReactTooltip.rebuild()
|
|
155
|
-
})
|
|
137
|
+
}, [seriesHighlight]) // eslint-disable-line
|
|
156
138
|
|
|
157
139
|
return (
|
|
158
140
|
<ErrorBoundary component='PieChart'>
|
|
159
141
|
<svg width={width} height={height} className={`animated-pie group ${config.animate === false || animatedPie ? 'animated' : ''}`} role='img' aria-label={handleChartAriaLabels(config)}>
|
|
160
142
|
<Group top={centerY} left={centerX}>
|
|
161
|
-
<Pie data={filteredData ||
|
|
162
|
-
{pie => <AnimatedPie
|
|
143
|
+
<Pie data={filteredData || cleanedData} pieValue={d => d[config.runtime.yAxis.dataKey]} pieSortValues={() => -1} innerRadius={radius - donutThickness} outerRadius={radius}>
|
|
144
|
+
{pie => <AnimatedPie {...pie} getKey={d => d.data[config.runtime.xAxis.dataKey]} />}
|
|
163
145
|
</Pie>
|
|
164
146
|
</Group>
|
|
165
147
|
</svg>
|
|
166
148
|
<div ref={triggerRef} />
|
|
167
|
-
<ReactTooltip id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
149
|
+
<ReactTooltip id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} variant='light' arrowColor='rgba(0,0,0,0)' className='tooltip' />
|
|
168
150
|
</ErrorBoundary>
|
|
169
151
|
)
|
|
170
152
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
import ConfigContext from '../ConfigContext'
|
|
3
|
+
import { Group } from '@visx/group'
|
|
4
|
+
|
|
5
|
+
const CoveScatterPlot = ({ xScale, yScale, getXAxisData, getYAxisData }) => {
|
|
6
|
+
const { colorScale, transformedData: data, config, formatNumber, seriesHighlight, colorPalettes } = useContext(ConfigContext)
|
|
7
|
+
|
|
8
|
+
// TODO: copied from line chart should probably be a constant somewhere.
|
|
9
|
+
let circleRadii = 4.5
|
|
10
|
+
const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
|
|
11
|
+
|
|
12
|
+
const handleTooltip = (item, s) => `<div>
|
|
13
|
+
${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && hasMultipleSeries ? `${config.runtime.seriesLabels[s] || ''}<br/>` : ''}
|
|
14
|
+
${config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')} <br/>
|
|
15
|
+
${config.yAxis.label}: ${formatNumber(item[s], 'left')}
|
|
16
|
+
</div>`
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Group className='scatter-plot' left={config.yAxis.size}>
|
|
20
|
+
{data.map((item, dataIndex) => {
|
|
21
|
+
// prettier-ignore
|
|
22
|
+
return config.runtime.seriesKeys.map((s, index) => {
|
|
23
|
+
const transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s) === -1
|
|
24
|
+
const displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s) !== -1
|
|
25
|
+
const seriesColor = config.palette ? colorPalettes[config.palette][index] : '#000'
|
|
26
|
+
|
|
27
|
+
let pointStyles = {
|
|
28
|
+
filter: 'unset',
|
|
29
|
+
opacity: 1,
|
|
30
|
+
stroke: displayArea ? 'black' : ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<circle
|
|
35
|
+
key={`${dataIndex}-${index}`}
|
|
36
|
+
r={circleRadii}
|
|
37
|
+
cx={xScale(item[config.xAxis.dataKey])}
|
|
38
|
+
cy={yScale(item[s])}
|
|
39
|
+
fill={displayArea ? seriesColor : 'transparent'}
|
|
40
|
+
fillOpacity={transparentArea ? .25 : 1}
|
|
41
|
+
style={pointStyles}
|
|
42
|
+
data-tooltip-html={handleTooltip(item, s)}
|
|
43
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
})}
|
|
48
|
+
</Group>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
export default CoveScatterPlot
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React, { useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import * as allCurves from '@visx/curve'
|
|
4
|
+
import { Group } from '@visx/group'
|
|
5
|
+
import { LinePath } from '@visx/shape'
|
|
6
|
+
import { Text } from '@visx/text'
|
|
7
|
+
import { scaleLinear, scalePoint } from '@visx/scale'
|
|
8
|
+
import { AxisBottom } from '@visx/axis'
|
|
9
|
+
import { MarkerArrow } from '@visx/marker'
|
|
10
|
+
|
|
11
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
12
|
+
|
|
13
|
+
import useReduceData from '../hooks/useReduceData'
|
|
14
|
+
|
|
15
|
+
import ConfigContext from '../ConfigContext'
|
|
16
|
+
|
|
17
|
+
export default function SparkLine({ width: parentWidth, height: parentHeight }) {
|
|
18
|
+
const { transformedData: data, config, parseDate, formatDate, seriesHighlight, formatNumber, colorScale, handleChartAriaLabels } = useContext(ConfigContext)
|
|
19
|
+
let width = parentWidth
|
|
20
|
+
const { minValue, maxValue } = useReduceData(config, data, ConfigContext)
|
|
21
|
+
|
|
22
|
+
const margin = { top: 5, right: 10, bottom: 10, left: 10 }
|
|
23
|
+
const height = parentHeight
|
|
24
|
+
|
|
25
|
+
const xMax = width - config.runtime.yAxis.size
|
|
26
|
+
const yMax = height - margin.top - 20
|
|
27
|
+
|
|
28
|
+
const getXAxisData = d => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
|
|
29
|
+
const getYAxisData = (d, seriesKey) => d[seriesKey]
|
|
30
|
+
|
|
31
|
+
let xScale
|
|
32
|
+
let yScale
|
|
33
|
+
let seriesScale
|
|
34
|
+
const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
|
|
35
|
+
const isMaxValid = Number(enteredMaxValue) >= Number(maxValue)
|
|
36
|
+
const isMinValid = Number(enteredMinValue) <= Number(minValue)
|
|
37
|
+
|
|
38
|
+
// REMOVE bad data points from the data set
|
|
39
|
+
// Examples: NA, N/A, "1,234", "anystring"
|
|
40
|
+
// - if you dont call this on data into LineGroup below, for example
|
|
41
|
+
// then entire data series are removed because of the defined statement
|
|
42
|
+
// i.e. if a series has any bad data points the entire series wont plot
|
|
43
|
+
const cleanData = (data, testing = false) => {
|
|
44
|
+
let cleanedup = []
|
|
45
|
+
if (testing) console.log('## Data to clean=', data)
|
|
46
|
+
data.forEach(function (d, i) {
|
|
47
|
+
let cleanedSeries = {}
|
|
48
|
+
Object.keys(d).forEach(function (key) {
|
|
49
|
+
if (key === 'Date') {
|
|
50
|
+
// pass thru the dates
|
|
51
|
+
cleanedSeries[key] = d[key]
|
|
52
|
+
} else {
|
|
53
|
+
// remove comma and dollar signs
|
|
54
|
+
let tmp = d[key] !== null && d[key] !== '' ? d[key].replace(/[,$]/g, '') : ''
|
|
55
|
+
if (testing) console.log('tmp no comma or $', tmp)
|
|
56
|
+
if ((tmp !== '' && tmp !== null && !isNaN(tmp)) || (tmp !== '' && tmp !== null && /\d+\.?\d*/.test(tmp))) {
|
|
57
|
+
cleanedSeries[key] = tmp
|
|
58
|
+
} else {
|
|
59
|
+
// return nothing to skip bad data point
|
|
60
|
+
cleanedSeries[key] = '' // returning blank fixes broken chart draw
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
cleanedup.push(cleanedSeries)
|
|
65
|
+
})
|
|
66
|
+
if (testing) console.log('## cleanedData =', cleanedup)
|
|
67
|
+
return cleanedup
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Just do this once up front otherwise we end up
|
|
71
|
+
// calling clean several times on same set of data (TT)
|
|
72
|
+
const cleanedData = cleanData(data, config.xAxis.dataKey)
|
|
73
|
+
|
|
74
|
+
if (cleanedData) {
|
|
75
|
+
let min = enteredMinValue && isMinValid ? enteredMinValue : minValue
|
|
76
|
+
let max = enteredMaxValue && isMaxValid ? enteredMaxValue : Number.MIN_VALUE
|
|
77
|
+
|
|
78
|
+
if (max === Number.MIN_VALUE) {
|
|
79
|
+
max = maxValue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//Adds Y Axis data padding if applicable
|
|
83
|
+
if (config.runtime.yAxis.paddingPercent) {
|
|
84
|
+
let paddingValue = (max - min) * config.runtime.yAxis.paddingPercent
|
|
85
|
+
min -= paddingValue
|
|
86
|
+
max += paddingValue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let xAxisDataMapped = cleanedData.map(d => getXAxisData(d))
|
|
90
|
+
|
|
91
|
+
if (config.runtime.horizontal) {
|
|
92
|
+
xScale = scaleLinear({
|
|
93
|
+
domain: [min, max],
|
|
94
|
+
range: [0, xMax]
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
yScale = config.runtime.xAxis.type === 'date' ? scaleLinear({ domain: [Math.min(...xAxisDataMapped), Math.max(...xAxisDataMapped)] }) : scalePoint({ domain: xAxisDataMapped, padding: 0.5 })
|
|
98
|
+
|
|
99
|
+
seriesScale = scalePoint({
|
|
100
|
+
domain: config.runtime.barSeriesKeys || config.runtime.seriesKeys,
|
|
101
|
+
range: [0, yMax]
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
yScale.rangeRound([0, yMax])
|
|
105
|
+
} else {
|
|
106
|
+
min = min < 0 ? min * 1.11 : min
|
|
107
|
+
|
|
108
|
+
yScale = scaleLinear({
|
|
109
|
+
domain: [min, max],
|
|
110
|
+
range: [yMax - margin.bottom, margin.top]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
xScale = scalePoint({
|
|
114
|
+
domain: xAxisDataMapped,
|
|
115
|
+
range: [margin.left, width - margin.right]
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// eslint-disable-next-line
|
|
119
|
+
seriesScale = scalePoint({
|
|
120
|
+
domain: config.runtime.barSeriesKeys || config.runtime.seriesKeys,
|
|
121
|
+
range: [0, xMax]
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const handleSparkLineTicks = [xScale.domain()[0], xScale.domain()[xScale.domain().length - 1]]
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ErrorBoundary component='SparkLine'>
|
|
130
|
+
<svg role='img' aria-label={handleChartAriaLabels(config)} width={width} height={height} className={'sparkline'} tabIndex={0}>
|
|
131
|
+
{config.runtime.lineSeriesKeys.length > 0
|
|
132
|
+
? config.runtime.lineSeriesKeys
|
|
133
|
+
: config.runtime.seriesKeys.map((seriesKey, index) => (
|
|
134
|
+
<>
|
|
135
|
+
<Group
|
|
136
|
+
className='sparkline-group'
|
|
137
|
+
height={parentHeight}
|
|
138
|
+
style={{ height: parentHeight }}
|
|
139
|
+
top={margin.top}
|
|
140
|
+
key={`series-${seriesKey}`}
|
|
141
|
+
opacity={config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(seriesKey) === -1 ? 0.5 : 1}
|
|
142
|
+
display={config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
|
|
143
|
+
>
|
|
144
|
+
{cleanedData.map((d, dataIndex) => {
|
|
145
|
+
let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${formatNumber(getYAxisData(d, seriesKey))}` : formatNumber(getYAxisData(d, seriesKey))
|
|
146
|
+
let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${d[config.runtime.xAxis.dataKey]}` : d[config.runtime.xAxis.dataKey]
|
|
147
|
+
|
|
148
|
+
const tooltip = `<div>
|
|
149
|
+
${yAxisTooltip}<br />
|
|
150
|
+
${xAxisTooltip}<br />
|
|
151
|
+
${config.seriesLabel ? `${config.seriesLabel}: ${seriesKey}` : ''}
|
|
152
|
+
</div>`
|
|
153
|
+
|
|
154
|
+
let circleRadii = 4.5
|
|
155
|
+
return (
|
|
156
|
+
<Group key={`series-${seriesKey}-point-${dataIndex}`}>
|
|
157
|
+
<Text display={config.labels ? 'block' : 'none'} x={xScale(getXAxisData(d))} y={yScale(getYAxisData(d, seriesKey))} fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'} textAnchor='middle'>
|
|
158
|
+
{formatNumber(d[seriesKey])}
|
|
159
|
+
</Text>
|
|
160
|
+
|
|
161
|
+
{dataIndex + 1 !== data.length && (config.lineDatapointStyle === 'always show' || config.lineDatapointStyle === 'hover') && (
|
|
162
|
+
<circle
|
|
163
|
+
key={`${seriesKey}-${dataIndex}`}
|
|
164
|
+
r={circleRadii}
|
|
165
|
+
cx={xScale(getXAxisData(d))}
|
|
166
|
+
cy={yScale(getYAxisData(d, seriesKey))}
|
|
167
|
+
fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
168
|
+
style={{ fill: colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000' }}
|
|
169
|
+
data-tooltip-html={tooltip}
|
|
170
|
+
data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
</Group>
|
|
174
|
+
)
|
|
175
|
+
})}
|
|
176
|
+
<LinePath
|
|
177
|
+
curve={allCurves.curveLinear}
|
|
178
|
+
data={cleanedData}
|
|
179
|
+
x={d => xScale(getXAxisData(d))}
|
|
180
|
+
y={d => yScale(getYAxisData(d, seriesKey))}
|
|
181
|
+
stroke={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
182
|
+
strokeWidth={2}
|
|
183
|
+
strokeOpacity={1}
|
|
184
|
+
shapeRendering='geometricPrecision'
|
|
185
|
+
markerEnd={`url(#${'arrow'}--${index})`}
|
|
186
|
+
/>
|
|
187
|
+
<MarkerArrow
|
|
188
|
+
id={`arrow--${index}`}
|
|
189
|
+
refX={2}
|
|
190
|
+
size={6}
|
|
191
|
+
markerEnd={`url(#${'arrow'}--${index})`}
|
|
192
|
+
strokeOpacity={1}
|
|
193
|
+
fillOpacity={1}
|
|
194
|
+
// stroke={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
195
|
+
fill={colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[seriesKey] : seriesKey) : '#000'}
|
|
196
|
+
/>
|
|
197
|
+
</Group>
|
|
198
|
+
<AxisBottom
|
|
199
|
+
top={yMax + margin.top}
|
|
200
|
+
hideAxisLine
|
|
201
|
+
hideTicks
|
|
202
|
+
scale={xScale}
|
|
203
|
+
tickValues={handleSparkLineTicks}
|
|
204
|
+
tickFormat={formatDate}
|
|
205
|
+
stroke={'black'}
|
|
206
|
+
tickStroke={'black'}
|
|
207
|
+
tickLabelProps={() => ({
|
|
208
|
+
fill: 'black',
|
|
209
|
+
fontSize: 11,
|
|
210
|
+
textAnchor: 'middle'
|
|
211
|
+
})}
|
|
212
|
+
/>
|
|
213
|
+
</>
|
|
214
|
+
))}
|
|
215
|
+
</svg>
|
|
216
|
+
</ErrorBoundary>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
export default function useIntersectionObserver(elementRef, { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false }) {
|
|
4
|
-
const [entry, setEntry] = useState
|
|
4
|
+
const [entry, setEntry] = useState()
|
|
5
5
|
|
|
6
6
|
const frozen = entry?.isIntersecting && freezeOnceVisible
|
|
7
7
|
|
|
@@ -22,7 +22,7 @@ export default function useIntersectionObserver(elementRef, { threshold = 0, roo
|
|
|
22
22
|
observer.observe(node)
|
|
23
23
|
|
|
24
24
|
return () => observer.disconnect()
|
|
25
|
-
}, 500)
|
|
25
|
+
}, 500)
|
|
26
26
|
}, [elementRef, threshold, root, rootMargin, frozen])
|
|
27
27
|
|
|
28
28
|
return entry
|