@cdc/chart 4.22.10 → 4.23.1

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 (80) hide show
  1. package/README.md +5 -5
  2. package/dist/495.js +3 -0
  3. package/dist/703.js +1 -0
  4. package/dist/cdcchart.js +723 -6
  5. package/examples/age-adjusted-rates.json +1486 -1218
  6. package/examples/box-plot-data.json +71 -0
  7. package/examples/box-plot.csv +5 -0
  8. package/examples/{private/yaxis-test.json → box-plot.json} +46 -54
  9. package/examples/case-rate-example-config.json +1 -1
  10. package/examples/covid-confidence-example-config.json +33 -33
  11. package/examples/covid-example-config.json +34 -34
  12. package/examples/covid-example-data-confidence.json +30 -30
  13. package/examples/covid-example-data.json +20 -20
  14. package/examples/cutoff-example-config.json +36 -36
  15. package/examples/cutoff-example-data.json +36 -36
  16. package/examples/date-exclusions-config.json +1 -1
  17. package/examples/dynamic-legends.json +124 -124
  18. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart-with-numbers-on-bar.json +191 -197
  19. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +230 -240
  20. package/examples/gallery/bar-chart-horizontal/horizontal-stacked.json +239 -247
  21. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +138 -136
  22. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +79 -79
  23. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +80 -80
  24. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +67 -67
  25. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +179 -110
  26. package/examples/gallery/lollipop/lollipop-style-horizontal.json +215 -219
  27. package/examples/gallery/paired-bar/paired-bar-chart.json +195 -195
  28. package/examples/horizontal-chart.json +35 -35
  29. package/examples/horizontal-stacked-bar-chart.json +34 -34
  30. package/examples/line-chart.json +75 -75
  31. package/examples/new-data.csv +17 -0
  32. package/examples/newdata.json +90 -0
  33. package/examples/paired-bar-data.json +16 -14
  34. package/examples/paired-bar-example.json +48 -48
  35. package/examples/paired-bar-formatted.json +36 -36
  36. package/examples/planet-chart-horizontal-example-config.json +33 -33
  37. package/examples/planet-combo-example-config.json +34 -31
  38. package/examples/planet-example-config.json +35 -33
  39. package/examples/planet-example-data.json +56 -56
  40. package/examples/planet-pie-example-config.json +28 -28
  41. package/examples/stacked-vertical-bar-example.json +1 -1
  42. package/examples/temp-example-config.json +61 -54
  43. package/examples/temp-example-data.json +1 -1
  44. package/package.json +3 -2
  45. package/src/CdcChart.tsx +449 -434
  46. package/src/components/BarChart.tsx +383 -497
  47. package/src/components/BoxPlot.js +92 -0
  48. package/src/components/DataTable.tsx +182 -197
  49. package/src/components/EditorPanel.js +1068 -722
  50. package/src/components/Filters.js +131 -0
  51. package/src/components/Legend.js +286 -329
  52. package/src/components/LineChart.tsx +143 -81
  53. package/src/components/LinearChart.tsx +432 -451
  54. package/src/components/PairedBarChart.tsx +197 -213
  55. package/src/components/PieChart.tsx +105 -151
  56. package/src/components/SparkLine.js +179 -201
  57. package/src/components/useIntersectionObserver.tsx +19 -20
  58. package/src/context.tsx +3 -3
  59. package/src/data/initial-state.js +44 -17
  60. package/src/hooks/useActiveElement.js +13 -13
  61. package/src/hooks/useChartClasses.js +34 -28
  62. package/src/hooks/useColorPalette.ts +56 -63
  63. package/src/hooks/useLegendClasses.js +18 -10
  64. package/src/hooks/useReduceData.ts +64 -77
  65. package/src/hooks/useRightAxis.js +25 -0
  66. package/src/hooks/useTopAxis.js +6 -0
  67. package/src/index.html +19 -19
  68. package/src/index.tsx +13 -16
  69. package/src/scss/DataTable.scss +6 -5
  70. package/src/scss/editor-panel.scss +71 -69
  71. package/src/scss/main.scss +188 -114
  72. package/src/scss/variables.scss +1 -1
  73. package/examples/private/line-test-data.json +0 -22
  74. package/examples/private/line-test-two.json +0 -216
  75. package/examples/private/line-test.json +0 -102
  76. package/examples/private/newtest.csv +0 -101
  77. package/examples/private/shawn.json +0 -1296
  78. package/examples/private/test.json +0 -10124
  79. package/examples/private/yaxis-testing.csv +0 -27
  80. package/examples/private/yaxis.json +0 -28
package/src/CdcChart.tsx CHANGED
@@ -1,156 +1,147 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
1
+ import React, { useState, useEffect, useCallback } from 'react'
2
2
 
3
3
  // IE11
4
- import 'core-js/stable';
5
- import ResizeObserver from 'resize-observer-polyfill';
6
- import 'whatwg-fetch';
4
+ import 'core-js/stable'
5
+ import ResizeObserver from 'resize-observer-polyfill'
6
+ import 'whatwg-fetch'
7
+ import * as d3 from 'd3-array'
7
8
 
8
9
  // External Libraries
9
- import { scaleOrdinal } from '@visx/scale';
10
- import ParentSize from '@visx/responsive/lib/components/ParentSize';
11
- import { timeParse, timeFormat } from 'd3-time-format';
12
- import Papa from 'papaparse';
13
- import parse from 'html-react-parser';
14
-
10
+ import { scaleOrdinal } from '@visx/scale'
11
+ import ParentSize from '@visx/responsive/lib/components/ParentSize'
12
+ import { timeParse, timeFormat } from 'd3-time-format'
13
+ import { format } from 'd3-format'
14
+ import Papa from 'papaparse'
15
+ import parse from 'html-react-parser'
16
+ import { Base64 } from 'js-base64'
15
17
 
16
18
  // Primary Components
17
- import Context from './context';
18
- import PieChart from './components/PieChart';
19
- import LinearChart from './components/LinearChart';
19
+ import Context from './context'
20
+ import PieChart from './components/PieChart'
21
+ import LinearChart from './components/LinearChart'
20
22
 
21
- import {colorPalettesChart as colorPalettes} from '../../core/data/colorPalettes';
23
+ import { colorPalettesChart as colorPalettes } from '../../core/data/colorPalettes'
22
24
 
23
- import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events';
25
+ import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
24
26
 
