@cdc/chart 4.23.2 → 4.23.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 (94) hide show
  1. package/dist/cdcchart.js +42292 -40337
  2. package/examples/feature/__data__/area-chart.json +56 -0
  3. package/examples/{planet-example-data.json → feature/__data__/planet-example-data-max-increase.json} +4 -4
  4. package/examples/feature/__data__/planet-example-data.json +68 -0
  5. package/examples/feature/area/area-chart.json +244 -0
  6. package/examples/{example-bar-chart.json → feature/bar/example-bar-chart.json} +4 -1
  7. package/examples/feature/bar/horizontal-chart-max-increase.json +44 -0
  8. package/examples/{horizontal-chart.json → feature/bar/horizontal-chart.json} +10 -4
  9. package/examples/{horizontal-stacked-bar-chart.json → feature/bar/horizontal-stacked-bar-chart.json} +7 -3
  10. package/examples/{planet-chart-horizontal-example-config.json → feature/bar/planet-chart-horizontal-example-config.json} +8 -3
  11. package/examples/feature/bar/planet-example-config.json +156 -0
  12. package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -8
  13. package/examples/feature/boxplot/testing.csv +38 -0
  14. package/examples/feature/combo/combochart-categories_are_numbers .json +18 -0
  15. package/examples/{planet-combo-example-config.json → feature/combo/planet-combo-example-config.json} +1 -1
  16. package/examples/feature/deviation/planet-deviation-config.json +168 -0
  17. package/examples/feature/deviation/planet-deviation-data.json +38 -0
  18. package/examples/feature/filters/filter-testing.json +178 -0
  19. package/examples/feature/forecasting/case_date_example.csv +130 -0
  20. package/examples/feature/forecasting/effective_reproduction.json +202 -0
  21. package/examples/feature/forecasting/r_data.csv +130 -0
  22. package/examples/feature/line/line-chart-max-increase.json +32 -0
  23. package/examples/feature/line/line-chart.json +124 -0
  24. package/examples/{paired-bar-example.json → feature/paired-bar/paired-bar-example.json} +10 -4
  25. package/examples/{planet-pie-example-config.json → feature/pie/planet-pie-example-config.json} +2 -2
  26. package/examples/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +3 -3
  27. package/examples/{scatterplot.json → feature/scatterplot/scatterplot.json} +3 -3
  28. package/examples/feature/sparkline/example-sparkline.json +76 -0
  29. package/examples/feature/tests-big-small/big-small-test-bar.json +328 -0
  30. package/examples/feature/tests-big-small/big-small-test-line.json +328 -0
  31. package/examples/feature/tests-big-small/big-small-test-negative.json +328 -0
  32. package/examples/{case-rate-example-config.json → feature/tests-case-rate/case-rate-example-config.json} +2 -2
  33. package/examples/{covid-confidence-example-config.json → feature/tests-covid/covid-confidence-example-config.json} +8 -3
  34. package/examples/{covid-example-config.json → feature/tests-covid/covid-example-config.json} +7 -3
  35. package/examples/{cutoff-example-config.json → feature/tests-cutoff/cutoff-example-config.json} +7 -3
  36. package/examples/{date-exclusions-config.json → feature/tests-date-exclusions/date-exclusions-config.json} +2 -2
  37. package/examples/{example-bar-chart-nonnumeric.json → feature/tests-non-numerics/example-bar-chart-nonnumeric.json} +1 -1
  38. package/examples/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-nonnumeric.json} +5 -5
  39. package/examples/{planet-pie-example-config-nonnumeric.json → feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json} +2 -2
  40. package/examples/{sparkline-chart-nonnumeric.json → feature/tests-non-numerics/sparkline-chart-nonnumeric.json} +2 -2
  41. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
  42. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
  43. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
  44. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
  45. package/examples/gallery/line/line.json +1 -0
  46. package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
  47. package/index.html +76 -35
  48. package/package.json +6 -3
  49. package/src/CdcChart.jsx +245 -106
  50. package/src/components/AreaChart.jsx +233 -0
  51. package/src/components/BarChart.jsx +103 -62
  52. package/src/components/BoxPlot.jsx +39 -18
  53. package/src/components/DataTable.jsx +26 -21
  54. package/src/components/DeviationBar.jsx +191 -0
  55. package/src/components/EditorPanel.jsx +662 -298
  56. package/src/components/Legend.jsx +59 -46
  57. package/src/components/LineChart.jsx +12 -36
  58. package/src/components/LinearChart.jsx +163 -64
  59. package/src/components/PairedBarChart.jsx +6 -7
  60. package/src/components/PieChart.jsx +12 -17
  61. package/src/components/ScatterPlot.jsx +19 -16
  62. package/src/components/SparkLine.jsx +84 -118
  63. package/src/components/useIntersectionObserver.jsx +1 -1
  64. package/src/data/initial-state.js +27 -7
  65. package/src/hooks/useColorPalette.js +58 -48
  66. package/src/hooks/useReduceData.js +3 -4
  67. package/src/index.jsx +3 -2
  68. package/src/scss/editor-panel.scss +20 -0
  69. package/src/scss/main.scss +8 -6
  70. package/src/test/CdcChart.test.jsx +6 -0
  71. package/examples/box-plot.csv +0 -5
  72. package/examples/dynamic-legends.json +0 -125
  73. package/examples/line-chart.json +0 -34
  74. package/examples/planet-example-config.json +0 -37
  75. package/examples/temp-example-config.json +0 -64
  76. package/examples/temp-example-data.json +0 -130
  77. package/src/components/Filters.jsx +0 -125
  78. /package/examples/{age-adjusted-rates.json → feature/__data__/age-adjusted-rates.json} +0 -0
  79. /package/examples/{new-data.csv → feature/__data__/new-data.csv} +0 -0
  80. /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
  81. /package/examples/{stacked-vertical-bar-example-negative.json → feature/bar/stacked-vertical-bar-example-negative.json} +0 -0
  82. /package/examples/{stacked-vertical-bar-example.json → feature/bar/stacked-vertical-bar-example.json} +0 -0
  83. /package/examples/{box-plot-data.json → feature/boxplot/box-plot-data.json} +0 -0
  84. /package/examples/{newdata.json → feature/boxplot/boxplot-data.json} +0 -0
  85. /package/examples/{paired-bar-data.json → feature/paired-bar/paired-bar-data.json} +0 -0
  86. /package/examples/{paired-bar-formatted.json → feature/paired-bar/paired-bar-formatted.json} +0 -0
  87. /package/examples/{case-rate-example-data.json → feature/tests-case-rate/case-rate-example-data.json} +0 -0
  88. /package/examples/{covid-example-data-confidence.json → feature/tests-covid/covid-example-data-confidence.json} +0 -0
  89. /package/examples/{covid-example-data.json → feature/tests-covid/covid-example-data.json} +0 -0
  90. /package/examples/{cutoff-example-data.json → feature/tests-cutoff/cutoff-example-data.json} +0 -0
  91. /package/examples/{date-exclusions-data.json → feature/tests-date-exclusions/date-exclusions-data.json} +0 -0
  92. /package/examples/{example-combo-bar-nonnumeric.json → feature/tests-non-numerics/example-combo-bar-nonnumeric.json} +0 -0
  93. /package/examples/{planet-example-data-nonnumeric.json → feature/tests-non-numerics/planet-example-data-nonnumeric.json} +0 -0
  94. /package/examples/{stacked-vertical-bar-example-nonnumerics.json → feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json} +0 -0
