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