25
- import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses';
27
+ import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
26
28
 
27
- import SparkLine from './components/SparkLine';
28
- import Legend from './components/Legend';
29
- import DataTable from './components/DataTable';
30
- import defaults from './data/initial-state';
31
- import EditorPanel from './components/EditorPanel';
32
- import Loading from '@cdc/core/components/Loading';
29
+ import SparkLine from './components/SparkLine'
30
+ import Legend from './components/Legend'
31
+ import DataTable from './components/DataTable'
32
+ import defaults from './data/initial-state'
33
+ import EditorPanel from './components/EditorPanel'
34
+ import Loading from '@cdc/core/components/Loading'
35
+ import Filters from './components/Filters'
36
+ import CoveMediaControls from '@cdc/core/helpers/CoveMediaControls'
33
37
 
34
38
  // helpers
35
39
  import numberFromString from '@cdc/core/helpers/numberFromString'
36
- import getViewport from '@cdc/core/helpers/getViewport';
37
- import { DataTransform } from '@cdc/core/helpers/DataTransform';
38
- import cacheBustingString from '@cdc/core/helpers/cacheBustingString';
39
-
40
- import './scss/main.scss';
41
-
42
- export default function CdcChart(
43
- { configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname,link} :
44
- { configUrl?: string, config?: any, isEditor?: boolean, isDashboard?: boolean, setConfig?, setEditing?, hostname?,link?:any }
45
- ) {
46
-
47
- const transform = new DataTransform();
48
-
49
- interface keyable { [key: string]: any }
50
-
51
- const [loading, setLoading] = useState<Boolean>(true);
52
- const [colorScale, setColorScale] = useState<any>(null);
53
- const [config, setConfig] = useState<keyable>({});
54
- const [stateData, setStateData] = useState<Array<Object>>(config.data || []);
55
- const [excludedData, setExcludedData] = useState<Array<Object>>();
56
- const [filteredData, setFilteredData] = useState<Array<Object>>();
57
- const [seriesHighlight, setSeriesHighlight] = useState<Array<String>>([]);
58
- const [currentViewport, setCurrentViewport] = useState<String>('lg');
59
- const [dimensions, setDimensions] = useState<Array<Number>>([]);
60
- const [externalFilters, setExternalFilters] = useState(null);
40
+ import getViewport from '@cdc/core/helpers/getViewport'
41
+ import { DataTransform } from '@cdc/core/helpers/DataTransform'
42
+ import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
43
+
44
+ import './scss/main.scss'
45
+
46
+ export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }: { configUrl?: string; config?: any; isEditor?: boolean; isDashboard?: boolean; setConfig?; setEditing?; hostname?; link?: any }) {
47
+ const transform = new DataTransform()
48
+ interface keyable {
49
+ [key: string]: any
50
+ }
51
+
52
+ const [loading, setLoading] = useState<Boolean>(true)
53
+ const [colorScale, setColorScale] = useState<any>(null)
54
+ const [config, setConfig] = useState<keyable>({})
55
+ const [stateData, setStateData] = useState<Array<Object>>(config.data || [])
56
+ const [excludedData, setExcludedData] = useState<Array<Object>>()
57
+ const [filteredData, setFilteredData] = useState<Array<Object>>()
58
+ const [seriesHighlight, setSeriesHighlight] = useState<Array<String>>([])
59
+ const [currentViewport, setCurrentViewport] = useState<String>('lg')
60
+ const [dimensions, setDimensions] = useState<Array<Number>>([])
61
+ const [externalFilters, setExternalFilters] = useState(null)
61
62
  const [container, setContainer] = useState()
62
63
  const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
63
64
  const [dynamicLegendItems, setDynamicLegendItems] = useState([])
65
+ const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
64
66
 
65
- const legendGlyphSize = 15;
66
- const legendGlyphSizeHalf = legendGlyphSize / 2;
67
+ const legendGlyphSize = 15
68
+ const legendGlyphSizeHalf = legendGlyphSize / 2
67
69
 
68
- const {
69
- barBorderClass,
70
- lineDatapointClass,
71
- contentClasses,
72
- innerContainerClasses,
73
- sparkLineStyles
74
- } = useDataVizClasses(config)
70
+ // Destructure items from config for more readable JSX
71
+ const { legend, title, description, visualizationType } = config
72
+ const { barBorderClass, lineDatapointClass, contentClasses, innerContainerClasses, sparkLineStyles } = useDataVizClasses(config)
75
73
 
76
74
  const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
77
75
 
78
-
79
-
80
-
81
76
  const handleChartAriaLabels = (state, testing = false) => {
82
- if(testing) console.log(`handleChartAriaLabels Testing On:`, state);
83
- try {
84
- if(!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state');
85
- let ariaLabel = '';
86
-
87
- if(state.visualizationType) {
88
- ariaLabel += `${state.visualizationType} chart`
89
- }
77
+ if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
78
+ try {
79
+ if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
80
+ let ariaLabel = ''
90
81
 
91
- if(state.title && state.visualizationType) {
92
- ariaLabel += ` with the title: ${state.title}`
93
- }
82
+ if (state.visualizationType) {
83
+ ariaLabel += `${state.visualizationType} chart`
84
+ }
94
85
 
95
- return ariaLabel;
96
- } catch(e) {
97
- console.error(e.message)
86
+ if (state.title && state.visualizationType) {
87
+ ariaLabel += ` with the title: ${state.title}`
98
88
  }
89
+
90
+ return ariaLabel
91
+ } catch (e) {
92
+ console.error(e.message)
93
+ }
99
94
  }
100
95
 
101
96
  const loadConfig = async () => {
102
- let response = configObj || await (await fetch(configUrl)).json();
97
+ let response = configObj || (await (await fetch(configUrl)).json())
103
98
 
104
99
  // If data is included through a URL, fetch that and store
105
- let data = response.formattedData || response.data || {};
100
+ let data = response.formattedData || response.data || {}
106
101
 
107
102
  if (response.dataUrl) {
108
-
109
103
  try {
110
104
  const regex = /(?:\.([^.]+))?$/
111
105
 
112
- const ext = (regex.exec(response.dataUrl)[1])
106
+ const ext = regex.exec(response.dataUrl)[1]
113
107
  if ('csv' === ext) {
114
- data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
115
- .then(response => response.text())
116
- .then(responseText => {
117
- const parsedCsv = Papa.parse(responseText, {
118
- header: true,
119
- dynamicTyping: true,
120
- skipEmptyLines: true
121
- })
122
- return parsedCsv.data
123
- })
108
+ data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
109
+ .then(response => response.text())
110
+ .then(responseText => {
111
+ const parsedCsv = Papa.parse(responseText, {
112
+ header: true,
113
+ dynamicTyping: true,
114
+ skipEmptyLines: true
115
+ })
116
+ return parsedCsv.data
117
+ })
124
118
  }
125
119
 
126
120
  if ('json' === ext) {
127
- data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
128
- .then(response => response.json())
121
+ data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
129
122
  }
130
123
  } catch {
131
- console.error(`Cannot parse URL: ${response.dataUrl}`);
132
- data = [];
124
+ console.error(`Cannot parse URL: ${response.dataUrl}`)
125
+ data = []
133
126
  }
134
127
 
135
- if(response.dataDescription) {
136
- data = transform.autoStandardize(data);
137
- data = transform.developerStandardize(data, response.dataDescription);
128
+ if (response.dataDescription) {
129
+ data = transform.autoStandardize(data)
130
+ data = transform.developerStandardize(data, response.dataDescription)
138
131
  }
139
132
  }
140
133
 
141
- if(data) {
134
+ if (data) {
142
135
  setStateData(data)
143
136
  setExcludedData(data)
144
137
  }
145
138
 
146
- let newConfig = {...defaults, ...response}
147
- if(undefined === newConfig.table.show) newConfig.table.show = !isDashboard
148
- updateConfig(newConfig, data);
139
+ let newConfig = { ...defaults, ...response }
140
+ if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
141
+ updateConfig(newConfig, data)
149
142
  }
150
143
 
151
-
152
144
  const updateConfig = (newConfig, dataOverride = undefined) => {
153
-
154
145
  let data = dataOverride || stateData
155
146
 
156
147
  // Deeper copy
@@ -158,23 +149,17 @@ export default function CdcChart(
158
149
  if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
159
150
  newConfig[key] = { ...defaults[key], ...newConfig[key] }
160
151
  }
161
- });
152
+ })
162
153
 
163
154
  // Loop through and set initial data with exclusions - this should persist through any following data transformations (ie. filters)
164
155
  let newExcludedData
165
156
 
166
157
  if (newConfig.exclusions && newConfig.exclusions.active) {
167
-
168
158
  if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
169
159
  newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey]))