package/src/CdcChart.jsx CHANGED
@@ -12,7 +12,6 @@ import { timeParse, timeFormat } from 'd3-time-format'
12
12
  import { format } from 'd3-format'
13
13
  import Papa from 'papaparse'
14
14
  import parse from 'html-react-parser'
15
- import { Base64 } from 'js-base64'
16
15
  import 'react-tooltip/dist/react-tooltip.css'
17
16
 
18
17
  // Primary Components
@@ -20,7 +19,7 @@ import ConfigContext from './ConfigContext'
20
19
  import PieChart from './components/PieChart'
21
20
  import LinearChart from './components/LinearChart'
22
21
 
23
- import { colorPalettesChart as colorPalettes } from '../../core/data/colorPalettes'
22
+ import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
24
23
 
25
24
  import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
26
25
 
@@ -32,7 +31,7 @@ import DataTable from './components/DataTable'
32
31
  import defaults from './data/initial-state'
33
32
  import EditorPanel from './components/EditorPanel'
34
33
  import Loading from '@cdc/core/components/Loading'
35
- import Filters from './components/Filters'
34
+ import Filters from '@cdc/core/components/Filters'
36
35
  import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
37
36
 
38
37
  // Helpers
@@ -41,13 +40,11 @@ import getViewport from '@cdc/core/helpers/getViewport'
41
40
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
42
41
  import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
