@cdc/chart 4.23.3 → 4.23.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/cdcchart.js +52543 -50830
  2. package/examples/feature/__data__/area-chart.json +56 -0
  3. package/examples/{planet-example-data.json → feature/__data__/planet-example-data.json} +3 -8
  4. package/examples/feature/__data__/planet-logaritmic-data.json +56 -0
  5. package/examples/feature/area/area-chart-category.json +240 -0
  6. package/examples/{area-chart.json → feature/area/area-chart-date.json} +70 -13
  7. package/examples/feature/bar/example-bar-chart.json +558 -0
  8. package/examples/{horizontal-chart-max-increase.json → feature/bar/horizontal-chart-max-increase.json} +10 -4
  9. package/examples/{horizontal-chart.json → feature/bar/horizontal-chart.json} +10 -4
  10. package/examples/{horizontal-stacked-bar-chart.json → feature/bar/horizontal-stacked-bar-chart.json} +7 -3
  11. package/examples/{planet-chart-horizontal-example-config.json → feature/bar/planet-chart-horizontal-example-config.json} +8 -3
  12. package/examples/feature/bar/planet-chart-logaritmic-config.json +170 -0
  13. package/examples/{planet-example-config.json → feature/bar/planet-example-config.json} +2 -2
  14. package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -7
  15. package/examples/feature/boxplot/testing.csv +38 -0
  16. package/examples/feature/boxplot/valid-boxplot.csv +17 -0
  17. package/examples/feature/combo/combochart-categories_are_numbers .json +18 -0
  18. package/examples/{planet-combo-example-config.json → feature/combo/planet-combo-example-config.json} +1 -1
  19. package/examples/{planet-deviation-config.json → feature/deviation/planet-deviation-config.json} +2 -2
  20. package/examples/{planet-deviation-data.json → feature/deviation/planet-deviation-data.json} +9 -9
  21. package/examples/feature/filters/filter-testing.json +212 -0
  22. package/examples/feature/forecasting/case_date_example.csv +130 -0
  23. package/examples/feature/forecasting/effective_reproduction.json +202 -0
  24. package/examples/feature/forecasting/r_data.csv +130 -0
  25. package/examples/feature/forecasting/random_data.csv +366 -0
  26. package/examples/feature/line/line-chart.json +124 -0
  27. package/examples/{paired-bar-example.json → feature/paired-bar/paired-bar-example.json} +10 -4
  28. package/examples/{planet-pie-example-config.json → feature/pie/planet-pie-example-config.json} +2 -2
  29. package/examples/{scatterplot.json → feature/scatterplot/scatterplot.json} +1 -1
  30. package/examples/feature/test-highlight/test-highlight-2.json +789 -0
  31. package/examples/feature/test-highlight/test-highlight-vertical.json +561 -0
  32. package/examples/feature/test-highlight/test-highlight.json +100 -0
  33. package/examples/{case-rate-example-config.json → feature/tests-case-rate/case-rate-example-config.json} +2 -2
  34. package/examples/{covid-confidence-example-config.json → feature/tests-covid/covid-confidence-example-config.json} +8 -3
  35. package/examples/{covid-example-config.json → feature/tests-covid/covid-example-config.json} +7 -3
  36. package/examples/{cutoff-example-config.json → feature/tests-cutoff/cutoff-example-config.json} +7 -3
  37. package/examples/{date-exclusions-config.json → feature/tests-date-exclusions/date-exclusions-config.json} +2 -2
  38. package/examples/{example-bar-chart-nonnumeric.json → feature/tests-non-numerics/example-bar-chart-nonnumeric.json} +1 -1
  39. package/examples/{planet-pie-example-config-nonnumeric.json → feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json} +2 -2
  40. package/examples/{sparkline-chart-nonnumeric.json → feature/tests-non-numerics/sparkline-chart-nonnumeric.json} +1 -1
  41. package/examples/{stacked-vertical-bar-example-nonnumerics.json → feature/tests-non-numerics/stacked-vertical-bar-example-nonnumerics.json} +1 -2
  42. package/examples/gallery/bar-chart-horizontal/horizontal-highlight.json +345 -0
  43. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
  44. package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
  45. package/index.html +73 -49
  46. package/package.json +2 -2
  47. package/src/CdcChart.jsx +405 -40
  48. package/src/components/AreaChart.jsx +122 -80
  49. package/src/components/BarChart.jsx +126 -49
  50. package/src/components/BoxPlot.jsx +28 -20
  51. package/src/components/DataTable.jsx +7 -6
  52. package/src/components/DeviationBar.jsx +34 -34
  53. package/src/components/EditorPanel.jsx +1332 -352
  54. package/src/components/Legend.jsx +40 -4
  55. package/src/components/LineChart.jsx +10 -23
  56. package/src/components/LinearChart.jsx +133 -286
  57. package/src/components/PairedBarChart.jsx +6 -6
  58. package/src/components/PieChart.jsx +2 -4
  59. package/src/components/SparkLine.jsx +6 -42
  60. package/src/data/initial-state.js +23 -4
  61. package/src/hooks/useHighlightedBars.js +154 -0
  62. package/src/hooks/useMinMax.js +92 -0
  63. package/src/hooks/useReduceData.js +31 -57
  64. package/src/hooks/useScales.js +202 -0
  65. package/src/index.jsx +2 -1
  66. package/src/scss/editor-panel.scss +15 -0
  67. package/src/scss/main.scss +8 -6
  68. package/examples/box-plot.csv +0 -5
  69. package/examples/dynamic-legends.json +0 -125
  70. package/examples/example-bar-chart.json +0 -36
  71. package/examples/line-chart.json +0 -34
  72. package/examples/temp-example-config.json +0 -64
  73. package/examples/temp-example-data.json +0 -130
  74. package/src/components/Filters.jsx +0 -126
  75. /package/examples/{age-adjusted-rates.json → feature/__data__/age-adjusted-rates.json} +0 -0
  76. /package/examples/{new-data.csv → feature/__data__/new-data.csv} +0 -0
  77. /package/examples/{planet-example-data-max-increase.json → feature/__data__/planet-example-data-max-increase.json} +0 -0
  78. /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
  79. /package/examples/{stacked-vertical-bar-example-negative.json → feature/bar/stacked-vertical-bar-example-negative.json} +0 -0
  80. /package/examples/{stacked-vertical-bar-example.json → feature/bar/stacked-vertical-bar-example.json} +0 -0
  81. /package/examples/{box-plot-data.json → feature/boxplot/box-plot-data.json} +0 -0
  82. /package/examples/{newdata.json → feature/boxplot/boxplot-data.json} +0 -0
  83. /package/examples/{line-chart-max-increase.json → feature/line/line-chart-max-increase.json} +0 -0
  84. /package/examples/{paired-bar-data.json → feature/paired-bar/paired-bar-data.json} +0 -0
  85. /package/examples/{paired-bar-formatted.json → feature/paired-bar/paired-bar-formatted.json} +0 -0
  86. /package/examples/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +0 -0
  87. /package/examples/{example-sparkline.json → feature/sparkline/example-sparkline.json} +0 -0
  88. /package/examples/{big-small-test-bar.json → feature/tests-big-small/big-small-test-bar.json} +0 -0
  89. /package/examples/{big-small-test-line.json → feature/tests-big-small/big-small-test-line.json} +0 -0
  90. /package/examples/{big-small-test-negative.json → feature/tests-big-small/big-small-test-negative.json} +0 -0
  91. /package/examples/{case-rate-example-data.json → feature/tests-case-rate/case-rate-example-data.json} +0 -0
  92. /package/examples/{covid-example-data-confidence.json → feature/tests-covid/covid-example-data-confidence.json} +0 -0
  93. /package/examples/{covid-example-data.json → feature/tests-covid/covid-example-data.json} +0 -0
  94. /package/examples/{cutoff-example-data.json → feature/tests-cutoff/cutoff-example-data.json} +0 -0
  95. /package/examples/{date-exclusions-data.json → feature/tests-date-exclusions/date-exclusions-data.json} +0 -0
  96. /package/examples/{example-combo-bar-nonnumeric.json → feature/tests-non-numerics/example-combo-bar-nonnumeric.json} +0 -0
  97. /package/examples/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-nonnumeric.json} +0 -0
  98. /package/examples/{planet-example-data-nonnumeric.json → feature/tests-non-numerics/planet-example-data-nonnumeric.json} +0 -0
