@cdc/chart 1.3.2 → 1.3.4

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