43
42
  import isNumber from '@cdc/core/helpers/isNumber'
44
- import cleanData from '@cdc/core/helpers/cleanData'
45
43
 
46
44
  import './scss/main.scss'
47
45
 
48
- export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
46
+ export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDebug = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
49
47
  const transform = new DataTransform()
50
-
51
48
  const [loading, setLoading] = useState(true)
52
49
  const [colorScale, setColorScale] = useState(null)
53
50
  const [config, setConfig] = useState({})
@@ -63,17 +60,30 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
63
60
  const [dynamicLegendItems, setDynamicLegendItems] = useState([])
64
61
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
65
62
 
66
- const legendGlyphSize = 15
67
- const legendGlyphSizeHalf = legendGlyphSize / 2
68
-
69
63
  // Destructure items from config for more readable JSX
70
- const { legend, title, description, visualizationType } = config
71
- const { barBorderClass, lineDatapointClass, contentClasses, innerContainerClasses, sparkLineStyles } = useDataVizClasses(config)
64
+ let { legend, title, description, visualizationType } = config
65
+
66
+ // set defaults on titles if blank AND only in editor
67
+ if (isEditor) {
68
+ if (!title || title === '') title = 'Chart Title'
69
+ }
70
+
71
+ if (config.table && (!config.table?.label || config.table?.label === '')) config.table.label = 'Data Table'
72
+
73
+ const { barBorderClass, lineDatapointClass, contentClasses, sparkLineStyles } = useDataVizClasses(config)
72
74
 
73
75
  const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
74
76
 
77
+ const sortAsc = (a, b) => {
78
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
79
+ }
80
+
81
+ const sortDesc = (a, b) => {
82
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
83
+ }
84
+
75
85
  const handleChartAriaLabels = (state, testing = false) => {
76
- if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
86
+ if (testing) console.log(`handleChartAriaLabels Testing On:`, state) // eslint-disable-line
77
87
  try {
78
88
  if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
79
89
  let ariaLabel = ''
@@ -88,7 +98,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
88
98
 
89
99
  return ariaLabel
90
100
  } catch (e) {
91
- console.error(e.message)
101
+ console.error('COVE: ', e.message) // eslint-disable-line
102
+ }
103
+ }
104
+
105
+ const handleLineType = lineType => {
106
+ switch (lineType) {
107
+ case 'dashed-sm':
108
+ return '5 5'
109
+ case 'dashed-md':
110
+ return '10 5'
111
+ case 'dashed-lg':
112
+ return '15 5'
113
+ default:
114
+ return 0
92
115
  }
93
116
  }
94
117
 
@@ -120,7 +143,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
120
143
  data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
121
144
  }
122
145
  } catch {
123
- console.error(`Cannot parse URL: ${response.dataUrl}`)
146
+ console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
124
147
  data = []
125
148
  }
126
149
 
@@ -136,7 +159,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
136
159
  }
137
160
 
138
161
  let newConfig = { ...defaults, ...response }
162
+ if (newConfig.visualizationType === 'Box Plot') {
163
+ newConfig.legend.hide = true
164
+ }
139
165
  if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