package/src/CdcChart.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback } from 'react'
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react'
2
2
 
3
3
  // IE11
4
4
  import ResizeObserver from 'resize-observer-polyfill'
@@ -9,10 +9,10 @@ import * as d3 from 'd3-array'
9
9
  import { scaleOrdinal } from '@visx/scale'
10
10
  import ParentSize from '@visx/responsive/lib/components/ParentSize'
11
11
  import { timeParse, timeFormat } from 'd3-time-format'
12
- import { format } from 'd3-format'
13
12
  import Papa from 'papaparse'
14
13
  import parse from 'html-react-parser'
15
14
  import 'react-tooltip/dist/react-tooltip.css'
15
+ import chroma from 'chroma-js'
16
16
 
17
17
  // Primary Components
18
18
  import ConfigContext from './ConfigContext'
@@ -27,11 +27,10 @@ import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
27
27
 
28
28
  import SparkLine from './components/SparkLine'
29
29
  import Legend from './components/Legend'
30
- import DataTable from './components/DataTable'
31
30
  import defaults from './data/initial-state'
32
31
  import EditorPanel from './components/EditorPanel'
33
32
  import Loading from '@cdc/core/components/Loading'
34
- import Filters from './components/Filters'
33
+ import Filters from '@cdc/core/components/Filters'
35
34
  import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
