@cdc/chart 4.23.3 → 4.23.5

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 (98) hide show
  1. package/dist/cdcchart.js +52543 -50830
  2. package/examples/feature/__data__/area-chart.json +56 -0
  3. package/examples/{planet-example-data.json → feature/__data__/planet-example-data.json} +3 -8
  4. package/examples/feature/__data__/planet-logaritmic-data.json +56 -0
  5. package/examples/feature/area/area-chart-category.json +240 -0
  6. package/examples/{area-chart.json → feature/area/area-chart-date.json} +70 -13
  7. package/examples/feature/bar/example-bar-chart.json +558 -0
  8. package/examples/{horizontal-chart-max-increase.json → feature/bar/horizontal-chart-max-increase.json} +10 -4
  9. package/examples/{horizontal-chart.json → feature/bar/horizontal-chart.json} +10 -4
  10. package/examples/{horizontal-stacked-bar-chart.json → feature/bar/horizontal-stacked-bar-chart.json} +7 -3
  11. package/examples/{planet-chart-horizontal-example-config.json → feature/bar/planet-chart-horizontal-example-config.json} +8 -3
  12. package/examples/feature/bar/planet-chart-logaritmic-config.json +170 -0
  13. package/examples/{planet-example-config.json → feature/bar/planet-example-config.json} +2 -2
  14. package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -7
  15. package/examples/feature/boxplot/testing.csv +38 -0
  16. package/examples/feature/boxplot/valid-boxplot.csv +17 -0
  17. package/examples/feature/combo/combochart-categories_are_numbers .json +18 -0
  18. package/examples/{planet-combo-example-config.json → feature/combo/planet-combo-example-config.json} +1 -1
  19. package/examples/{planet-deviation-config.json → feature/deviation/planet-deviation-config.json} +2 -2
  20. package/examples/{planet-deviation-data.json → feature/deviation/planet-deviation-data.json} +9 -9
  21. package/examples/feature/filters/filter-testing.json +212 -0
  22. package/examples/feature/forecasting/case_date_example.csv +130 -0
  23. package/examples/feature/forecasting/effective_reproduction.json +202 -0
  24. package/examples/feature/forecasting/r_data.csv +130 -0
  25. package/examples/feature/forecasting/random_data.csv +366 -0
  26. package/examples/feature/line/line-chart.json +124 -0
  27. package/examples/{paired-bar-example.json → feature/paired-bar/paired-bar-example.json} +10 -4
  28. package/examples/{planet-pie-example-config.json → feature/pie/planet-pie-example-config.json} +2 -2
  29. package/examples/{scatterplot.json → feature/scatterplot/scatterplot.json} +1 -1
  30. package/examples/feature/test-highlight/test-highlight-2.json +789 -0
  31. package/examples/feature/test-highlight/test-highlight-vertical.json +561 -0
  32. package/examples/feature/test-highlight/test-highlight.json +100 -0
  33. package/examples/{case-rate-example-config.json → feature/tests-case-rate/case-rate-example-config.json} +2 -2
  34. package/examples/{covid-confidence-example-config.json → feature/tests-covid/covid-confidence-example-config.json} +8 -3
  35. package/examples/{covid-example-config.json → feature/tests-covid/covid-example-config.json} +7 -3
  36. package/examples/{cutoff-example-config.json → feature/tests-cutoff/cutoff-example-config.json} +7 -3
  37. package/examples/{date-exclusions-config.json → feature/tests-date-exclusions/date-exclusions-config.json} +2 -2
  38. package/examples/{example-bar-chart-nonnumeric.json → feature/tests-non-numerics/example-bar-chart-nonnumeric.json} +1 -1
  39. package/examples/{planet-pie-example-config-nonnumeric.json → feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json} +2 -2
  40. package/examples/{sparkline-chart-nonnumeric.json → feature/tests-non-numerics/sparkline-chart-nonnumeric.json} +1 -1
  41. package/examples/{stacked-vertical-bar-example-nonnumerics.json → feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json} +1 -2
  42. package/examples/gallery/bar-chart-horizontal/horizontal-highlight.json +345 -0
  43. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
  44. package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
  45. package/index.html +73 -49
  46. package/package.json +2 -2
  47. package/src/CdcChart.jsx +405 -40
  48. package/src/components/AreaChart.jsx +122 -80
  49. package/src/components/BarChart.jsx +126 -49
  50. package/src/components/BoxPlot.jsx +28 -20
  51. package/src/components/DataTable.jsx +7 -6
  52. package/src/components/DeviationBar.jsx +34 -34
  53. package/src/components/EditorPanel.jsx +1332 -352
  54. package/src/components/Legend.jsx +40 -4
  55. package/src/components/LineChart.jsx +10 -23
  56. package/src/components/LinearChart.jsx +133 -286
  57. package/src/components/PairedBarChart.jsx +6 -6
  58. package/src/components/PieChart.jsx +2 -4
  59. package/src/components/SparkLine.jsx +6 -42
  60. package/src/data/initial-state.js +23 -4
  61. package/src/hooks/useHighlightedBars.js +154 -0
  62. package/src/hooks/useMinMax.js +92 -0
  63. package/src/hooks/useReduceData.js +31 -57
  64. package/src/hooks/useScales.js +202 -0
  65. package/src/index.jsx +2 -1
  66. package/src/scss/editor-panel.scss +15 -0
  67. package/src/scss/main.scss +8 -6
  68. package/examples/box-plot.csv +0 -5
  69. package/examples/dynamic-legends.json +0 -125
  70. package/examples/example-bar-chart.json +0 -36
  71. package/examples/line-chart.json +0 -34
  72. package/examples/temp-example-config.json +0 -64
  73. package/examples/temp-example-data.json +0 -130
  74. package/src/components/Filters.jsx +0 -126
  75. /package/examples/{age-adjusted-rates.json → feature/__data__/age-adjusted-rates.json} +0 -0
  76. /package/examples/{new-data.csv → feature/__data__/new-data.csv} +0 -0
  77. /package/examples/{planet-example-data-max-increase.json → feature/__data__/planet-example-data-max-increase.json} +0 -0
  78. /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
  79. /package/examples/{stacked-vertical-bar-example-negative.json → feature/bar/stacked-vertical-bar-example-negative.json} +0 -0
  80. /package/examples/{stacked-vertical-bar-example.json → feature/bar/stacked-vertical-bar-example.json} +0 -0
  81. /package/examples/{box-plot-data.json → feature/boxplot/box-plot-data.json} +0 -0
  82. /package/examples/{newdata.json → feature/boxplot/boxplot-data.json} +0 -0
  83. /package/examples/{line-chart-max-increase.json → feature/line/line-chart-max-increase.json} +0 -0
  84. /package/examples/{paired-bar-data.json → feature/paired-bar/paired-bar-data.json} +0 -0
  85. /package/examples/{paired-bar-formatted.json → feature/paired-bar/paired-bar-formatted.json} +0 -0
  86. /package/examples/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +0 -0
  87. /package/examples/{example-sparkline.json → feature/sparkline/example-sparkline.json} +0 -0
  88. /package/examples/{big-small-test-bar.json → feature/tests-big-small/big-small-test-bar.json} +0 -0
  89. /package/examples/{big-small-test-line.json → feature/tests-big-small/big-small-test-line.json} +0 -0
  90. /package/examples/{big-small-test-negative.json → feature/tests-big-small/big-small-test-negative.json} +0 -0
  91. /package/examples/{case-rate-example-data.json → feature/tests-case-rate/case-rate-example-data.json} +0 -0
  92. /package/examples/{covid-example-data-confidence.json → feature/tests-covid/covid-example-data-confidence.json} +0 -0
  93. /package/examples/{covid-example-data.json → feature/tests-covid/covid-example-data.json} +0 -0
  94. /package/examples/{cutoff-example-data.json → feature/tests-cutoff/cutoff-example-data.json} +0 -0
  95. /package/examples/{date-exclusions-data.json → feature/tests-date-exclusions/date-exclusions-data.json} +0 -0
  96. /package/examples/{example-combo-bar-nonnumeric.json → feature/tests-non-numerics/example-combo-bar-nonnumeric.json} +0 -0
  97. /package/examples/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-nonnumeric.json} +0 -0
  98. /package/examples/{planet-example-data-nonnumeric.json → feature/tests-non-numerics/planet-example-data-nonnumeric.json} +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useContext, useCallback } from 'react'