166
+
140
167
  updateConfig(newConfig, data)
141
168
  }
142
169
 
@@ -188,11 +215,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
188
215
  newConfig.filters.forEach((filter, index) => {
189
216
  let filterValues = []
190
217
 
191
- filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData)
218
+ filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData).sort(filter.order === 'desc' ? sortDesc : sortAsc)
192
219
 
193
220
  newConfig.filters[index].values = filterValues
194
221
  // Initial filter should be active
195
222
  newConfig.filters[index].active = filterValues[0]
223
+ newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle ? newConfig.filters[index].filterStyle : 'dropdown'
196
224
  })
197
225
  currentData = filterData(newConfig.filters, newExcludedData)
198
226
  setFilteredData(currentData)
@@ -218,9 +246,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
218
246
  }
219
247
 
220
248
  if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
221
- // stats
222
- let allKeys = data.map(d => d[newConfig.xAxis.dataKey])
223
- let allValues = data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
249
+ let allKeys = newExcludedData ? newExcludedData.map(d => d[newConfig.xAxis.dataKey]) : data.map(d => d[newConfig.xAxis.dataKey])
250
+ let allValues = newExcludedData ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey])) : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
224
251
 
225
252
  const uniqueArray = function (arrArg) {
226
253
  return arrArg.filter(function (elem, pos, arr) {
@@ -232,39 +259,96 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
232
259
  let tableData = []
233
260
  const plots = []
234
261
 
262
+ /**
263
+ * Calculates the first quartile (q1) and third quartile (q3) from an array of integers or decimals.
264
+ *
265
+ * @param {Array} arr - The array of integers or decimals.
266
+ * @returns {Object} An object containing the q1 and q3 values.
267
+ */
268
+ const getQuartiles = arr => {
269
+ arr.sort((a, b) => a - b)
270
+
271
+ // Calculate the index of the median value of the array
272
+ const medianIndex = Math.floor(arr.length / 2)
273
+
274
+ // Check if the length of the array is even or odd
275
+ const isEvenLength = arr.length % 2 === 0
276
+
277
+ // Split the array into two subarrays based on the median index
278
+ const q1Array = isEvenLength ? arr.slice(0, medianIndex) : arr.slice(0, medianIndex + 1)
279
+ const q3Array = isEvenLength ? arr.slice(medianIndex) : arr.slice(medianIndex + 1)
280
+
281
+ // Calculate the median of the first subarray to get the q1 value
282
+ const q1Index = Math.floor(q1Array.length / 2)
283
+ const q1 = isEvenLength ? (q1Array[q1Index - 1] + q1Array[q1Index]) / 2 : q1Array[q1Index]
284
+
285
+ // Calculate the median of the second subarray to get the q3 value
286
+ const q3Index = Math.floor(q3Array.length / 2)
287
+ const q3 = isEvenLength ? (q3Array[q3Index - 1] + q3Array[q3Index]) / 2 : q3Array[q3Index]
288
+
289
+ // Return an object containing the q1 and q3 values
290
+ return { q1, q3 }
291
+ }
292
+
235
293
  // group specific statistics
236
294
  // prevent re-renders
295
+ if (!groups) return
237
296
  groups.forEach((g, index) => {
238
- if (!g) return
239
- // filter data by group
240
- let filteredData = data.filter(item => item[newConfig.xAxis.dataKey] === g)
241
- let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
242
- // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
243
- const q1 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.firstQuartilePercentage) / 100)
244
- const q3 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.thirdQuartilePercentage) / 100)
245
- const iqr = q3 - q1
246
- const lowerBounds = q1 - (q3 - q1) * 1.5
247
- const upperBounds = q3 + (q3 - q1) * 1.5
248
- const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
249
- let nonOutliers = filteredDataValues
250
-
251
- nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
252
-
253
- plots.push({
254
- columnCategory: g,
255
- columnMax: Number(q3 + 1.5 * iqr).toFixed(2),
256
- columnThirdQuartile: q3.toFixed(2),
257
- columnMedian: d3.median(filteredDataValues),
258
- columnFirstQuartile: q1.toFixed(2),
259
- columnMin: Number(q1 - 1.5 * iqr).toFixed(2),
260
- columnCount: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
261
- columnSd: d3.deviation(filteredDataValues).toFixed(2),
262
- columnMean: d3.mean(filteredDataValues).toFixed(2),
263
- columnIqr: iqr.toFixed(2),
264
- columnOutliers: outliers,
265
- values: filteredDataValues,
266
- nonOutlierValues: nonOutliers
267
- })
297
+ try {
298
+ if (!g) throw new Error('No groups resolved in box plots')
299
+
300
+ // filter data by group
301
+ let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
302
+ let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
303
+
304
+ // Sort the data for upcoming functions.
305
+ let sortedData = filteredDataValues.sort((a, b) => a - b)
306
+
307
+ // ! - Notice d3.quantile doesn't work here, and we had to take a custom route.
308
+ const quartiles = getQuartiles(sortedData)
309
+
310
+ if (!filteredData) throw new Error('boxplots dont have data yet')
311
+ if (!plots) throw new Error('boxplots dont have plots yet')
312
+
313
+ if (newConfig.boxplot.firstQuartilePercentage === '') {
314
+ newConfig.boxplot.firstQuartilePercentage = 0
315
+ }
316
+
317
+ if (newConfig.boxplot.thirdQuartilePercentage === '') {
318
+ newConfig.boxplot.thirdQuartilePercentage = 0
319
+ }
320
+
321
+ const q1 = quartiles.q1
322
+ const q3 = quartiles.q3
323
+ const iqr = q3 - q1
324
+ const lowerBounds = q1 - (q3 - q1) * 1.5
325
+ const upperBounds = q3 + (q3 - q1) * 1.5
326
+
327
+ const outliers = sortedData.filter(v => v < lowerBounds || v > upperBounds)
328
+ let nonOutliers = filteredDataValues
329
+
330
+ nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
331
+
332
+ plots.push({
333
+ columnCategory: g,
334
+ columnMax: d3.min([d3.max(filteredDataValues), q1 + 1.5 * iqr]),
335
+ columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
336
+ columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
337
+ columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
338
+ columnMin: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
339
+ columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
340
+ columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
341
+ columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
342
+ columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
343
+ columnLowerBounds: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
344
+ columnUpperBounds: d3.min([d3.max(sortedData), q1 + 1.5 * iqr]),
345
+ columnOutliers: outliers,
346
+ values: filteredDataValues,
347
+ nonOutlierValues: nonOutliers
348
+ })
349
+ } catch (e) {
350
+ console.error('COVE: ', e.message) // eslint-disable-line
351
+ }
268
352
  })