170
- } else if (
171
- newConfig.xAxis.type === 'date' &&
172
- (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) &&
173
- newConfig.xAxis.dateParseFormat
174
- ) {
175
-
160
+ } else if (newConfig.xAxis.type === 'date' && (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) && newConfig.xAxis.dateParseFormat) {
176
161
  // Filter dates
177
- const timestamp = (e) => new Date(e).getTime();
162
+ const timestamp = e => new Date(e).getTime()
178
163
 
179
164
  let startDate = timestamp(newConfig.exclusions.dateStart)
180
165
  let endDate = timestamp(newConfig.exclusions.dateEnd) + 86399999 //Increase by 24h in ms (86400000ms - 1ms) to include selected end date for .getTime() comparative
@@ -183,16 +168,12 @@ export default function CdcChart(
183
168
  let endDateValid = undefined !== typeof endDate && false === isNaN(endDate)
184
169
 
185
170
  if (startDateValid && endDateValid) {
186
- newExcludedData = data.filter(e =>
187
- (timestamp(e[newConfig.xAxis.dataKey]) >= startDate) &&
188
- (timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
189
- )
171
+ newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
190
172
  } else if (startDateValid) {
191
173
  newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate)
192
174
  } else if (endDateValid) {
193
175
  newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
194
176
  }
195
-
196
177
  } else {
197
178
  newExcludedData = dataOverride || stateData
198
179
  }
@@ -203,134 +184,192 @@ export default function CdcChart(
203
184
  setExcludedData(newExcludedData)
204
185
 
205
186
  // After data is grabbed, loop through and generate filter column values if there are any
206
- let currentData;
187
+ let currentData
207
188
  if (newConfig.filters) {
208
-
209
189
  newConfig.filters.forEach((filter, index) => {
190
+ let filterValues = []
210
191
 
211
- let filterValues = [];
212
-
213
- filterValues = generateValuesForFilter(filter.columnName, newExcludedData);
214
-
215
- newConfig.filters[index].values = filterValues;
216
- // Initial filter should be active
217
- newConfig.filters[index].active = filterValues[0];
192
+ filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData)
218
193
 
219
- });
220
- currentData = filterData(newConfig.filters, newExcludedData);
221
- setFilteredData(currentData);
194
+ newConfig.filters[index].values = filterValues
195
+ // Initial filter should be active
196
+ newConfig.filters[index].active = filterValues[0]
197
+ })
198
+ currentData = filterData(newConfig.filters, newExcludedData)
199
+ setFilteredData(currentData)
222
200
  }
223
201
 
224
202
  //Enforce default values that need to be calculated at runtime
225
- newConfig.runtime = {};
226
- newConfig.runtime.seriesLabels = {};
227
- newConfig.runtime.seriesLabelsAll = [];
228
- newConfig.runtime.originalXAxis = newConfig.xAxis;
229
-
203
+ newConfig.runtime = {}
204
+ newConfig.runtime.seriesLabels = {}
205
+ newConfig.runtime.seriesLabelsAll = []
206
+ newConfig.runtime.originalXAxis = newConfig.xAxis
230
207
 
