@cdc/chart 1.3.4 → 4.22.11

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