269
353
 
270
354
  // make deep copy so we can remove some fields for data
@@ -273,6 +357,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
273
357
  tableData.map(table => {
274
358
  delete table.columnIqr
275
359
  delete table.nonOutlierValues
360
+ delete table.columnLowerBounds
361
+ delete table.columnUpperBounds
362
+ return null // resolve eslint
276
363
  })
277
364
 
278
365
  // any other data we can add to boxplots
@@ -285,7 +372,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
285
372
  if (newConfig.visualizationType === 'Combo' && newConfig.series) {
286
373
  newConfig.runtime.barSeriesKeys = []
287
374
  newConfig.runtime.lineSeriesKeys = []
375
+ newConfig.runtime.areaSeriesKeys = []
376
+
288
377
  newConfig.series.forEach(series => {
378
+ if (series.type === 'Area Chart') {
379
+ newConfig.runtime.areaSeriesKeys.push(series)
380
+ }
289
381
  if (series.type === 'Bar') {
290
382
  newConfig.runtime.barSeriesKeys.push(series.dataKey)
291
383
  }
@@ -294,8 +386,17 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
294
386
  }
295
387
  })
296
388
  }
389
+ if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
390
+ newConfig.runtime.areaSeriesKeys = []
391
+
392
+ newConfig.series.forEach(series => {
393
+ if (series.type === 'Area Chart') {
394
+ newConfig.runtime.areaSeriesKeys.push(series)
395
+ }
396
+ })
397
+ }
297
398
 