1
+ import React, { useContext, useEffect, useState } from 'react'
2
2
 
3
3
  // cdc
4
4
  import ConfigContext from '../ConfigContext'
@@ -6,26 +6,29 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
6
  import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
7
 
8
8
  // visx & d3
9
+ import * as allCurves from '@visx/curve'
9
10
  import { AreaClosed, LinePath, Bar } from '@visx/shape'
10
11
  import { Group } from '@visx/group'
11
- import * as allCurves from '@visx/curve'
12
- import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'
12
+ import { useTooltip, useTooltipInPortal, defaultStyles, Tooltip } from '@visx/tooltip'
13
13
  import { localPoint } from '@visx/event'
14
14
  import { bisector } from 'd3-array'
15
15
 
16
- const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
16
+ const CoveAreaChart = ({ xScale, yScale, yMax, xMax, chartRef }) => {
17
17
  // enable various console logs in the file
18
18
  const DEBUG = false
19
+ const [chartPosition, setChartPosition] = useState(null)
20
+
21
+ useEffect(() => {
22
+ setChartPosition(chartRef.current.getBoundingClientRect())
23
+ }, [chartRef])
19
24
 
20
25
  // import data from context
21
- const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight } = useContext(ConfigContext)
26
+ const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight, colorScale } = useContext(ConfigContext)
27
+ const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
22
28
 
23
29
  // import tooltip helpers
24
30
  const { tooltipData, showTooltip } = useTooltip()
25
31
 
26
- // used for offset on tooltip hover
27
- let isEditor = window.location.href.includes('editor=true')
28
-
29
32
  // here we're inside of the svg,
30
33
  // it appears we need to use TooltipInPortal.