36
35
 
37
36
  // Helpers
@@ -40,11 +39,40 @@ import getViewport from '@cdc/core/helpers/getViewport'
40
39
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
41
40
  import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
42
41
  import isNumber from '@cdc/core/helpers/isNumber'
43
- import cleanData from '@cdc/core/helpers/cleanData'
44
42
 
45
43
  import './scss/main.scss'
44
+ // load both then config below determines which to use
45
+ import DataTable_horiz from './components/DataTable'
46
+ import DataTable_vert from '@cdc/core/components/DataTable'
46
47
 
47
- export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
48
+ const generateColorsArray = (color = '#000000', special = false) => {
49
+ let colorObj = chroma(color)
50
+ let hoverColor = special ? colorObj.brighten(0.5).hex() : colorObj.saturate(1.3).hex()
51
+
52
+ return [color, hoverColor, colorObj.darken(0.3).hex()]
53
+ }
54
+ const hashObj = row => {
55
+ try {
56
+ if (!row) throw new Error('No row supplied to hashObj')
57
+
58
+ let str = JSON.stringify(row)
59
+ let hash = 0
60
+
61
+ if (str.length === 0) return hash
62
+
63
+ for (let i = 0; i < str.length; i++) {
64
+ let char = str.charCodeAt(i)
65
+ hash = (hash << 5) - hash + char
66
+ hash = hash & hash
67
+ }
68
+
69
+ return hash
70
+ } catch (e) {
71
+ console.error('COVE: ', e) // eslint-disable-line
72
+ }
73
+ }
74
+
75
+ export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDebug = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
48
76
  const transform = new DataTransform()
49
77
  const [loading, setLoading] = useState(true)
50
78
  const [colorScale, setColorScale] = useState(null)
@@ -61,6 +89,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
61
89
  const [dynamicLegendItems, setDynamicLegendItems] = useState([])
62
90
  const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
63
91
 
92
+ let legendMemo = useRef(new Map()) // map collection
93
+ let innerContainerRef = useRef()
94
+
95
+ if (isDebug) console.log('Chart config', config)
96
+
97
+ const DataTable = config?.table?.showVertical ? DataTable_vert : DataTable_horiz
98
+
64
99
  // Destructure items from config for more readable JSX
65
100
  let { legend, title, description, visualizationType } = config
66
101
 
@@ -84,7 +119,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
84
119
  }
85
120
 
86
121
  const handleChartAriaLabels = (state, testing = false) => {
87
- if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
122
+ if (testing) console.log(`handleChartAriaLabels Testing On:`, state) // eslint-disable-line
88
123
  try {
89
124
  if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
90
125
  let ariaLabel = ''
@@ -103,26 +138,121 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
103
138
  }
104
139
  }
105
140
 