298
- if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
399
+ if (((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Deviation Bar') && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
299
400
  newConfig.runtime.xAxis = newConfig.yAxis
300
401
  newConfig.runtime.yAxis = newConfig.xAxis
301
402
  newConfig.runtime.horizontal = true
@@ -326,7 +427,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
326
427
  return filteredData
327
428
  }
328
429
 
329
- // Gets filer values from dataset
430
+ // Gets filter values from dataset
330
431
  const generateValuesForFilter = (columnName, data = this.state.data) => {
331
432
  const values = []
332
433
 
@@ -386,7 +487,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
386
487
  }
387
488
 
388
489
  setContainer(node)
389
- }, [])
490
+ }, []) // eslint-disable-line
390
491
 
391
492
  function isEmpty(obj) {
392
493
  return Object.keys(obj).length === 0
@@ -395,7 +496,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
395
496
  // Load data when component first mounts
396
497
  useEffect(() => {
397
498
  loadConfig()
398
- }, [])
499
+ }, []) // eslint-disable-line
399
500
 
400
501
  /**
401
502
  * When cove has a config and container ref publish the cove_loaded event.
@@ -405,7 +506,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
405
506
  publish('cove_loaded', { config: config })
406
507
  setCoveLoadedEventRan(true)
407
508
  }
408
- }, [container, config])
509
+ }, [container, config]) // eslint-disable-line
409
510
 
410
511
  /**
411
512
  * Handles filter change events outside of COVE
@@ -448,20 +549,22 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
448
549
  setConfig(newConfigHere)
449
550
  setFilteredData(filterData(externalFilters, excludedData))
450
551
  }
451
- }, [externalFilters])
552
+ }, [externalFilters]) // eslint-disable-line
452
553
 
453
554
  // Load data when configObj data changes
454
555
  if (configObj) {
455
556
  // eslint-disable-next-line react-hooks/rules-of-hooks
456
557
  useEffect(() => {
457
558
  loadConfig()
458
- }, [configObj.data])
559
+ }, [configObj.data]) // eslint-disable-line
459
560
  }
460
561
 
461
562
  // Generates color palette to pass to child chart component
462
563
  useEffect(() => {
463
564
  if (stateData && config.xAxis && config.runtime.seriesKeys) {
464
- let palette = config.customColors || colorPalettes[config.palette]
565
+ const configPalette = config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
566
+ const allPalettes = { ...colorPalettes, ...twoColorPalette }
567
+ let palette = config.customColors || allPalettes[configPalette]
465
568
  let numberOfKeys = config.runtime.seriesKeys.length
466
569
  let newColorScale
467
570
 
@@ -484,7 +587,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
484
587
  if (config && stateData && config.sortData) {
485
588
  stateData.sort(sortData)
486
589
  }
487
- }, [config, stateData])
590
+ }, [config, stateData]) // eslint-disable-line
488
591
 
489
592
  // Called on legend click, highlights/unhighlights the data series with the given label
490
593
  const highlight = label => {
@@ -543,33 +646,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
543
646
  return timeFormat(config.runtime[section].dateDisplayFormat)(date)
544
647
  }
545
648
 
546
- const DownloadButton = ({ data }, type = 'link') => {
547
- const fileName = `${config.title.substring(0, 50)}.csv`
548
-
549
- const csvData = Papa.unparse(data)
550
-
551
- const saveBlob = () => {
552
- if (typeof window.navigator.msSaveBlob === 'function') {
553
- const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
554
- window.navigator.msSaveBlob(dataBlob, fileName)
555
- }
556
- }
557
-
558
- if (type === 'download') {
559
- return (
560
- <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`}>
561
- Download Data (CSV)
562
- </a>
563
- )
564
- } else {
565
- return (
566
- <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`}>
567
- Download Data (CSV)
568
- </a>
569
- )
570
- }
571
- }
572
-
573
649
  // function calculates the width of given text and its font-size
574
650
  function getTextWidth(text, font) {
575
651
  const canvas = document.createElement('canvas')
@@ -580,30 +656,55 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
580
656
  return Math.ceil(context.measureText(text).width)
581
657
  }
582
658
 
659
+ const abbreviateNumber = num => {
660
+ let unit = ''
661
+ let absNum = Math.abs(num)
662
+
663
+ if (absNum >= 1e9) {
664
+ unit = 'B'
665
+ num = num / 1e9
666
+ } else if (absNum >= 1e6) {
667
+ unit = 'M'
668
+ num = num / 1e6
669
+ } else if (absNum >= 1e3) {
670
+ unit = 'K'
671
+ num = num / 1e3
672
+ }
673
+
674
+ return num + unit
675
+ }
676
+
583
677
  // Format numeric data based on settings in config
584
- const formatNumber = (num, axis) => {
678
+ const formatNumber = (num, axis, shouldAbbreviate = false) => {
585
679
  // if num is NaN return num
586
680
  if (isNaN(num) || !num) return num
681
+ // Check if the input number is negative
682
+ const isNegative = num < 0
683
+
684
+ // If the input number is negative, take the absolute value
685
+ if (isNegative) {
686
+ num = Math.abs(num)
687
+ }
587
688
 
588
689
  // destructure dataFormat values
589
690
  let {
590
- dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, rightPrefix, rightSuffix }
691
+ dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
591
692
  } = config
592
- let formatSuffix = format('.2s')
593
693
 
594
694
  // check if value contains comma and remove it. later will add comma below.
595
695
  if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
596
696
 
597
697
  let original = num
598
698
  let stringFormattingOptions
599
-
600
- if (axis !== 'right') {
699
+ if (axis === 'left') {
601
700
  stringFormattingOptions = {
602
701
  useGrouping: config.dataFormat.commas ? true : false,
603
702
  minimumFractionDigits: roundTo ? Number(roundTo) : 0,
604
703
  maximumFractionDigits: roundTo ? Number(roundTo) : 0
605
704
  }
606
- } else {
705
+ }
706
+
707
+ if (axis === 'right') {
607
708
  stringFormattingOptions = {
608
709
  useGrouping: config.dataFormat.rightCommas ? true : false,
609
710
  minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
@@ -611,6 +712,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
611
712
  }
612
713
  }
613
714
 
715
+ if (axis === 'bottom') {
716
+ stringFormattingOptions = {
717
+ useGrouping: config.dataFormat.bottomCommas ? true : false,
718
+ minimumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0,
719
+ maximumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0
720
+ }
721
+ }
722
+
614
723
  num = numberFromString(num)
615
724
 
616
725
  if (isNaN(num)) {
@@ -631,22 +740,26 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
631
740
  // Use commas also updates bars and the data table
632
741
  // We can't use commas when we're formatting the dataFormatted number
633
742
  // Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
634
- if ((axis === 'left' && commas && abbreviated) || (axis === 'bottom' && commas && abbreviated)) {
635
- num = num
743
+ //
744
+ // Edge case for small numbers with decimals
745
+ // - if roundTo undefined which means it is blank, then do not round
746
+
747
+ if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
748
+ num = num // eslint-disable-line
636
749
  } else {
637
750
  num = num.toLocaleString('en-US', stringFormattingOptions)
638
751
  }
639
752
  let result = ''
640
753
 
641
- if (abbreviated && axis === 'left') {
642
- num = formatSuffix(parseFloat(num)).replace('G', 'B')
754
+ if (abbreviated && axis === 'left' && shouldAbbreviate) {
755
+ num = abbreviateNumber(parseFloat(num))
643
756
  }
644
757
 
645
- if (abbreviated && axis === 'bottom') {
646
- num = formatSuffix(parseFloat(num)).replace('G', 'B')
758
+ if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
759
+ num = abbreviateNumber(parseFloat(num))
647
760
  }
648
761
 
649
- if (prefix && axis !== 'right') {
762
+ if (prefix && axis === 'left') {
650
763
  result += prefix
651
764
  }
652
765
 
@@ -654,9 +767,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
654
767
  result += rightPrefix
655
768
  }
656
769
 
770
+ if (bottomPrefix && axis === 'bottom') {
771
+ result += bottomPrefix
772
+ }
773
+
657
774
  result += num
658
775
 
659
- if (suffix && axis !== 'right') {
776
+ if (suffix && axis === 'left') {
660
777
  result += suffix
661
778
  }
662
779
 
@@ -664,6 +781,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
664
781
  result += rightSuffix
665
782
  }
666
783
 
784
+ if (bottomSuffix && axis === 'bottom') {
785
+ result += bottomSuffix
786
+ }
787
+ if (isNegative) {
788
+ result = '-' + result
789
+ }
790
+
667
791
  return String(result)
668
792
  }
669
793
 
@@ -675,7 +799,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
675
799
  Combo: <LinearChart />,
676
800
  Pie: <PieChart />,
677
801
  'Box Plot': <LinearChart />,
678
- 'Scatter Plot': <LinearChart />
802
+ 'Area Chart': <LinearChart />,
803
+ 'Scatter Plot': <LinearChart />,
804
+ 'Deviation Bar': <LinearChart />
679
805
  }
680
806
 
681
807
  const missingRequiredSections = () => {
@@ -696,10 +822,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
696
822
  return false
697
823
  }
698
824
 
825
+ const clean = data => {
826
+ return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
827
+ }
828
+
699
829
  // Prevent render if loading
700
830
  let body = <Loading />
701
831
 
702
832
  if (!loading) {
833
+ const tableLink = (
834
+ <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
835
+ {config.dataKey} (Go to Table)
836
+ </a>
837
+ )
703
838
  body = (
704
839
  <>
705
840
  {isEditor && <EditorPanel />}
@@ -707,7 +842,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
707
842
  <div className='cdc-chart-inner-container'>
708
843
  {/* Title */}
