@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.
Files changed (74) hide show
  1. package/dist/cdcchart.js +56289 -702
  2. package/examples/Barchart_with_negative.json +34 -0
  3. package/examples/area-chart.json +187 -0
  4. package/examples/big-small-test-bar.json +328 -0
  5. package/examples/big-small-test-line.json +328 -0
  6. package/examples/big-small-test-negative.json +328 -0
  7. package/examples/box-plot.json +1 -2
  8. package/examples/dynamic-legends.json +1 -1
  9. package/examples/example-bar-chart-nonnumeric.json +36 -0
  10. package/examples/example-bar-chart.json +36 -0
  11. package/examples/example-combo-bar-nonnumeric.json +105 -0
  12. package/examples/example-sparkline.json +76 -0
  13. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
  14. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +1 -1
  15. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
  16. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
  17. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +2 -2
  18. package/examples/gallery/line/line.json +1 -0
  19. package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
  20. package/examples/horizontal-chart-max-increase.json +38 -0
  21. package/examples/line-chart-max-increase.json +32 -0
  22. package/examples/line-chart-nonnumeric.json +32 -0
  23. package/examples/line-chart.json +21 -63
  24. package/examples/newdata.json +1 -1
  25. package/examples/planet-combo-example-config.json +143 -20
  26. package/examples/planet-deviation-config.json +168 -0
  27. package/examples/planet-deviation-data.json +38 -0
  28. package/examples/planet-example-config.json +139 -20
  29. package/examples/planet-example-data-max-increase.json +56 -0
  30. package/examples/planet-example-data-nonnumeric.json +56 -0
  31. package/examples/planet-example-data.json +9 -9
  32. package/examples/planet-pie-example-config-nonnumeric.json +30 -0
  33. package/examples/scatterplot-continuous.csv +17 -0
  34. package/examples/scatterplot.json +136 -0
  35. package/examples/sparkline-chart-nonnumeric.json +76 -0
  36. package/examples/stacked-vertical-bar-example-negative.json +154 -0
  37. package/examples/stacked-vertical-bar-example-nonnumerics.json +154 -0
  38. package/index.html +91 -0
  39. package/package.json +33 -24
  40. package/src/{CdcChart.tsx → CdcChart.jsx} +196 -124
  41. package/src/components/AreaChart.jsx +198 -0
  42. package/src/components/{BarChart.tsx → BarChart.jsx} +154 -122
  43. package/src/components/BoxPlot.jsx +101 -0
  44. package/src/components/{DataTable.tsx → DataTable.jsx} +109 -28
  45. package/src/components/DeviationBar.jsx +191 -0
  46. package/src/components/{EditorPanel.js → EditorPanel.jsx} +676 -157
  47. package/src/components/{Filters.js → Filters.jsx} +6 -11
  48. package/src/components/Legend.jsx +316 -0
  49. package/src/components/{LineChart.tsx → LineChart.jsx} +22 -26
  50. package/src/components/{LinearChart.tsx → LinearChart.jsx} +214 -91
  51. package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +44 -78
  52. package/src/components/{PieChart.tsx → PieChart.jsx} +26 -44
  53. package/src/components/ScatterPlot.jsx +51 -0
  54. package/src/components/SparkLine.jsx +218 -0
  55. package/src/components/{useIntersectionObserver.tsx → useIntersectionObserver.jsx} +2 -2
  56. package/src/data/initial-state.js +51 -5
  57. package/src/hooks/useColorPalette.js +68 -0
  58. package/src/hooks/{useReduceData.ts → useReduceData.js} +26 -16
  59. package/src/hooks/useRightAxis.js +3 -1
  60. package/src/index.jsx +16 -0
  61. package/src/scss/DataTable.scss +22 -0
  62. package/src/scss/editor-panel.scss +5 -0
  63. package/src/scss/main.scss +30 -10
  64. package/src/test/CdcChart.test.jsx +6 -0
  65. package/vite.config.js +4 -0
  66. package/dist/495.js +0 -3
  67. package/dist/703.js +0 -1
  68. package/src/components/BoxPlot.js +0 -92
  69. package/src/components/Legend.js +0 -291
  70. package/src/components/SparkLine.js +0 -185
  71. package/src/hooks/useColorPalette.ts +0 -76
  72. package/src/index.html +0 -67
  73. package/src/index.tsx +0 -18
  74. /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,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
- ${config.dataDescription.valueKey}: ${formatNumber(d[groupTwo.dataKey])}
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={width} height={height} viewBox={`0 0 ${width} ${height}`} role='img' tabIndex={0}>
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
- 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)
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-tip={dataTipOne(d)}
142
- data-for={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
117
+ data-tooltip-html={dataTipOne(d)}
118
+ data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
143
119
  stroke='#333'
144
- strokeWidth={config.barBorderThickness || 1}
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={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'}>
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
- 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
- }
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
- .bar-${groupTwo.dataKey}-${d[config.xAxis.dataKey]} {
188
- transform-origin: ${halfWidth}px ${y}px
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-tip={dataTipTwo(d)}
203
- data-for={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
204
- strokeWidth={config.barBorderThickness || 1}
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={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'}>
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, { ProvidedProps, PieArcDatum } from '@visx/shape/lib/shapes/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 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, {
@@ -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
- type AnimatedPieProps<Datum> = ProvidedProps<Datum> & {
53
- animate?: boolean
54
- getKey: (d: PieArcDatum<Datum>) => string
55
- delay?: number
56
- }
57
-
58
- function AnimatedPie<Datum>({ arcs, path, getKey }: AnimatedPieProps<Datum>) {
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 }: { item: PieArcDatum<Datum>; props: PieStyles; key: string }) => {
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-tip={tooltip}
96
- data-for={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
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 }: { item: PieArcDatum<Datum>; props: PieStyles; key: string }) => {
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 || data} pieValue={d => d[config.runtime.yAxis.dataKey]} pieSortValues={() => -1} innerRadius={radius - donutThickness} outerRadius={radius}>
162
- {pie => <AnimatedPie<any> {...pie} getKey={d => d.data[config.runtime.xAxis.dataKey]} />}
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}`} html={true} type='light' arrowColor='rgba(0,0,0,0)' className='tooltip' />
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<any>()
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