141
+ const reloadURLData = async () => {
142
+ if (config.dataUrl) {
143
+ const dataUrl = new URL(config.runtimeDataUrl || config.dataUrl)
144
+ let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
145
+
146
+ let isUpdateNeeded = false
147
+ config.filters.forEach(filter => {
148
+ if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
149
+ qsParams[filter.queryParameter] = filter.active
150
+ isUpdateNeeded = true
151
+ }
152
+ })
153
+
154
+ if ((!config.formattedData || config.formattedData.urlFiltered) && !isUpdateNeeded) return
155
+
156
+ let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
157
+ .map((param, i) => {
158
+ let qs = i === 0 ? '?' : '&'
159
+ qs += param + '='
160
+ qs += qsParams[param]
161
+ return qs
162
+ })
163
+ .join('')}`
164
+
165
+ let data
166
+
167
+ try {
168
+ const regex = /(?:\.([^.]+))?$/
169
+
170
+ const ext = regex.exec(dataUrl.pathname)[1]
171
+ if ('csv' === ext) {
172
+ data = await fetch(dataUrlFinal)
173
+ .then(response => response.text())
174
+ .then(responseText => {
175
+ const parsedCsv = Papa.parse(responseText, {
176
+ header: true,
177
+ dynamicTyping: true,
178
+ skipEmptyLines: true
179
+ })
180
+ return parsedCsv.data
181
+ })
182
+ } else if ('json' === ext) {
183
+ data = await fetch(dataUrlFinal).then(response => response.json())
184
+ } else {
185
+ data = []
186
+ }
187
+ } catch {
188
+ console.error(`Cannot parse URL: ${dataUrlFinal}`)
189
+ data = []
190
+ }
191
+
192
+ if (config.dataDescription) {
193
+ data = transform.autoStandardize(data)
194
+ data = transform.developerStandardize(data, config.dataDescription)
195
+ }
196
+
197
+ Object.assign(data, { urlFiltered: true })
198
+
199
+ updateConfig({ ...config, runtimeDataUrl: dataUrlFinal, data, formattedData: data })
200
+
201
+ if (data) {
202
+ setStateData(data)
203
+ setExcludedData(data)
204
+ setFilteredData(filterData(config.filters, data))
205
+ }
206
+ }
207
+ }
208
+
106
209
  const handleLineType = lineType => {
107
210
  switch (lineType) {
108
211
  case 'dashed-sm':
109
212
  return '5 5'
213
+ case 'Dashed Small':
214
+ return '5 5'
110
215
  case 'dashed-md':
111
216
  return '10 5'
217
+ case 'Dashed Medium':
218
+ return '10 5'
112
219
  case 'dashed-lg':
113
220
  return '15 5'
221
+ case 'Dashed Large':
222
+ return '15 5'
114
223
  default:
115
224
  return 0
116
225
  }
117
226
  }
118
227
 
228
+ const lineOptions = [
229
+ {
230
+ value: 'Dashed Small',
231
+ key: 'dashed-sm'
232
+ },
233
+ {
234
+ value: 'Dashed Medium',
235
+ key: 'dashed-md'
236
+ },
237
+ {
238
+ value: 'Dashed Large',
239
+ key: 'dashed-lg'
240
+ },
241
+ {
242
+ value: 'Solid Line',
243
+ key: 'solid-line'
244
+ }
245
+ ]
246
+
119
247
  const loadConfig = async () => {
120
248
  let response = configObj || (await (await fetch(configUrl)).json())
121
249
 
122
250
  // If data is included through a URL, fetch that and store
123
251
  let data = response.formattedData || response.data || {}
124
252
 
125
- if (response.dataUrl) {
253
+ const urlFilters = response.filters ? (response.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
254
+
255
+ if (response.dataUrl && !urlFilters) {
126
256
  try {
127
257
  const regex = /(?:\.([^.]+))?$/
128
258
 
@@ -144,7 +274,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
144
274
  data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
145
275
  }
146
276
  } catch {
147
- console.error(`COVE: Cannot parse URL: ${response.dataUrl}`)
277
+ console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
148
278
  data = []
149
279
  }
150
280
 
@@ -159,11 +289,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
159
289
  setExcludedData(data)
160
290
  }
161
291
 
292
+ // force showVertical for data tables false if it does not exist
293
+ if (response !== undefined && response.table !== undefined) {
294
+ if (!response.table || !response.table.showVertical) {
295
+ response.table = response.table || {}
296
+ response.table.showVertical = false
297
+ }
298
+ }
162
299
  let newConfig = { ...defaults, ...response }
163
300
  if (newConfig.visualizationType === 'Box Plot') {
164
301
  newConfig.legend.hide = true
165
302
  }
166
303
  if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
304
+
167
305
  updateConfig(newConfig, data)
168
306
  }
169
307
 
@@ -219,7 +357,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
219
357
 
220
358
  newConfig.filters[index].values = filterValues
221
359
  // Initial filter should be active
222
- newConfig.filters[index].active = filterValues[0]
360
+
361
+ newConfig.filters[index].active = newConfig.filters[index].active || filterValues[0]
362
+ newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle ? newConfig.filters[index].filterStyle : 'dropdown'
223
363
  })
224
364
  currentData = filterData(newConfig.filters, newExcludedData)
225
365
  setFilteredData(currentData)
@@ -258,6 +398,37 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
258
398
  let tableData = []
259
399
  const plots = []
260
400
 
401
+ /**
402
+ * Calculates the first quartile (q1) and third quartile (q3) from an array of integers or decimals.
403
+ *
404
+ * @param {Array} arr - The array of integers or decimals.
405
+ * @returns {Object} An object containing the q1 and q3 values.
406
+ */
407
+ const getQuartiles = arr => {
408
+ arr.sort((a, b) => a - b)
409
+
410
+ // Calculate the index of the median value of the array
411
+ const medianIndex = Math.floor(arr.length / 2)
412
+
413
+ // Check if the length of the array is even or odd
414
+ const isEvenLength = arr.length % 2 === 0
415
+
416
+ // Split the array into two subarrays based on the median index
417
+ const q1Array = isEvenLength ? arr.slice(0, medianIndex) : arr.slice(0, medianIndex + 1)
418
+ const q3Array = isEvenLength ? arr.slice(medianIndex) : arr.slice(medianIndex + 1)
419
+
420
+ // Calculate the median of the first subarray to get the q1 value
421
+ const q1Index = Math.floor(q1Array.length / 2)
422
+ const q1 = isEvenLength ? (q1Array[q1Index - 1] + q1Array[q1Index]) / 2 : q1Array[q1Index]
423
+
424
+ // Calculate the median of the second subarray to get the q3 value
425
+ const q3Index = Math.floor(q3Array.length / 2)
426
+ const q3 = isEvenLength ? (q3Array[q3Index - 1] + q3Array[q3Index]) / 2 : q3Array[q3Index]
427
+
428
+ // Return an object containing the q1 and q3 values
429
+ return { q1, q3 }
430
+ }
431
+
261
432
  // group specific statistics
262
433
  // prevent re-renders
263
434
  if (!groups) return
@@ -268,10 +439,16 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
268
439
  // filter data by group
269
440
  let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
270
441
  let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
271
- // let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
442
+
443
+ // Sort the data for upcoming functions.
444
+ let sortedData = filteredDataValues.sort((a, b) => a - b)
445
+
446
+ // ! - Notice d3.quantile doesn't work here, and we had to take a custom route.
447
+ const quartiles = getQuartiles(sortedData)
272
448
 
273
449
  if (!filteredData) throw new Error('boxplots dont have data yet')
274
450
  if (!plots) throw new Error('boxplots dont have plots yet')
451
+
275
452
  if (newConfig.boxplot.firstQuartilePercentage === '') {
276
453
  newConfig.boxplot.firstQuartilePercentage = 0
277
454
  }
@@ -280,27 +457,30 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
280
457
  newConfig.boxplot.thirdQuartilePercentage = 0
281
458
  }
282
459
 
283
- const q1 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.firstQuartilePercentage) / 100)
284
- const q3 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.thirdQuartilePercentage) / 100)
460
+ const q1 = quartiles.q1
461
+ const q3 = quartiles.q3
285
462
  const iqr = q3 - q1
286
463
  const lowerBounds = q1 - (q3 - q1) * 1.5
287
464
  const upperBounds = q3 + (q3 - q1) * 1.5
288
- const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
465
+
466
+ const outliers = sortedData.filter(v => v < lowerBounds || v > upperBounds)
289
467
  let nonOutliers = filteredDataValues
290
468
 
291
469
  nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
292
470
 
293
471
  plots.push({
294
472
  columnCategory: g,
295
- columnMax: Number(q3 + 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
473
+ columnMax: d3.min([d3.max(filteredDataValues), q1 + 1.5 * iqr]),
296
474
  columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
297
475
  columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
298
476
  columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
299
- columnMin: Number(q1 - 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
477
+ columnMin: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
300
478
  columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
301
479
  columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
302
480
  columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
303
481
  columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
482
+ columnLowerBounds: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
483
+ columnUpperBounds: d3.min([d3.max(sortedData), q1 + 1.5 * iqr]),
304
484
  columnOutliers: outliers,
305
485
  values: filteredDataValues,
306
486
  nonOutlierValues: nonOutliers
@@ -316,6 +496,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
316
496
  tableData.map(table => {
317
497
  delete table.columnIqr
318
498
  delete table.nonOutlierValues
499
+ delete table.columnLowerBounds
500
+ delete table.columnUpperBounds
319
501
  return null // resolve eslint
320
502
  })
321
503
 
@@ -329,7 +511,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
329
511
  if (newConfig.visualizationType === 'Combo' && newConfig.series) {
330
512
  newConfig.runtime.barSeriesKeys = []
331
513
  newConfig.runtime.lineSeriesKeys = []
514
+ newConfig.runtime.areaSeriesKeys = []
515
+
332
516
  newConfig.series.forEach(series => {
517
+ if (series.type === 'Area Chart') {
518
+ newConfig.runtime.areaSeriesKeys.push(series)
519
+ }
333
520
  if (series.type === 'Bar') {
334
521
  newConfig.runtime.barSeriesKeys.push(series.dataKey)
335
522
  }
@@ -338,6 +525,15 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
338
525
  }
339
526
  })
340
527
  }
528
+ if (newConfig.visualizationType === 'Area Chart' && newConfig.series) {
529
+ newConfig.runtime.areaSeriesKeys = []
530
+
531
+ newConfig.series.forEach(series => {
532
+ if (series.type === 'Area Chart') {
533
+ newConfig.runtime.areaSeriesKeys.push(series)
534
+ }
535
+ })
536
+ }
341
537
 
342
538
  if (((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Deviation Bar') && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
343
539
  newConfig.runtime.xAxis = newConfig.yAxis
@@ -359,18 +555,21 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
359
555
 
360
556
  data.forEach(row => {
361
557
  let add = true
362
- filters.forEach(filter => {
363
- if (row[filter.columnName] !== filter.active) {
364
- add = false
365
- }
366
- })
558
+ filters
559
+ .filter(filter => filter.type !== 'url')
560
+ .forEach(filter => {
561
+ if (row[filter.columnName] != filter.active) {
562
+ add = false
563
+ }
564
+ })
367
565
 
368
566
  if (add) filteredData.push(row)
369
567
  })
568
+
370
569
  return filteredData
371
570
  }
372
571
 
373
- // Gets filer values from dataset
572
+ // Gets filter values from dataset
374
573
  const generateValuesForFilter = (columnName, data = this.state.data) => {
375
574
  const values = []
376
575
 
@@ -441,6 +640,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
441
640
  loadConfig()
442
641
  }, []) // eslint-disable-line
443
642
 
643
+ useEffect(() => {
644
+ reloadURLData()
645
+ }, [JSON.stringify(config.filters)])
646
+
444
647
  /**
445
648
  * When cove has a config and container ref publish the cove_loaded event.
446
649
  */
@@ -575,10 +778,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
575
778
 
576
779
  const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
577
780
 
578
- const parseDate = dateString => {
781
+ const parseDate = (dateString, showError = true) => {
579
782
  let date = timeParse(config.runtime[section].dateParseFormat)(dateString)
580
783
  if (!date) {
581
- config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`
784
+ if (showError) {
785
+ config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`
786
+ }
582
787
  return new Date()
583
788
  } else {
584
789
  return date
@@ -599,8 +804,26 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
599
804
  return Math.ceil(context.measureText(text).width)
600
805
  }
601
806
 
807
+ const abbreviateNumber = num => {
808
+ let unit = ''
809
+ let absNum = Math.abs(num)
810
+
811
+ if (absNum >= 1e9) {
812
+ unit = 'B'
813
+ num = num / 1e9
814
+ } else if (absNum >= 1e6) {
815
+ unit = 'M'
816
+ num = num / 1e6
817
+ } else if (absNum >= 1e3) {
818
+ unit = 'K'
819
+ num = num / 1e3
820
+ }
821
+
822
+ return num + unit
823
+ }
824
+
602
825
  // Format numeric data based on settings in config
603
- const formatNumber = (num, axis) => {
826
+ const formatNumber = (num, axis, shouldAbbreviate = false) => {
604
827
  // if num is NaN return num
605
828
  if (isNaN(num) || !num) return num
606
829
  // Check if the input number is negative
@@ -616,8 +839,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
616
839
  dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
617
840
  } = config
618
841
 
619
- let formatSuffix = format('.2s')
620
-
621
842
  // check if value contains comma and remove it. later will add comma below.
622
843
  if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
623
844
 
@@ -670,19 +891,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
670
891
  //
671
892
  // Edge case for small numbers with decimals
672
893
  // - if roundTo undefined which means it is blank, then do not round
673
- if ((axis === 'left' && commas && abbreviated) || (axis === 'bottom' && commas && abbreviated)) {
894
+
895
+ if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
674
896
  num = num // eslint-disable-line
675
897
  } else {
676
898
  num = num.toLocaleString('en-US', stringFormattingOptions)
677
899
  }
678
900
  let result = ''
679
901
 
680
- if (abbreviated && axis === 'left') {
681
- num = formatSuffix(parseFloat(num)).replace('G', 'B')
902
+ if (abbreviated && axis === 'left' && shouldAbbreviate) {
903
+ num = abbreviateNumber(parseFloat(num))
682
904
  }
683
905
 
684
- if (bottomAbbreviated && axis === 'bottom') {
685
- num = formatSuffix(parseFloat(num)).replace('G', 'B')
906
+ if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
907
+ num = abbreviateNumber(parseFloat(num))
686
908
  }
687
909
 
688
910
  if (prefix && axis === 'left') {
@@ -748,17 +970,128 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
748
970
  return false
749
971
  }
750
972
 
973
+ // used for Additional Column
974
+ const displayDataAsText = (value, columnName) => {
975
+ if (value === null || value === '' || value === undefined) {
976
+ return ''
977
+ }
978
+
979
+ if (typeof value === 'string' && value.length > 0 && config.legend.type === 'equalnumber') {
980
+ return value
981
+ }
982
+
983
+ let formattedValue = value
984
+
985
+ let columnObj //= config.columns[columnName]
986
+ // config.columns not an array but a hash of objects
987
+ if (Object.keys(config.columns).length > 0) {
988
+ Object.keys(config.columns).forEach(function (key) {
989
+ var column = config.columns[key]
990
+ // add if not the index AND it is enabled to be added to data table
991
+ if (column.name === columnName) {
992
+ columnObj = column
993
+ }
994
+ })
995
+ }
996
+
997
+ if (columnObj === undefined) {
998
+ // then use left axis config
999
+ columnObj = config.type === 'chart' ? config.dataFormat : config.primary
1000
+ // NOTE: Left Value Axis uses different names
1001
+ // so map them below so the code below works
1002
+ // - copy commas to useCommas to work below
1003
+ columnObj['useCommas'] = columnObj.commas
1004
+ // - copy roundTo to roundToPlace to work below
1005
+ columnObj['roundToPlace'] = columnObj.roundTo ? columnObj.roundTo : ''
1006
+ }
1007
+
1008
+ if (columnObj) {
1009
+ // If value is a number, apply specific formattings
1010
+ let hasDecimal = false
1011
+ let decimalPoint = 0
1012
+ if (Number(value)) {
1013
+ if (columnObj.roundToPlace >= 0) {
1014
+ hasDecimal = columnObj.roundToPlace ? columnObj.roundToPlace !== '' || columnObj.roundToPlace !== null : false
1015
+ decimalPoint = columnObj.roundToPlace ? Number(columnObj.roundToPlace) : 0
1016
+
1017
+ // Rounding
1018
+ if (columnObj.hasOwnProperty('roundToPlace') && hasDecimal) {
1019
+ formattedValue = Number(value).toFixed(decimalPoint)
1020
+ }
1021
+ }
1022
+
1023
+ if (columnObj.hasOwnProperty('useCommas') && columnObj.useCommas === true) {
1024
+ // Formats number to string with commas - allows up to 5 decimal places, if rounding is not defined.
1025
+ // Otherwise, uses the rounding value set at 'columnObj.roundToPlace'.
1026
+ formattedValue = Number(value).toLocaleString('en-US', {
1027
+ style: 'decimal',
1028
+ minimumFractionDigits: hasDecimal ? decimalPoint : 0,
1029
+ maximumFractionDigits: hasDecimal ? decimalPoint : 5
1030
+ })
1031
+ }
1032
+ }
1033
+
1034
+ // add prefix and suffix if set
1035
+ formattedValue = (columnObj.prefix || '') + formattedValue + (columnObj.suffix || '')
1036
+ }
1037
+
1038
+ return formattedValue
1039
+ }
1040
+
1041
+ // this is passed DOWN into the various components
1042
+ // then they do a lookup based on the bin number as index into here (TT)
1043
+ const applyLegendToRow = rowObj => {
1044
+ try {
1045
+ if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
1046
+ // Navigation map
1047
+ if ('navigation' === config.type) {
1048
+ let mapColorPalette = colorPalettes[config.color] || colorPalettes['bluegreenreverse']
1049
+ return generateColorsArray(mapColorPalette[3])
1050
+ }
1051
+
1052
+ let hash = hashObj(rowObj)
1053
+
1054
+ if (legendMemo.current.has(hash)) {
1055
+ let idx = legendMemo.current.get(hash)
1056
+ if (runtimeLegend[idx]?.disabled) return false
1057
+
1058
+ // DEV-784 changed to use bin prop to get color instead of idx
1059
+ // bc we re-order legend when showSpecialClassesLast is checked
1060
+ let legendBinColor = runtimeLegend.find(o => o.bin === idx)?.color
1061
+ return generateColorsArray(legendBinColor, runtimeLegend[idx]?.special)
1062
+ }
1063
+
1064
+ // Fail state
1065
+ return generateColorsArray()
1066
+ } catch (e) {
1067
+ console.error('COVE: ', e) // eslint-disable-line
1068
+ }
1069
+ }
1070
+
1071
+ const clean = data => {
1072
+ return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
1073
+ }
1074
+
1075
+ // required for DataTable
1076
+ const displayGeoName = key => {
1077
+ return key
1078
+ }
1079
+
751
1080
  // Prevent render if loading
752
1081
  let body = <Loading />
753
1082
 
754
1083
  if (!loading) {
1084
+ const tableLink = (
1085
+ <a href={`#data-table-${config.dataKey}`} className='margin-left-href'>
1086
+ {config.dataKey} (Go to Table)
1087
+ </a>
1088
+ )
755
1089
  body = (
756
1090
  <>
757
1091
  {isEditor && <EditorPanel />}
758
1092
  {!missingRequiredSections() && !config.newViz && (
759
1093
  <div className='cdc-chart-inner-container'>
760
1094
  {/* Title */}
761
-
762
1095
  {title && config.showTitle && (
763
1096
  <div role='heading' className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>
764
1097
  {config && <sup className='superTitle'>{parse(config.superTitle || '')}</sup>}
@@ -769,11 +1102,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
769
1102
  Skip Over Chart Container
770
1103
  </a>
771
1104
  {/* Filters */}
772
- {config.filters && !externalFilters && <Filters />}
1105
+ {config.filters && !externalFilters && <Filters config={config} setConfig={setConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={excludedData} filterData={filterData} isNumber={isNumber} dimensions={dimensions} />}
773
1106
  {/* Visualization */}
774
1107
  {config?.introText && <section className='introText'>{parse(config.introText)}</section>}
775
1108
  <div
776
- style={{ marginBottom: config.legend.position !== 'bottom' && currentViewport !== 'sm' && currentViewport !== 'xs' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
1109
+ style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
777
1110
  className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
778
1111
  >
779
1112
  {/* All charts except sparkline */}
@@ -797,7 +1130,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
797
1130
  {!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
798
1131
  </div>
799
1132
  {/* Link */}
800
- {link && link}
1133
+ {isDashboard && config.table && config.table.show && config.table.showDataTableLink ? tableLink : link && link}
1134
+
801
1135
  {/* Description */}
802
1136
  {description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
803
1137
 
@@ -808,7 +1142,36 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
808
1142
  </CoveMediaControls.Section>
809
1143
 
810
1144
  {/* Data Table */}
811
- {config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && <DataTable />}
1145
+ {config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && (
1146
+ <DataTable
1147
+ config={config}
1148
+ rawData={config.data}
1149
+ runtimeData={filteredData || excludedData}
1150
+ //navigationHandler={navigationHandler}
1151
+ expandDataTable={config.table.expanded}
1152
+ //headerColor={general.headerColor}
1153
+ columns={config.columns}
1154
+ showDownloadButton={config.general.showDownloadButton}
1155
+ runtimeLegend={dynamicLegendItems}
1156
+ displayDataAsText={displayDataAsText}
1157
+ displayGeoName={displayGeoName}
1158
+ applyLegendToRow={applyLegendToRow}
1159
+ tableTitle={config.table.label}
1160
+ indexTitle={config.table.indexLabel}
1161
+ vizTitle={title}
1162
+ viewport={currentViewport}
1163
+ parseDate={parseDate}
1164
+ formatDate={formatDate}
1165
+ formatNumber={formatNumber}
1166
+ tabbingId={handleChartTabbing}
1167
+ showDownloadImgButton={config.showDownloadImgButton}
1168
+ showDownloadPdfButton={config.showDownloadPdfButton}
1169
+ innerContainerRef={innerContainerRef}
1170
+ outerContainerRef={outerContainerRef}
1171
+ imageRef={imageId}
1172
+ isDebug={isDebug}
1173
+ />
1174
+ )}
812
1175
  {config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
813
1176
  {/* show pdf or image button */}
814
1177
  </div>
@@ -827,7 +1190,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
827
1190
  setConfig,
828
1191
  rawData: stateData ?? {},
829
1192
  excludedData: excludedData,
830
- transformedData: filteredData || excludedData,
1193
+ transformedData: clean(filteredData || excludedData), // do this right before passing to components
1194
+ tableData: filteredData || excludedData, // do not clean table data
831
1195
  unfilteredData: stateData,
832
1196
  seriesHighlight,
833
1197
  colorScale,
@@ -854,10 +1218,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
854
1218
  filterData,
855
1219
  imageId,
856
1220
  handleLineType,
1221
+ lineOptions,
857
1222
  isNumber,
858
- cleanData,
859
1223
  getTextWidth,
860
- twoColorPalette
1224
+ twoColorPalette,
1225
+ isDebug
861
1226
  }
862
1227
 
863
1228
  const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]