31
34
  const { TooltipInPortal } = useTooltipInPortal({
@@ -36,20 +39,21 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
36
39
 
37
40
  // Draw transparent bars over the chart to get tooltip data
38
41
  // Turn DEBUG on for additional context.
39
- let barThickness = xMax / config.data.length
42
+ if (!data) return
43
+ let barThickness = xMax / data.length
40
44
  let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
41
45
  let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
42
46
 
43
47
  // Tooltip helper for getting data to the closest date/category hovered.
44
48
  const getXValueFromCoordinate = x => {
45
- if (config.xAxis.type === 'categorical') {
49
+ if (config.xAxis.type === 'categorical' || config.visualizationType === 'Combo') {
46
50
  let eachBand = xScale.step()
47
51
  let numerator = x
48
52
  const index = Math.floor(Number(numerator) / eachBand)
49
53
  return xScale.domain()[index - 1] // fixes off by 1 error
50
54
  }
51
55
 
52
- if (config.xAxis.type === 'date') {
56
+ if (config.xAxis.type === 'date' && config.visualizationType !== 'Combo') {
53
57
  const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
54
58
  const x0 = xScale.invert(x)
55
59
  const index = bisectDate(config.data, x0, 1)
@@ -58,51 +62,55 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
58
62
  }
59
63
  }
60
64
 
61
- const handleMouseOver = useCallback(
62
- (e, data) => {
63
- // get the svg coordinates of the mouse
64
- // and get the closest values
65
- const eventSvgCoords = localPoint(e)
66
- const { x, y } = eventSvgCoords
67
-
68
- let closestXScaleValue = getXValueFromCoordinate(x)
69
- let formattedDate = formatDate(closestXScaleValue)
70
-
71
- let yScaleValues
72
- if (config.xAxis.type === 'categorical') {
73
- yScaleValues = data.filter(d => d[config.xAxis.dataKey] === closestXScaleValue)
74
- } else {
75
- yScaleValues = data.filter(d => d[config.xAxis.dataKey] === formattedDate)
76
- }
77
-
78
- let seriesToInclude = []
79
- let yScaleMaxValues = []
80
- let itemsToLoop = [config.runtime.xAxis.dataKey, ...config.runtime.seriesKeys]
65
+ const handleMouseOver = (e, data) => {
66
+ // get the svg coordinates of the mouse
67
+ // and get the closest values
68
+ const eventSvgCoords = localPoint(e)
69
+ const { x, y } = eventSvgCoords
81
70
 
82
- itemsToLoop.map(seriesKey => {
83
- return Object.entries(yScaleValues[0]).forEach(item => item[0] === seriesKey && seriesToInclude.push(item))
84
- })
71
+ let closestXScaleValue = getXValueFromCoordinate(x)
72
+ let formattedDate = formatDate(closestXScaleValue)
85
73
 
86
- // filter out the series that aren't added to the map.
87
- seriesToInclude.map(series => yScaleMaxValues.push(Number(yScaleValues[0][series])))
88
- seriesToInclude = Object.fromEntries(seriesToInclude)
89
-
90
- let tooltipData = {}
91
- tooltipData.data = seriesToInclude
92
- tooltipData.dataXPosition = isEditor ? 300 + x + 20 : x + 20
93
- tooltipData.dataYPosition = y - 20
74
+ let yScaleValues
75
+ if (config.xAxis.type === 'categorical') {
76
+ yScaleValues = data.filter(d => d[config.xAxis.dataKey] === closestXScaleValue)
77
+ } else {
78
+ yScaleValues = data.filter(d => formatDate(parseDate(d[config.xAxis.dataKey])) === formattedDate)
79
+ }
94
80
 
95
- let tooltipInformation = {
96
- tooltipData: tooltipData,
97
- tooltipTop: 0,
98
- tooltipValues: yScaleValues,
99
- tooltipLeft: x
81
+ let seriesToInclude = []
82
+ let yScaleMaxValues = []
83
+ let itemsToLoop = [config.runtime.xAxis.dataKey, ...config.runtime.seriesKeys]
84
+
85
+ itemsToLoop.map(seriesKey => {
86
+ if (!seriesKey) return
87
+ if (!yScaleValues[0]) return
88
+ for (const item of Object.entries(yScaleValues[0])) {
89
+ if (item[0] === seriesKey) {
90
+ seriesToInclude.push(item)
91
+ }
100
92
  }
93
+ })
94
+
95
+ // filter out the series that aren't added to the map.
96
+ seriesToInclude.map(series => yScaleMaxValues.push(Number(yScaleValues[0][series])))
97
+ if (!seriesToInclude) return
98
+ let tooltipDataFromSeries = Object.fromEntries(seriesToInclude) ? Object.fromEntries(seriesToInclude) : {}
99
+
100
+ let tooltipData = {}
101
+ tooltipData.data = tooltipDataFromSeries
102
+ tooltipData.dataXPosition = x + 20
103
+ tooltipData.dataYPosition = y - 100
104
+
105
+ let tooltipInformation = {
106
+ tooltipData: tooltipData,
107
+ tooltipTop: 0,
108
+ tooltipValues: yScaleValues,
109
+ tooltipLeft: x
110
+ }
101
111
 
102
- showTooltip(tooltipInformation)
103
- },
104
- [showTooltip] // eslint-disable-line
105
- )
112
+ showTooltip(tooltipInformation)
113
+ }
106
114
 
107
115
  const TooltipListItem = ({ item }) => {
108
116
  const [label, value] = item
@@ -113,59 +121,94 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
113
121
  return config.xAxis.type === 'date' ? xScale(parseDate(d[config.xAxis.dataKey])) : xScale(d[config.xAxis.dataKey])
114
122
  }
115
123
 
116
- const handleY = (d, index) => {
117
- return yScale(d[config.series[index].dataKey])
124
+ const handleY = (d, index, s = undefined) => {
125
+ return yScale(d[s.dataKey])
118
126
  }
119
127
 
120
128
  return (
121
129
  data && (
122
130
  <ErrorBoundary component='AreaChart'>
123
- <Group className='area-chart' key='area-wrapper' left={config.yAxis.size}>
124
- {config.series.map((s, index) => {
125
- let seriesColor = colorPalettesChart[config.palette][index]
131
+ <Group className='area-chart' key='area-wrapper' left={Number(config.yAxis.size)}>
132
+ {(config.runtime.areaSeriesKeys || config.runtime.seriesKeys).map((s, index) => {
133
+ let seriesData = data.map(d => {
134
+ return {
135
+ [config.xAxis.dataKey]: d[config.xAxis.dataKey],
136
+ [s.dataKey]: d[s.dataKey]
137
+ }
138
+ })
139
+
126
140
  let curveType = allCurves[s.lineType]
127
141
  let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s.dataKey) === -1
128
142
  let displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s.dataKey) !== -1
129
143
 
130
- data.map(d => xScale(parseDate(d[config.xAxis.dataKey])))
144
+ if (config.xAxis.type === 'date') {
145
+ data.map(d => xScale(parseDate(d[config.xAxis.dataKey])))
146
+ }
131
147
 
132
148
  return (
133
- <>
149
+ <React.Fragment key={index}>
134
150
  {/* prettier-ignore */}
135
- {/* this is the line that appears on top of the area chart */}
136
- <LinePath data={data} x={d => handleX(d)} y={d => yScale(d[config.series[index].dataKey])} stroke={seriesColor} strokeWidth={2} strokeOpacity={1} shapeRendering='geometricPrecision' curve={curveType} strokeDasharray={s.type ? handleLineType(s.type) : 0} />
151
+ <LinePath
152
+ data={seriesData}
153
+ x={d => handleX(d)}
154
+ y={d => handleY(d, index, s)}
155
+ stroke={displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000' : 'transparent'}
156
+ strokeWidth={2}
157
+ strokeOpacity={1}
158
+ shapeRendering='geometricPrecision'
159
+ curve={curveType}
160
+ strokeDasharray={s.type ? handleLineType(s.type) : 0}
161
+ />
137
162
 
138
163
  {/* prettier-ignore */}
139
- {/* filled in sections */}
140
- <AreaClosed key={'area-chart'} fill={displayArea ? seriesColor : 'transparent'} fillOpacity={transparentArea ? 0.25 : 0.5} data={data} x={d => handleX(d)} y={d => handleY(d, index)} yScale={yScale} curve={curveType} strokeDasharray={s.type ? handleLineType(s.typ) : 0} />
164
+ <AreaClosed
165
+ key={'area-chart'}
166
+ fill={ displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000' : 'transparent'}
167
+ fillOpacity={transparentArea ? 0.25 : 0.5}
168
+ data={seriesData}
169
+ x={d => handleX(d)}
170
+ y={d => handleY(d, index, s)}
171
+ yScale={yScale}
172
+ curve={curveType}
173
+ strokeDasharray={s.type ? handleLineType(s.type) : 0}
174
+ />
141
175
 
142
- <Bar x={d => handleX(d)} y={d => yScale(d[config.series[index].dataKey])} yScale={yScale} width={xMax} height={yMax} fill={DEBUG ? 'red' : 'transparent'} fillOpacity={0.05} style={DEBUG ? { stroke: 'black', strokeWidth: 2 } : {}} onMouseMove={e => handleMouseOver(e, data)} />
176
+ {/* Transparent bar for tooltips */}
177
+ {/* prettier-ignore */}
178
+ <Bar
179
+ width={ Number(xMax)}
180
+ height={ Number(yMax)}
181
+ fill={DEBUG ? 'red' : 'transparent'}
182
+ fillOpacity={0.05}
183
+ style={DEBUG ? { stroke: 'black', strokeWidth: 2 } : {}}
184
+ onMouseMove={e => handleMouseOver(e, data)}
185
+ />
143
186
 
144
187
  {/* circles that appear on hover */}
145
- {tooltipData && (
188
+ {tooltipData && Object.entries(tooltipData.data).length > 0 && (
146
189
  <circle
147
190
  cx={config.xAxis.type === 'categorical' ? xScale(tooltipData.data[config.xAxis.dataKey]) : xScale(parseDate(tooltipData.data[config.xAxis.dataKey]))}
148
191
  cy={yScale(tooltipData.data[s.dataKey])}
149
192
  r={4.5}
150
193
  opacity={1}
151
194
  fillOpacity={1}
152
- fill={seriesColor}
195
+ fill={displayArea ? (colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000') : 'transparent'}
153
196
  style={{ filter: 'unset', opacity: 1 }}
154
197
  />
155
198
  )}
156
199
 
157
- {/* bars to handle tooltips */}
200
+ {/* another tool for showing bars during debug mode. */}
158
201
  {DEBUG &&
159
- config.data.map((item, index) => {
202
+ data.map((item, index) => {
160
203
  return (
161
204
  <Bar
162
205
  className='bar-here'
163
- x={barThickness * index + offset}
164
- y={d => yScale(d[config.series[index].dataKey])}
206
+ x={Number(barThickness * index)}
207
+ y={d => Number(yScale(d[config.series[index].dataKey]))}
165
208
  yScale={yScale}
166
- width={barThicknessAdjusted}
209
+ width={Number(barThickness)}
167
210
  height={yMax}
168
- fill={'transparent'}
211
+ fill={DEBUG ? 'red' : 'transparent'}
169
212
  fillOpacity={1}
170
213
  style={{ stroke: 'black', strokeWidth: 2 }}
171
214
  onMouseMove={e => handleMouseOver(e, data)}
@@ -173,20 +216,19 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
173
216
  )
174
217
  })}
175
218
 
176
- {tooltipData && (
177
- <TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition} left={tooltipData.dataXPosition} style={defaultStyles}>
178
- <Group x={config.yAxis.size + 10} y={0}>
179
- <ul style={{ listStyle: 'none', paddingLeft: 'unset' }}>
180
- {Object.entries(tooltipData.data).map(item => (
181
- <li>
219
+ {tooltipData && Object.entries(tooltipData.data).length > 0 && (
220
+ <TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition + chartPosition?.top} left={tooltipData.dataXPosition + chartPosition?.left} style={defaultStyles}>
221
+ <ul style={{ listStyle: 'none', paddingLeft: 'unset', fontFamily: 'sans-serif', margin: 'auto', lineHeight: '1rem' }} data-tooltip-id={tooltip_id}>
222
+ {typeof tooltipData === 'object' &&
223
+ Object.entries(tooltipData.data).map(item => (
224
+ <li style={{ padding: '2.5px 0' }}>
182
225
  <TooltipListItem item={item} />
183
226
  </li>
184
227
  ))}
185
- </ul>
186
- </Group>
228
+ </ul>
187
229
  </TooltipInPortal>
188
230
  )}
189
- </>
231
+ </React.Fragment>
190
232
  )
191
233
  })}
192
234
  </Group>
@@ -6,16 +6,14 @@ import chroma from 'chroma-js'
6
6
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
7
7
  import ConfigContext from '../ConfigContext'
8
8
  import { BarStackHorizontal } from '@visx/shape'
9
+ import { useHighlightedBars } from '../hooks/useHighlightedBars'
9
10
 
10
11
  export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getXAxisData, getYAxisData, animatedChart, visible }) {
11
- const { transformedData: data, colorScale, seriesHighlight, config, formatNumber, updateConfig, colorPalettes, formatDate, isNumber, cleanData, getTextWidth, parseDate } = useContext(ConfigContext)
12
- // Just do this once up front otherwise we end up
13
- // calling clean several times on same set of data (TT)
14
- const cleanedData = cleanData(data, config.xAxis.dataKey)
15
-
12
+ const { transformedData: data, colorScale, seriesHighlight, config, formatNumber, updateConfig, colorPalettes, tableData, formatDate, isNumber, getTextWidth, parseDate } = useContext(ConfigContext)
13
+ const { HighLightedBarUtils } = useHighlightedBars(config)
16
14
  const { orientation, visualizationSubType } = config
17
15
  const isHorizontal = orientation === 'horizontal'
18
-
16
+ const barBorderWidth = 1
19
17
  const lollipopBarWidth = config.lollipopSize === 'large' ? 7 : config.lollipopSize === 'medium' ? 6 : 5
20
18
  const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
21
19
 
@@ -28,21 +26,18 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
28
26
  const tipRounding = config.tipRounding
29
27
  const radius = config.roundingStyle === 'standard' ? '8px' : config.roundingStyle === 'shallow' ? '5px' : config.roundingStyle === 'finger' ? '15px' : '0px'
30
28
  const stackCount = config.runtime.seriesKeys.length
31
- const barBorderWidth = 1
32
29
  const fontSize = { small: 16, medium: 18, large: 20 }
33
30
  const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
34
31
 
35
- const applyRadius = (index, isNegative) => {
36
- if (index === undefined || index === null || !isRounded) return
32
+ const applyRadius = index => {
33
+ if (index === undefined || index === null || !isRounded) return {}
37
34
  let style = {}
38
35
 
39
36
  if ((isStacked && index + 1 === stackCount) || !isStacked) {
40
- if (isNegative) {
41
- // reverse borderRadius to bottom
42
- style = isHorizontal ? { borderRadius: `0 ${radius} ${radius} 0` } : { borderRadius: `0 0 ${radius} ${radius}` }
43
- } else {
44
- style = isHorizontal ? { borderRadius: `0 ${radius} ${radius} 0` } : { borderRadius: `${radius} ${radius} 0 0` }
45
- }
37
+ style = isHorizontal ? { borderRadius: `0 ${radius} ${radius} 0` } : { borderRadius: `${radius} ${radius} 0 0` }
38
+ }
39
+ if (!isStacked && index === -1) {
40
+ style = isHorizontal ? { borderRadius: `${radius} 0 0 ${radius} ` } : { borderRadius: ` 0 0 ${radius} ${radius}` }
46
41
  }
47
42
  if (tipRounding === 'full' && isStacked && index === 0 && stackCount > 1) {
48
43
  style = isHorizontal ? { borderRadius: `${radius} 0 0 ${radius}` } : { borderRadius: `0 0 ${radius} ${radius}` }
@@ -52,7 +47,27 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
52
47
  }
53
48
  return style
54
49
  }
55
- // }
50
+
51
+ const assignColorsToValues = () => {
52
+ const palettesArr = colorPalettes[config.palette]
53
+ const values = tableData.map(d => {
54
+ return d[config.legend.colorCode]
55
+ })
56
+ // Map to hold unique values and their colors
57
+ let colorMap = new Map()
58
+ // Resultant array to hold colors to the values
59
+ let result = []
60
+
61
+ for (let i = 0; i < values.length; i++) {
62
+ // If value not in map, add it and assign a color
63
+ if (!colorMap.has(values[i])) {
64
+ colorMap.set(values[i], palettesArr[colorMap.size % palettesArr.length])
65
+ }
66
+ // push the colosr to the result array
67
+ result.push(colorMap.get(values[i]))
68
+ }
69
+ return result
70
+ }
56
71
 
57
72
  const updateBars = defaultBars => {
58
73
  // function updates stacked && regular && lollipop horizontal bars
@@ -136,7 +151,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
136
151
  <Group left={parseFloat(config.runtime.yAxis.size)}>
137
152
  {/* Stacked Vertical */}
138
153
  {config.visualizationSubType === 'stacked' && !isHorizontal && (
139
- <BarStack data={cleanedData} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} x={d => d[config.runtime.xAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale}>
154
+ <BarStack data={data} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} x={d => d[config.runtime.xAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale}>
140
155
  {barStacks =>
141
156
  barStacks.reverse().map(barStack =>
142
157
  barStack.bars.map(bar => {
@@ -148,7 +163,8 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
148
163
  // tooltips
149
164
  const xAxisValue = config.runtime.xAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.xAxis.dataKey])) : data[bar.index][config.runtime.xAxis.dataKey]
150
165
  const yAxisValue = formatNumber(bar.bar ? bar.bar.data[bar.key] : 0, 'left')
151
- const style = applyRadius(barStack.index, yAxisValue < 0)
166
+
167
+ const style = applyRadius(barStack.index)
152
168
  let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
153
169
  const xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
154
170
  if (!hasMultipleSeries) {
@@ -172,7 +188,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
172
188
  `}
173
189
  </style>
174
190
  <Group key={`bar-stack-${barStack.index}-${bar.index}`} id={`barStack${barStack.index}-${bar.index}`} className='stack vertical'>
175
- <Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barThickness * bar.index + offset} y={bar.y - 5} fill={bar.color} textAnchor='middle'>
191
+ <Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barThickness * bar.index + offset} y={bar.y - 5} fill={'#000'} textAnchor='middle'>
176
192
  {yAxisValue}
177
193
  </Text>
178
194
  <foreignObject
@@ -199,7 +215,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
199
215
  {/* Stacked Horizontal */}
200
216
  {config.visualizationSubType === 'stacked' && isHorizontal && (
201
217
  <>
202
- <BarStackHorizontal data={cleanedData} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} height={yMax} y={d => d[config.runtime.yAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale} offset='none'>
218
+ <BarStackHorizontal data={data} keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys} height={yMax} y={d => d[config.runtime.yAxis.dataKey]} xScale={xScale} yScale={yScale} color={colorScale} offset='none'>
203
219
  {barStacks =>
204
220
  barStacks.map(barStack =>
205
221
  updateBars(barStack.bars).map((bar, index) => {
@@ -210,7 +226,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
210
226
  // tooltips
211
227
  const xAxisValue = formatNumber(data[bar.index][bar.key], 'left')
212
228
  const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[bar.index][config.runtime.originalXAxis.dataKey])) : data[bar.index][config.runtime.originalXAxis.dataKey]
213
- const style = applyRadius(barStack.index, yAxisValue < 0)
229
+ const style = applyRadius(barStack.index)
214
230
  let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
215
231
  let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
216
232
  if (!hasMultipleSeries) {
@@ -297,7 +313,7 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
297
313
  {config.visualizationSubType !== 'stacked' && (
298
314
  <Group>
299
315
  <BarGroup
300
- data={cleanedData}
316
+ data={data}
301
317
  keys={config.runtime.barSeriesKeys || config.runtime.seriesKeys}
302
318
  height={yMax}
303
319
  x0={d => d[config.runtime.originalXAxis.dataKey]}
@@ -318,22 +334,44 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
318
334
  left={config.runtime.horizontal ? 0 : (xMax / barGroups.length) * barGroup.index}
319
335
  >
320
336
  {barGroup.bars.map((bar, index) => {
337
+ const scaleVal = config.useLogScale ? 0.1 : 0
338
+ const getHighlightedBarColorByValue = value => {
339
+ const match = config?.highlightedBarValues.filter(item => {
340
+ if (!item.value) return
341
+ return config.xAxis.type === 'date' ? formatDate(parseDate(item.value)) === value : item.value === value
342
+ })[0]
343
+
344
+ if (!match?.color) return `rgba(255, 102, 1)`
345
+ return match.color
346
+ }
347
+
348
+ const getHighlightedBarByValue = value => {
349
+ const match = config?.highlightedBarValues.filter(item => {
350
+ if (!item.value) return
351
+ return config.xAxis.type === 'date' ? formatDate(parseDate(item.value)) === value : item.value === value
352
+ })[0]
353
+
354
+ if (!match?.color) return false
355
+ return match
356
+ }
357
+
358
+ let highlightedBarValues = config.highlightedBarValues.map(item => item.value).filter(item => item !== ('' || undefined))
359
+
360
+ highlightedBarValues = config.xAxis.type === 'date' ? HighLightedBarUtils.formatDates(highlightedBarValues) : highlightedBarValues
361
+
321
362
  let transparentBar = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(bar.key) === -1
322
363
  let displayBar = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(bar.key) !== -1
323
- let barHeight = orientation === 'horizontal' ? config.barHeight : isNumber(Math.abs(yScale(bar.value) - yScale(0))) ? Math.abs(yScale(bar.value) - yScale(0)) : 0
364
+ let barHeight = orientation === 'horizontal' ? config.barHeight : isNumber(Math.abs(yScale(bar.value) - yScale(scaleVal))) ? Math.abs(yScale(bar.value) - yScale(scaleVal)) : 0
324
365
  let barY = bar.value >= 0 && isNumber(bar.value) ? bar.y : yScale(0)
325
366
  let barGroupWidth = ((config.runtime.horizontal ? yMax : xMax) / barGroups.length) * (config.barThickness || 0.8)
326
367
  let offset = (((config.runtime.horizontal ? yMax : xMax) / barGroups.length) * (1 - (config.barThickness || 0.8))) / 2
327
-
368
+ const barX = bar.value < 0 ? Math.abs(xScale(bar.value)) : xScale(0)
369
+ const barWidthHorizontal = Math.abs(xScale(bar.value) - xScale(scaleVal))
328
370
  // ! Unsure if this should go back.
329
371
  if (config.isLollipopChart) {
330
372
  offset = (config.runtime.horizontal ? yMax : xMax) / barGroups.length / 2 - lollipopBarWidth / 2
331
373
  }
332
- const set = new Set()
333
- data.forEach(d => set.add(d[config.legend.colorCode]))
334
- const uniqValues = Array.from(set)
335
-
336
- let palette = colorPalettes[config.palette].slice(0, uniqValues.length)
374
+ let palette = assignColorsToValues()
337
375
 
338
376
  let barWidth = config.isLollipopChart ? lollipopBarWidth : barGroupWidth / barGroup.bars.length
339
377
  let barColor = config.runtime.seriesLabels && config.runtime.seriesLabels[bar.key] ? colorScale(config.runtime.seriesLabels[bar.key]) : colorScale(bar.key)
@@ -351,18 +389,42 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
351
389
  xAxisValue = tempValue
352
390
  barWidth = config.barHeight
353
391
  }
392
+
393
+ const barPosition = bar.value < 0 ? 'below' : 'above'
394
+
354
395
  // check if bar text/value string fits into each bars.
355
396
  let textWidth = getTextWidth(xAxisValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
356
- let textFits = textWidth < bar.y - 5 // minus padding 5
357
-
397
+ let textFits = textWidth < barWidthHorizontal - 5 // minus padding 5
358
398
  let labelColor = '#000000'
359
399
 
360
400
  // Set label color
361
- if (chroma.contrast(labelColor, barColor) < 4.9) {
362
- if (textFits) labelColor = '#FFFFFF'
401
+ if (barColor && labelColor) {
402
+ if (chroma.contrast(labelColor, barColor) < 4.9) {
403
+ labelColor = textFits ? '#FFFFFF' : '#000000'
404
+ }
405
+ }
406
+
407
+ // Set if background is transparent'
408
+ labelColor = HighLightedBarUtils.checkFontColor(yAxisValue, highlightedBarValues, labelColor)
409
+
410
+ // control text position
411
+ let textAnchor = textFits ? 'end' : 'start'
412
+ let textAnchorLollipop = 'start'
413
+ let textPadding = textFits ? -5 : 5
414
+ let textPaddingLollipop = 10
415
+ // if bars are negative we change positions of text
416
+ if (barPosition === 'below') {
417
+ textAnchor = textFits ? 'start' : 'end'
418
+ textPadding = textFits ? 5 : -5
419
+ if (config.isLollipopChart) {
420
+ textAnchorLollipop = 'end'
421
+ textPaddingLollipop = -10
422
+ }
363
423
  }
364
424
 
365
- const style = applyRadius(index, yAxisValue < 0)
425
+ // create new Index based on bar value for border Radius
426
+ const newIndex = bar.value < 0 ? -1 : index
427
+ const style = applyRadius(newIndex)
366
428
 
367
429
  let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
368
430
  let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
@@ -379,6 +441,26 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
379
441
  ${xAxisTooltip}
380
442
  </div>`
381
443
 
444
+ const isRegularLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'regular'
445
+ const isTwoToneLollipopColor = config.isLollipopChart && config.lollipopColorStyle === 'two-tone'
446
+ const isHighlightedBar = config.orientation === 'vertical' ? highlightedBarValues?.includes(xAxisValue) : highlightedBarValues?.includes(yAxisValue)
447
+ const highlightedBarColor = config.orientation === 'vertical' ? getHighlightedBarColorByValue(xAxisValue) : getHighlightedBarColorByValue(yAxisValue)
448
+ const highlightedBar = config.orientation === 'vertical' ? getHighlightedBarByValue(xAxisValue) : getHighlightedBarByValue(yAxisValue)
449
+
450
+ const background = isRegularLollipopColor ? barColor : isTwoToneLollipopColor ? chroma(barColor).brighten(1) : isHighlightedBar ? 'transparent' : barColor
451
+
452
+ const borderColor = isHighlightedBar ? highlightedBarColor : config.barHasBorder === 'true' ? '#000' : 'transparent'
453
+
454
+ const borderWidth = isHighlightedBar ? highlightedBar.borderWidth : config.isLollipopChart ? 0 : config.barHasBorder === 'true' ? barBorderWidth : 0
455
+
456
+ const finalStyle = {
457
+ background,
458
+ borderColor,
459
+ borderStyle: 'solid',
460
+ borderWidth,
461
+ ...style
462
+ }
463
+
382
464
  return (
383
465
  <Group key={`${barGroup.index}--${index}--${orientation}`}>
384
466
  {/* This feels gross but inline transition was not working well*/}
@@ -394,15 +476,11 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
394
476
  <foreignObject
395
477
  id={`barGroup${barGroup.index}`}
396
478
  key={`bar-group-bar-${barGroup.index}-${bar.index}-${bar.value}-${bar.key}`}
397
- x={config.runtime.horizontal ? 0 : barWidth * bar.index + offset}
479
+ x={config.runtime.horizontal ? barX : barWidth * bar.index + offset}
398
480
  y={config.runtime.horizontal ? barWidth * bar.index : barY}
399
- width={config.runtime.horizontal ? bar.y : barWidth}
481
+ width={config.runtime.horizontal ? barWidthHorizontal : barWidth}
400
482
  height={isHorizontal && !config.isLollipopChart ? barWidth : isHorizontal && config.isLollipopChart ? lollipopBarWidth : barHeight}
401
- style={{
402
- background: config.isLollipopChart && config.lollipopColorStyle === 'regular' ? barColor : config.isLollipopChart && config.lollipopColorStyle === 'two-tone' ? chroma(barColor).brighten(1) : barColor,
403
- border: `${config.isLollipopChart ? 0 : config.barHasBorder === 'true' ? barBorderWidth : 0}px solid #333`,
404
- ...style
405
- }}
483
+ style={finalStyle}
406
484
  opacity={transparentBar ? 0.5 : 1}
407
485
  display={displayBar ? 'block' : 'none'}
408
486
  data-tooltip-html={tooltip}
@@ -414,21 +492,21 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
414
492
  x={bar.y}
415
493
  y={config.barHeight / 2 + config.barHeight * bar.index}
416
494
  fill={labelColor}
417
- dx={textFits ? -5 : 5}
495
+ dx={textPadding}
418
496
  verticalAnchor='middle'
419
- textAnchor={textFits ? 'end' : 'start'}
497
+ textAnchor={textAnchor}
420
498
  >
421
499
  {xAxisValue}
422
500
  </Text>
423
501
  )}
424
- ;
425
502
  {orientation === 'horizontal' && config.isLollipopChart && displayNumbersOnBar && (
426
503
  <Text
427
504
  display={displayBar ? 'block' : 'none'}
428
- x={`${bar.y + (config.isLollipopChart ? 15 : 5) + (config.isLollipopChart && barGroup.bars.length === bar.index ? offset : 0)}`} // padding
505
+ x={bar.y} // padding
429
506
  y={0}
430
507
  fill={'#000000'}
431
- textAnchor='start'
508
+ dx={textPaddingLollipop}
509
+ textAnchor={textAnchorLollipop}
432
510
  verticalAnchor='middle'
433
511
  fontWeight={'normal'}
434
512
  >
@@ -444,13 +522,12 @@ export default function BarChart({ xScale, yScale, seriesScale, xMax, yMax, getX
444
522
  : formatNumber(data[barGroup.index][config.runtime.originalXAxis.dataKey])}
445
523
  </Text>
446
524
  )}
447
- ;
525
+
448
526
  {orientation === 'vertical' && (
449
- <Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barWidth * (bar.index + 0.5) + offset} y={barY - 5} fill={barColor} textAnchor='middle'>
527
+ <Text display={config.labels && displayBar ? 'block' : 'none'} opacity={transparentBar ? 0.5 : 1} x={barWidth * (bar.index + 0.5) + offset} y={barY - 5} fill={labelColor} textAnchor='middle'>
450
528
  {yAxisValue}
451
529
  </Text>
452
530
  )}
453
- ;
454
531
  {config.isLollipopChart && config.lollipopShape === 'circle' && (
455
532
  <circle
456
533
  cx={orientation === 'horizontal' ? bar.y : barWidth * (barGroup.bars.length - bar.index - 1) + (isLabelBelowBar && orientation === 'horizontal' ? 0 : offset) + lollipopShapeSize / 3.5}