709
844
 
710
- {title && (
845
+ {title && config.showTitle && (
711
846
  <div role='heading' className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>
712
847
  {config && <sup className='superTitle'>{parse(config.superTitle || '')}</sup>}
713
848
  <div>{parse(title)}</div>
@@ -717,7 +852,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
717
852
  Skip Over Chart Container
718
853
  </a>
719
854
  {/* Filters */}
720
- {config.filters && !externalFilters && <Filters />}
855
+ {config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} isNumber={isNumber} dimensions={dimensions} />}
721
856
  {/* Visualization */}
722
857
  {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
723
858
  <div
@@ -745,7 +880,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
745
880
  {!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
746
881
  </div>
747
882
  {/* Link */}
748
- {link && link}
883
+ {isDashboard && config.table && config.table.show && config.table.showDataTableLink ? tableLink : link && link}
884
+
749
885
  {/* Description */}
750
886
  {description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
751
887
 
@@ -775,7 +911,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
775
911
  setConfig,
776
912
  rawData: stateData ?? {},
777
913
  excludedData: excludedData,
778
- transformedData: filteredData || excludedData,
914
+ transformedData: clean(filteredData || excludedData), // do this right before passing to components
915
+ tableData: filteredData || excludedData, // do not clean table data
779
916
  unfilteredData: stateData,
780
917
  seriesHighlight,
781
918
  colorScale,
@@ -800,10 +937,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
800
937
  dynamicLegendItems,
801
938
  setDynamicLegendItems,
802
939
  filterData,
803
- isNumber,
804
- cleanData,
805
940
  imageId,
806
- getTextWidth
941
+ handleLineType,
942
+ isNumber,
943
+ getTextWidth,
944
+ twoColorPalette,
945
+ isDebug
807
946
  }
808
947
 
809
948
  const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]