231
208
  if (newConfig.visualizationType === 'Pie') {
232
- newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey]);
233
- newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys;
209
+ newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
210
+ newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
234
211
  } else {
235
- newConfig.runtime.seriesKeys = newConfig.series ? newConfig.series.map((series) => {
236
- newConfig.runtime.seriesLabels[series.dataKey] = series.label || series.dataKey;
237
- newConfig.runtime.seriesLabelsAll.push(series.label || series.dataKey);
238
- return series.dataKey;
239
- }) : [];
212
+ newConfig.runtime.seriesKeys = newConfig.series
213
+ ? newConfig.series.map(series => {
214
+ newConfig.runtime.seriesLabels[series.dataKey] = series.label || series.dataKey
215
+ newConfig.runtime.seriesLabelsAll.push(series.label || series.dataKey)
216
+ return series.dataKey
217
+ })
218
+ : []
219
+ }
220
+
221
+ if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
222
+ console.log('hit', newConfig)
223
+
224
+ // stats
225
+ let allKeys = data.map(d => d[newConfig.xAxis.dataKey])
226
+ let allValues = data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
227
+
228
+ const uniqueArray = function (arrArg) {
229
+ return arrArg.filter(function (elem, pos, arr) {
230
+ return arr.indexOf(elem) === pos
231
+ })
232
+ }
233
+
234
+ const groups = uniqueArray(allKeys)
235
+ const plots = []
236
+
237
+ console.log('d', data)
238
+ console.log('newConfig', newConfig)
239
+ console.log('groups', groups)
240
+ console.log('allKeys', allKeys)
241
+ console.log('allValues', allValues)
242
+
243
+ // group specific statistics
244
+ // prevent re-renders
245
+ groups.map((g, index) => {
246
+ if (!g) return
247
+ // filter data by group
248
+ let filteredData = data.filter(item => item[newConfig.xAxis.dataKey] === g)
249
+ let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
250
+ console.log('g', g)
251
+ console.log('item', filteredData)
252
+ console.log('item', newConfig)
253
+ // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
254
+
255
+ const q1 = d3.quantile(filteredDataValues, 0.25)
256
+ const q3 = d3.quantile(filteredDataValues, 0.75)
257
+ const iqr = q3 - q1
258
+ const lowerBounds = q1 - (q3 - q1) * 1.5
259
+ const upperBounds = q3 + (q3 - q1) * 1.5
260
+ const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
261
+ plots.push({
262
+ columnCategory: g,
263
+ columnMean: d3.mean(filteredDataValues),
264
+ columnMedian: d3.median(filteredDataValues),
265
+ columnFirstQuartile: q1,
266
+ columnThirdQuartile: q3,
267
+ columnMin: q1 - 1.5 * iqr,
268
+ columnMax: q3 + 1.5 * iqr,
269
+ columnIqr: iqr,
270
+ columnOutliers: outliers,
271
+ values: filteredDataValues
272
+ })
273
+ })
274
+
275
+ // any other data we can add to boxplots
276
+ newConfig.boxplot['allValues'] = allValues
277
+ newConfig.boxplot['categories'] = groups
278
+ newConfig.boxplot.push(...plots)
240
279
  }
241
280
 
242
281
  if (newConfig.visualizationType === 'Combo' && newConfig.series) {
243
- newConfig.runtime.barSeriesKeys = [];
244
- newConfig.runtime.lineSeriesKeys = [];
245
- newConfig.series.forEach((series) => {
246
- if(series.type === 'Bar'){
247
- newConfig.runtime.barSeriesKeys.push(series.dataKey);
282
+ newConfig.runtime.barSeriesKeys = []
283
+ newConfig.runtime.lineSeriesKeys = []
284
+ newConfig.series.forEach(series => {
285
+ if (series.type === 'Bar') {
286
+ newConfig.runtime.barSeriesKeys.push(series.dataKey)
248
287
  }
249
- if(series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg'){
250
- newConfig.runtime.lineSeriesKeys.push(series.dataKey);
288
+ if (series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg') {
289
+ newConfig.runtime.lineSeriesKeys.push(series.dataKey)
251
290
  }
252
- });
291
+ })
253
292
  }
254
293
 
255
- if ( (newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
256
- newConfig.runtime.xAxis = newConfig.yAxis;
257
- newConfig.runtime.yAxis = newConfig.xAxis;
258
- newConfig.runtime.horizontal = true;
294
+ if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
295
+ newConfig.runtime.xAxis = newConfig.yAxis
296
+ newConfig.runtime.yAxis = newConfig.xAxis
297
+ newConfig.runtime.horizontal = true
259
298
  } else {
260
- newConfig.runtime.xAxis = newConfig.xAxis;
261
- newConfig.runtime.yAxis = newConfig.yAxis;
262
- newConfig.runtime.horizontal = false;
299
+ newConfig.runtime.xAxis = newConfig.xAxis
300
+ newConfig.runtime.yAxis = newConfig.yAxis
301
+ newConfig.runtime.horizontal = false
263
302
  }
264
- newConfig.runtime.uniqueId = Date.now();
265
- newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey ? 'Data Key property in Y Axis section must be set for pie charts.' : '';
303
+ newConfig.runtime.uniqueId = Date.now()
304
+ newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey ? 'Data Key property in Y Axis section must be set for pie charts.' : ''
266
305
 
267
- setConfig(newConfig);
268
- };
306
+ setConfig(newConfig)
307
+ }
269
308
 
270
309
  const filterData = (filters, data) => {
271
- let filteredData = [];
310
+ let filteredData = []
272
311
 
273
- data.forEach((row) => {
274
- let add = true;
275
- filters.forEach((filter) => {
312
+ data.forEach(row => {
313
+ let add = true
314
+ filters.forEach(filter => {
276
315
  if (row[filter.columnName] !== filter.active) {
277
- add = false;
316
+ add = false
278
317
  }
279
- });
318
+ })
280
319
 
281
- if(add) filteredData.push(row);
282
- });
283
- return filteredData;
320
+ if (add) filteredData.push(row)
321
+ })
322
+ return filteredData
284
323
  }
285
324
 
286
325
  // Gets filer values from dataset
287
326
  const generateValuesForFilter = (columnName, data = this.state.data) => {
288
- const values = [];
327
+ const values = []
289
328
 
290
- data.forEach( (row) => {
291
- const value = row[columnName]
292
- if(value && false === values.includes(value)) {
293
- values.push(value)
294
- }
295
- });
329
+ data.forEach(row => {
330
+ const value = row[columnName]
331
+ if (value && false === values.includes(value)) {
332
+ values.push(value)
333
+ }
334
+ })
296
335
 
297
- return values;
336
+ return values
298
337
  }
299
338
 
300
339
  // Sorts data series for horizontal bar charts
301
340
  const sortData = (a, b) => {
302
- let sortKey = config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal' ? config.xAxis.dataKey : config.yAxis.sortKey;
303
- let aData = parseFloat(a[sortKey]);
304
- let bData = parseFloat(b[sortKey]);
305
-
306
- if(aData < bData){
307
- return config.sortData === 'ascending' ? 1 : -1;
308
- } else if (aData > bData){
309
- return config.sortData === 'ascending' ? -1 : 1;
341
+ let sortKey = config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal' ? config.xAxis.dataKey : config.yAxis.sortKey
342
+ let aData = parseFloat(a[sortKey])
343
+ let bData = parseFloat(b[sortKey])
344
+
345
+ if (aData < bData) {
346
+ return config.sortData === 'ascending' ? 1 : -1
347
+ } else if (aData > bData) {
348
+ return config.sortData === 'ascending' ? -1 : 1
310
349
  } else {
311
- return 0;
350
+ return 0
312
351
  }
313
352
  }
314
353
 
315
354
  // Observes changes to outermost container and changes viewport size in state
316
- const resizeObserver:ResizeObserver = new ResizeObserver(entries => {
355
+ const resizeObserver: ResizeObserver = new ResizeObserver(entries => {
317
356
  for (let entry of entries) {
318
357
  let { width, height } = entry.contentRect
319
358
  let newViewport = getViewport(width)
320
- let svgMarginWidth = 32;
321
- let editorWidth = 350;
359
+ let svgMarginWidth = 32
360
+ let editorWidth = 350
322
361
 
323
362
  setCurrentViewport(newViewport)
324
363
 
325
- if(isEditor) {
326
- width = width - editorWidth;
364
+ if (isEditor) {
365
+ width = width - editorWidth
327
366
  }
328
367
 
329
- if(entry.target.dataset.lollipop === 'true') {
330
- width = width - 2.5;
368
+ if (entry.target.dataset.lollipop === 'true') {
369
+ width = width - 2.5
331
370
  }
332
371
 
333
- width = width - svgMarginWidth;
372
+ width = width - svgMarginWidth
334
373
 
335
374
  setDimensions([width, height])
336
375
  }
@@ -338,374 +377,348 @@ export default function CdcChart(
338
377
 
339
378
  const outerContainerRef = useCallback(node => {
340
379
  if (node !== null) {
341
- resizeObserver.observe(node);
380
+ resizeObserver.observe(node)
342
381
  }
343
382
 
344
383
  setContainer(node)
345
- },[]);
384
+ }, [])
346
385
 
347
386
  function isEmpty(obj) {
348
- return Object.keys(obj).length === 0;
387
+ return Object.keys(obj).length === 0
349
388
  }
350
389
 
351
390
  // Load data when component first mounts
352
391
  useEffect(() => {
353
- loadConfig();
354
- }, []);
392
+ loadConfig()
393
+ }, [])
355
394
 
356
395
  /**
357
396
  * When cove has a config and container ref publish the cove_loaded event.
358
397
  */
359
398
  useEffect(() => {
360
- if(container && !isEmpty(config) && !coveLoadedEventRan) {
399
+ if (container && !isEmpty(config) && !coveLoadedEventRan) {
361
400
  publish('cove_loaded', { config: config })
362
401
  setCoveLoadedEventRan(true)
363
402
  }
403
+ }, [container, config])
364
404
 
365
- }, [container, config]);
366
-
367
-
368
- /**
369
- * Handles filter change events outside of COVE
370
- * Updates externalFilters state
371
- * Another useEffect listens to externalFilterChanges and updates the config.
372
- */
405
+ /**
406
+ * Handles filter change events outside of COVE
407
+ * Updates externalFilters state
408
+ * Another useEffect listens to externalFilterChanges and updates the config.
409
+ */
373
410
  useEffect(() => {
374
-
375
- const handleFilterData = (e:CustomEvent) => {
376
- let tmp = [];
377
- tmp.push(e.detail)
378
- setExternalFilters(tmp)
411
+ const handleFilterData = (e: CustomEvent) => {
412
+ let tmp = []
413
+ tmp.push(e.detail)
414
+ setExternalFilters(tmp)
379
415
  }
380
-
381
- subscribe('cove_filterData', (e:CustomEvent) => handleFilterData(e))
416
+
417
+ subscribe('cove_filterData', (e: CustomEvent) => handleFilterData(e))
382
418
 
383
419
  return () => {
384
- unsubscribe('cove_filterData', handleFilterData);
420
+ unsubscribe('cove_filterData', handleFilterData)
385
421
  }
422
+ }, [config])
386
423
 
387
- }, [config]);
388
-
389
-
390
- /**
391
- * Handles changes to externalFilters
392
- * For some reason e.detail is returning [order: "asc"] even though
393
- * we're not passing that in. The code here checks for an active prop instead of an empty array.
394
- */
424
+ /**
425
+ * Handles changes to externalFilters
426
+ * For some reason e.detail is returning [order: "asc"] even though
427
+ * we're not passing that in. The code here checks for an active prop instead of an empty array.
428
+ */
395
429
  useEffect(() => {
396
-
397
- if(externalFilters && externalFilters[0]) {
430
+ if (externalFilters && externalFilters[0]) {
398
431
  const hasActiveProperty = externalFilters[0].hasOwnProperty('active')
399
432
 
400
- if(!hasActiveProperty) {
401
- let configCopy = {...config }
433
+ if (!hasActiveProperty) {
434
+ let configCopy = { ...config }
402
435
  delete configCopy['filters']
403
436
  setConfig(configCopy)
404
- setFilteredData(filterData(externalFilters, excludedData));
437
+ setFilteredData(filterData(externalFilters, excludedData))
405
438
  }
406
439
  }
407
440
 
408
- if(externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
409
- let newConfigHere = {...config, filters: externalFilters }
441
+ if (externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
442
+ let newConfigHere = { ...config, filters: externalFilters }
410
443
  setConfig(newConfigHere)
411
- setFilteredData(filterData(externalFilters, excludedData));
444
+ setFilteredData(filterData(externalFilters, excludedData))
412
445
  }
413
-
414
- }, [externalFilters]);
446
+ }, [externalFilters])
415
447
 
416
-
417
448
  // Load data when configObj data changes
418
- if(configObj){
449
+ if (configObj) {
419
450
  useEffect(() => {
420
- loadConfig();
421
- }, [configObj.data]);
451
+ loadConfig()
452
+ }, [configObj.data])
422
453
  }
423
454
 
424
455
  // Generates color palette to pass to child chart component
425
456
  useEffect(() => {
426
- if(stateData && config.xAxis && config.runtime.seriesKeys) {
457
+ if (stateData && config.xAxis && config.runtime.seriesKeys) {
427
458
  let palette = config.customColors || colorPalettes[config.palette]
428
459
  let numberOfKeys = config.runtime.seriesKeys.length
429
- let newColorScale;
460
+ let newColorScale
430
461
 
431
- while(numberOfKeys > palette.length) {
432
- palette = palette.concat(palette);
462
+ while (numberOfKeys > palette.length) {
463
+ palette = palette.concat(palette)
433
464
  }
434
465
 
435
- palette = palette.slice(0, numberOfKeys);
466
+ palette = palette.slice(0, numberOfKeys)
436
467
 
437
- newColorScale = () => scaleOrdinal({
438
- domain: config.runtime.seriesLabelsAll,
439
- range: palette,
440
- });
468
+ newColorScale = () =>
469
+ scaleOrdinal({
470
+ domain: config.runtime.seriesLabelsAll,
471
+ range: palette
472
+ })
441
473
 
442
- setColorScale(newColorScale);
443
- setLoading(false);
474
+ setColorScale(newColorScale)
475
+ setLoading(false)
444
476
  }
445
477
 
446
- if(config && stateData && config.sortData){
447
- stateData.sort(sortData);
478
+ if (config && stateData && config.sortData) {
479
+ stateData.sort(sortData)
448
480
  }
449
481
  }, [config, stateData])
450
482
 
451
483
  // Called on legend click, highlights/unhighlights the data series with the given label
452
- const highlight = (label) => {
453
- const newSeriesHighlight = [];
484
+ const highlight = label => {
485
+ const newSeriesHighlight = []
454
486
 
455
487
  // If we're highlighting all the series, reset them
456
- if( (seriesHighlight.length + 1 === config.runtime.seriesKeys.length) && !config.legend.dynamicLegend) {
488
+ if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && !config.legend.dynamicLegend) {
457
489
  highlightReset()
458
490
  return
459
491
  }
460
492
 
461
- seriesHighlight.forEach((value) => {
462
- newSeriesHighlight.push(value);
463
- });
493
+ seriesHighlight.forEach(value => {
494
+ newSeriesHighlight.push(value)
495
+ })
464
496
 
465
- let newHighlight = label.datum;
466
- if(config.runtime.seriesLabels){
467
- for(let i = 0; i < config.runtime.seriesKeys.length; i++) {
468
- if(config.runtime.seriesLabels[config.runtime.seriesKeys[i]] === label.datum){
469
- newHighlight = config.runtime.seriesKeys[i];
470
- break;
497
+ let newHighlight = label.datum
498
+ if (config.runtime.seriesLabels) {
499
+ for (let i = 0; i < config.runtime.seriesKeys.length; i++) {
500
+ if (config.runtime.seriesLabels[config.runtime.seriesKeys[i]] === label.datum) {
501
+ newHighlight = config.runtime.seriesKeys[i]
502
+ break
471
503
  }
472
504
  }
473
505
  }
474
506
 
475
507
  if (newSeriesHighlight.indexOf(newHighlight) !== -1) {
476
- newSeriesHighlight.splice(newSeriesHighlight.indexOf(newHighlight), 1);
508
+ newSeriesHighlight.splice(newSeriesHighlight.indexOf(newHighlight), 1)
477
509
  } else {
478
- newSeriesHighlight.push(newHighlight);
510
+ newSeriesHighlight.push(newHighlight)
479
511
  }
480
- setSeriesHighlight(newSeriesHighlight);
481
- };
512
+ setSeriesHighlight(newSeriesHighlight)
513
+ }
482
514
 
483
515
  // Called on reset button click, unhighlights all data series
484
516
  const highlightReset = () => {
485
- if(config.legend.dynamicLegend && dynamicLegendItems) {
486
- setSeriesHighlight(dynamicLegendItems.map( item => item.text ))
517
+ if (config.legend.dynamicLegend && dynamicLegendItems) {
518
+ setSeriesHighlight(dynamicLegendItems.map(item => item.text))
487
519
  } else {
488
- setSeriesHighlight([]);
520
+ setSeriesHighlight([])
489
521
  }
490
522
  }
491
523
 
492
- const section = config.orientation ==='horizontal' ? 'yAxis' :'xAxis';
524
+ const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
493
525
 
494
526
  const parseDate = (dateString: string) => {
495
- let date = timeParse(config.runtime[section].dateParseFormat)(dateString);
496
- if(!date) {
497
- config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`;
498
- return new Date();
527
+ let date = timeParse(config.runtime[section].dateParseFormat)(dateString)
528
+ if (!date) {
529
+ config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`
530
+ return new Date()
499
531
  } else {
500
- return date;
532
+ return date
501
533
  }
502
- };
503
-
534
+ }
504
535
 
505
536
  const formatDate = (date: Date) => {
506
- return timeFormat(config.runtime[section].dateDisplayFormat)(date);
507
- };
537
+ return timeFormat(config.runtime[section].dateDisplayFormat)(date)
538
+ }
539
+
540
+ const DownloadButton = ({ data }: any, type = 'link') => {
541
+ const fileName = `${config.title.substring(0, 50)}.csv`
542
+
543
+ const csvData = Papa.unparse(data)
544
+
545
+ const saveBlob = () => {
546
+ //@ts-ignore
547
+ if (typeof window.navigator.msSaveBlob === 'function') {
548
+ const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
549
+ //@ts-ignore
550
+ window.navigator.msSaveBlob(dataBlob, fileName)
551
+ }
552
+ }
553
+
554
+ if (type === 'download') {
555
+ return (
556
+ <a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn btn-download no-border`}>
557
+ Download Data (CSV)
558
+ </a>
559
+ )
560
+ } else {
561
+ return (
562
+ <a download={fileName} onClick={saveBlob} href={`data:text/csv;base64,${Base64.encode(csvData)}`} aria-label='Download this data in a CSV file format.' className={`btn no-border`}>
563
+ Download Data (CSV)
564
+ </a>
565
+ )
566
+ }
567
+ }
508
568
 
509
569
  // Format numeric data based on settings in config
510
- const formatNumber = (num) => {
511
- // check if value contains comma and remove it. later will add comma below.
512
- if(String(num).indexOf(',') !== -1) num = num.replaceAll(',', '');
570
+ const formatNumber = (num, axis) => {
513
571
  // if num is NaN return num
514
- if(isNaN(num)|| !num) return num ;
572
+ if (isNaN(num) || !num) return num
515
573
 
516
- let original = num;
517
- let prefix = config.dataFormat.prefix;
574
+ // destructure dataFormat values
575
+ let {
576
+ dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, rightPrefix, rightSuffix }
577
+ } = config
578
+ let formatSuffix = format('.2s')
579
+
580
+ // check if value contains comma and remove it. later will add comma below.
581
+ if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
518
582
 
519
- let stringFormattingOptions = {
520
- useGrouping: config.dataFormat.commas ? true : false,
521
- minimumFractionDigits: config.dataFormat.roundTo ? Number(config.dataFormat.roundTo) : 0,
522
- maximumFractionDigits: config.dataFormat.roundTo ? Number(config.dataFormat.roundTo) : 0
523
- };
583
+ let original = num
584
+ let stringFormattingOptions
585
+
586
+ if (axis !== 'right') {
587
+ stringFormattingOptions = {
588
+ useGrouping: config.dataFormat.commas ? true : false,
589
+ minimumFractionDigits: roundTo ? Number(roundTo) : 0,
590
+ maximumFractionDigits: roundTo ? Number(roundTo) : 0
591
+ }
592
+ } else {
593
+ stringFormattingOptions = {
594
+ useGrouping: config.dataFormat.rightCommas ? true : false,
595
+ minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
596
+ maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
597
+ }
598
+ }
599
+
600
+ num = numberFromString(num)
524
601
 
525
- num = numberFromString(num);
526
-
527
602
  if (isNaN(num)) {
528
- config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`;
603
+ config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`
529
604
  return original
530
605
  }
531
606
 
532
- if (!config.dataFormat) return num;
533
- if (config.dataCutoff){
607
+ if (!config.dataFormat) return num
608
+ if (config.dataCutoff) {
534
609
  let cutoff = numberFromString(config.dataCutoff)
535
610
 
536
- if(num < cutoff) {
537
- num = cutoff;
611
+ if (num < cutoff) {
612
+ num = cutoff
538
613
  }
539
614
  }
540
- num = num.toLocaleString('en-US', stringFormattingOptions)
541
615
 
542
- let result = ""
543
-
544
- if(prefix) {
545
- result += prefix
616
+ // When we're formatting the left axis
617
+ // Use commas also updates bars and the data table
618
+ // We can't use commas when we're formatting the dataFormatted number
619
+ // Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
620
+ if (axis === 'left' && commas && abbreviated) {
621
+ num = num
622
+ } else {
623
+ num = num.toLocaleString('en-US', stringFormattingOptions)
546
624
  }
625
+ let result = ''
547
626
 
548
- result += num
549
-
550
- if(config.dataFormat.suffix) {
551
- result += config.dataFormat.suffix
627
+ if (abbreviated && axis === 'left') {
628
+ num = formatSuffix(parseFloat(num)).replace('G', 'B')
552
629
  }
553
- return String(result)
554
- };
555
-
556
- // Destructure items from config for more readable JSX
557
- const { legend, title, description, visualizationType } = config;
558
-
559
- // Select appropriate chart type
560
- const chartComponents = {
561
- 'Paired Bar' : <LinearChart />,
562
- 'Bar' : <LinearChart />,
563
- 'Line' : <LinearChart />,
564
- 'Combo': <LinearChart />,
565
- 'Pie' : <PieChart />,
566
- }
567
-
568
- const Filters = () => {
569
- const changeFilterActive = (index, value) => {
570
- let newFilters = config.filters;
571
-
572
- newFilters[index].active = value;
573
-
574
- setConfig({...config, filters: newFilters});
575
-
576
- setFilteredData(filterData(newFilters, excludedData));
577
- };
578
630
 
579
- const announceChange = (text) => {};
580
-
581
- let filterList = '';
582
- if (config.filters) {
583
-
584
- filterList = config.filters.map((singleFilter, index) => {
585
- const values = [];
586
- const sortAsc = (a, b) => {
587
- return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
588
- };
631
+ if (abbreviated && axis === 'bottom') {
632
+ num = formatSuffix(parseFloat(num)).replace('G', 'B')
633
+ }
589
634
 
590
- const sortDesc = (a, b) => {
591
- return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
592
- };
635
+ if (prefix && axis !== 'right') {
636
+ result += prefix
637
+ }
593
638
 
594
- if(!singleFilter.order || singleFilter.order === '' ){
595
- singleFilter.order = 'asc'
596
- }
639
+ if (rightPrefix && axis === 'right') {
640
+ result += rightPrefix
641
+ }
597
642
 
598
- if(singleFilter.order === 'desc') {
599
- singleFilter.values = singleFilter.values.sort(sortDesc)
600
- }
643
+ result += num
601
644
 
602
- if(singleFilter.order === 'asc') {
603
- singleFilter.values = singleFilter.values.sort(sortAsc)
604
- }
645
+ if (suffix && axis !== 'right') {
646
+ result += suffix
647
+ }
605
648
 
606
- singleFilter.values.forEach((filterOption, index) => {
607
- values.push(
608
- <option key={index} value={filterOption}>
609
- {filterOption}
610
- </option>
611
- );
612
- });
613
-
614
- return (
615
- <div className="single-filter" key={index}>
616
- <label htmlFor={`filter-${index}`}>{singleFilter.label}</label>
617
- <select
618
- id={`filter-${index}`}
619
- className="filter-select"
620
- data-index="0"
621
- value={singleFilter.active}
622
- onChange={(val) => {
623
- changeFilterActive(index, val.target.value);
624
- announceChange(`Filter ${singleFilter.label} value has been changed to ${val.target.value}, please reference the data table to see updated values.`);
625
- }}
626
- >
627
- {values}
628
- </select>
629
- </div>
630
- );
631
- });
649
+ if (rightSuffix && axis === 'right') {
650
+ result += rightSuffix
632
651
  }
633
652
 
634
- return (<section className="filters-section">{filterList}</section>)
653
+ return String(result)
654
+ }
655
+
656
+ // Select appropriate chart type
657
+ const chartComponents = {
658
+ 'Paired Bar': <LinearChart />,
659
+ Bar: <LinearChart />,
660
+ Line: <LinearChart />,
661
+ Combo: <LinearChart />,
662
+ Pie: <PieChart />,
663
+ 'Box Plot': <LinearChart />
635
664
  }
636
665
 
637
666
  const missingRequiredSections = () => {
638
667
  if (config.visualizationType === 'Pie') {
639
668
  if (undefined === config?.yAxis.dataKey) {
640
- return true;
669
+ return true
641
670
  }
642
671
  } else {
643
672
  if (undefined === config?.series || false === config?.series.length > 0) {
644
- return true;
673
+ return true
645
674
  }
646
675
  }
647
676
 
648
677
  if (!config.xAxis.dataKey) {
649
- return true;
678
+ return true
650
679
  }
651
680
 
652
- return false;
653
- };
681
+ return false
682
+ }
654
683
 
655
684
  // Prevent render if loading
656
- let body = (<Loading />)
657
-
658
-
659
- if(!loading) {
660
-
685
+ let body = <Loading />
661
686
 
687
+ if (!loading) {
662
688
  body = (
663
689
  <>
664
690
  {isEditor && <EditorPanel />}
665
691
  {!missingRequiredSections() && !config.newViz && (
666
- <div className="cdc-chart-inner-container">
692
+ <div className='cdc-chart-inner-container'>
667
693
  {/* Title */}
668
-
694
+
669
695
  {title && (
670
- <div
671
- role="heading"
672
- className={`chart-title ${config.theme} cove-component__header`}
673
- aria-level={2}
674
- >
675
- {config && (
676
- <sup className="superTitle">{parse(config.superTitle || '')}</sup>
677
- )}
696
+ <div role='heading' className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>
697
+ {config && <sup className='superTitle'>{parse(config.superTitle || '')}</sup>}
678
698
  <div>{parse(title)}</div>
679
699
  </div>
680
700
  )}
681
- <a
682
- id="skip-chart-container"
683
- className="cdcdataviz-sr-only-focusable"
684
- href={handleChartTabbing}
685
- >
701
+ <a id='skip-chart-container' className='cdcdataviz-sr-only-focusable' href={handleChartTabbing}>
686
702
  Skip Over Chart Container
687
703
  </a>
688
704
  {/* Filters */}
689
705
  {config.filters && !externalFilters && <Filters />}
690
706
  {/* Visualization */}
691
- {config?.introText && <section className="introText">{parse(config.introText)}</section>}
707
+ {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
692
708
  <div
693
- className={`chart-container${
694
- config.legend.hide ? " legend-hidden" : ""
695
- }${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
709
+ style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
710
+ className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
696
711
  >
697
712
  {/* All charts except sparkline */}
698
- {config.visualizationType !== "Spark Line" &&
699
- chartComponents[visualizationType]
700
- }
713
+ {config.visualizationType !== 'Spark Line' && chartComponents[visualizationType]}
701
714
 
702
715
  {/* Sparkline */}
703
- {config.visualizationType === "Spark Line" && (
716
+ {config.visualizationType === 'Spark Line' && (
704
717
  <>
705
- { description && <div className="subtext">{parse(description)}</div>}
718
+ {description && <div className='subtext'>{parse(description)}</div>}
706
719
  <div style={sparkLineStyles}>
707
720
  <ParentSize>
708
- {(parent) => (
721
+ {parent => (
709
722
  <>
710
723
  <SparkLine width={parent.width} height={parent.height} />
711
724
  </>
@@ -713,26 +726,32 @@ export default function CdcChart(
713
726
  </ParentSize>
714
727
  </div>
715
728
  </>
716
- )
717
- }
718
- {!config.legend.hide && config.visualizationType !== "Spark Line" && <Legend />}
729
+ )}
730
+ {!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
719
731
  </div>
720
732
  {/* Link */}
721
733
  {link && link}
722
734
  {/* Description */}
723
- {description && config.visualizationType !== "Spark Line" && <div className="subtext">{parse(description)}</div>}
724
- {/* Data Table */}
735
+ {description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
736
+
737
+ {/* buttons */}
738
+ <CoveMediaControls.Section classes={['download-buttons']}>
739
+ {config.table.showDownloadImgButton && <CoveMediaControls.Button text='Download Image' title='Download Chart as Image' type='image' state={config} elementToCapture={imageId} />}
740
+ {config.table.showDownloadPdfButton && <CoveMediaControls.Button text='Download PDF' title='Download Chart as PDF' type='pdf' state={config} elementToCapture={imageId} />}
741
+ </CoveMediaControls.Section>
725
742
 
726
- {config.xAxis.dataKey && config.table.show && config.visualizationType !== "Spark Line" && <DataTable />}
727
- {config?.footnotes && <section className="footnotes">{parse(config.footnotes)}</section>}
743
+ {/* Data Table */}
744
+ {config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && <DataTable />}
745
+ {config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
746
+ {/* show pdf or image button */}
728
747
  </div>
729
748
  )}
730
749
  </>
731
- );
750
+ )
732
751
  }
733
752
 
734
- const getXAxisData = (d: any) => config.runtime.xAxis.type === 'date' ? (parseDate(d[config.runtime.originalXAxis.dataKey])).getTime() : d[config.runtime.originalXAxis.dataKey];
735
- const getYAxisData = (d: any, seriesKey: string) => d[seriesKey];
753
+ const getXAxisData = (d: any) => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
754
+ const getYAxisData = (d: any, seriesKey: string) => d[seriesKey]
736
755
 
737
756
  const contextValues = {
738
757
  getXAxisData,
@@ -764,26 +783,22 @@ export default function CdcChart(
764
783
  legend,
765
784
  setSeriesHighlight,
766
785
  dynamicLegendItems,
767
- setDynamicLegendItems
786
+ setDynamicLegendItems,
787
+ filterData,
788
+ imageId
768
789
  }
769
790
 
770
- const classes = [
771
- 'cdc-open-viz-module',
772
- 'type-chart',
773
- `${currentViewport}`,
774
- `font-${config.fontSize}`,
775
- `${config.theme}`
776
- ]
791
+ const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
777
792
 
778
- config.visualizationType === "Spark Line" && classes.push(`type-sparkline`)
793
+ config.visualizationType === 'Spark Line' && classes.push(`type-sparkline`)
779
794
  isEditor && classes.push('spacing-wrapper')
780
795
  isEditor && classes.push('isEditor')
781
796
 
782
797
  return (
783
798
  <Context.Provider value={contextValues}>
784
- <div className={`${classes.join(' ')}`} ref={outerContainerRef} data-lollipop={config.isLollipopChart}>
799
+ <div className={`${classes.join(' ')}`} ref={outerContainerRef} data-lollipop={config.isLollipopChart} data-download-id={imageId}>
785
800
  {body}
786
801
  </div>
787
802
  </Context.Provider>
788
- );
803
+ )
789
804
  }