@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.
- package/dist/cdcchart.js +42292 -40337
- package/examples/feature/__data__/area-chart.json +56 -0
- package/examples/{planet-example-data.json → feature/__data__/planet-example-data-max-increase.json} +4 -4
- package/examples/feature/__data__/planet-example-data.json +68 -0
- package/examples/feature/area/area-chart.json +244 -0
- package/examples/{example-bar-chart.json → feature/bar/example-bar-chart.json} +4 -1
- package/examples/feature/bar/horizontal-chart-max-increase.json +44 -0
- package/examples/{horizontal-chart.json → feature/bar/horizontal-chart.json} +10 -4
- package/examples/{horizontal-stacked-bar-chart.json → feature/bar/horizontal-stacked-bar-chart.json} +7 -3
- package/examples/{planet-chart-horizontal-example-config.json → feature/bar/planet-chart-horizontal-example-config.json} +8 -3
- package/examples/feature/bar/planet-example-config.json +156 -0
- package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -8
- package/examples/feature/boxplot/testing.csv +38 -0
- package/examples/feature/combo/combochart-categories_are_numbers .json +18 -0
- package/examples/{planet-combo-example-config.json → feature/combo/planet-combo-example-config.json} +1 -1
- package/examples/feature/deviation/planet-deviation-config.json +168 -0
- package/examples/feature/deviation/planet-deviation-data.json +38 -0
- package/examples/feature/filters/filter-testing.json +178 -0
- package/examples/feature/forecasting/case_date_example.csv +130 -0
- package/examples/feature/forecasting/effective_reproduction.json +202 -0
- package/examples/feature/forecasting/r_data.csv +130 -0
- package/examples/feature/line/line-chart-max-increase.json +32 -0
- package/examples/feature/line/line-chart.json +124 -0
- package/examples/{paired-bar-example.json → feature/paired-bar/paired-bar-example.json} +10 -4
- package/examples/{planet-pie-example-config.json → feature/pie/planet-pie-example-config.json} +2 -2
- package/examples/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +3 -3
- package/examples/{scatterplot.json → feature/scatterplot/scatterplot.json} +3 -3
- package/examples/feature/sparkline/example-sparkline.json +76 -0
- package/examples/feature/tests-big-small/big-small-test-bar.json +328 -0
- package/examples/feature/tests-big-small/big-small-test-line.json +328 -0
- package/examples/feature/tests-big-small/big-small-test-negative.json +328 -0
- package/examples/{case-rate-example-config.json → feature/tests-case-rate/case-rate-example-config.json} +2 -2
- package/examples/{covid-confidence-example-config.json → feature/tests-covid/covid-confidence-example-config.json} +8 -3
- package/examples/{covid-example-config.json → feature/tests-covid/covid-example-config.json} +7 -3
- package/examples/{cutoff-example-config.json → feature/tests-cutoff/cutoff-example-config.json} +7 -3
- package/examples/{date-exclusions-config.json → feature/tests-date-exclusions/date-exclusions-config.json} +2 -2
- package/examples/{example-bar-chart-nonnumeric.json → feature/tests-non-numerics/example-bar-chart-nonnumeric.json} +1 -1
- package/examples/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-nonnumeric.json} +5 -5
- package/examples/{planet-pie-example-config-nonnumeric.json → feature/tests-non-numerics/planet-pie-example-config-nonnumeric.json} +2 -2
- package/examples/{sparkline-chart-nonnumeric.json → feature/tests-non-numerics/sparkline-chart-nonnumeric.json} +2 -2
- package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
- package/examples/gallery/line/line.json +1 -0
- package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
- package/index.html +76 -35
- package/package.json +6 -3
- package/src/CdcChart.jsx +245 -106
- package/src/components/AreaChart.jsx +233 -0
- package/src/components/BarChart.jsx +103 -62
- package/src/components/BoxPlot.jsx +39 -18
- package/src/components/DataTable.jsx +26 -21
- package/src/components/DeviationBar.jsx +191 -0
- package/src/components/EditorPanel.jsx +662 -298
- package/src/components/Legend.jsx +59 -46
- package/src/components/LineChart.jsx +12 -36
- package/src/components/LinearChart.jsx +163 -64
- package/src/components/PairedBarChart.jsx +6 -7
- package/src/components/PieChart.jsx +12 -17
- package/src/components/ScatterPlot.jsx +19 -16
- package/src/components/SparkLine.jsx +84 -118
- package/src/components/useIntersectionObserver.jsx +1 -1
- package/src/data/initial-state.js +27 -7
- package/src/hooks/useColorPalette.js +58 -48
- package/src/hooks/useReduceData.js +3 -4
- package/src/index.jsx +3 -2
- package/src/scss/editor-panel.scss +20 -0
- package/src/scss/main.scss +8 -6
- package/src/test/CdcChart.test.jsx +6 -0
- package/examples/box-plot.csv +0 -5
- package/examples/dynamic-legends.json +0 -125
- package/examples/line-chart.json +0 -34
- package/examples/planet-example-config.json +0 -37
- package/examples/temp-example-config.json +0 -64
- package/examples/temp-example-data.json +0 -130
- package/src/components/Filters.jsx +0 -125
- /package/examples/{age-adjusted-rates.json → feature/__data__/age-adjusted-rates.json} +0 -0
- /package/examples/{new-data.csv → feature/__data__/new-data.csv} +0 -0
- /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
- /package/examples/{stacked-vertical-bar-example-negative.json → feature/bar/stacked-vertical-bar-example-negative.json} +0 -0
- /package/examples/{stacked-vertical-bar-example.json → feature/bar/stacked-vertical-bar-example.json} +0 -0
- /package/examples/{box-plot-data.json → feature/boxplot/box-plot-data.json} +0 -0
- /package/examples/{newdata.json → feature/boxplot/boxplot-data.json} +0 -0
- /package/examples/{paired-bar-data.json → feature/paired-bar/paired-bar-data.json} +0 -0
- /package/examples/{paired-bar-formatted.json → feature/paired-bar/paired-bar-formatted.json} +0 -0
- /package/examples/{case-rate-example-data.json → feature/tests-case-rate/case-rate-example-data.json} +0 -0
- /package/examples/{covid-example-data-confidence.json → feature/tests-covid/covid-example-data-confidence.json} +0 -0
- /package/examples/{covid-example-data.json → feature/tests-covid/covid-example-data.json} +0 -0
- /package/examples/{cutoff-example-data.json → feature/tests-cutoff/cutoff-example-data.json} +0 -0
- /package/examples/{date-exclusions-data.json → feature/tests-date-exclusions/date-exclusions-data.json} +0 -0
- /package/examples/{example-combo-bar-nonnumeric.json → feature/tests-non-numerics/example-combo-bar-nonnumeric.json} +0 -0
- /package/examples/{planet-example-data-nonnumeric.json → feature/tests-non-numerics/planet-example-data-nonnumeric.json} +0 -0
- /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 '
|
|
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 '
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
222
|
-
let
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
635
|
-
|
|
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 =
|
|
754
|
+
if (abbreviated && axis === 'left' && shouldAbbreviate) {
|
|
755
|
+
num = abbreviateNumber(parseFloat(num))
|
|
643
756
|
}
|
|
644
757
|
|
|
645
|
-
if (
|
|
646
|
-
num =
|
|
758
|
+
if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
|
|
759
|
+
num = abbreviateNumber(parseFloat(num))
|
|
647
760
|
}
|
|
648
761
|
|
|
649
|
-
if (prefix && axis
|
|
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
|
|
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
|
-
'
|
|
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
|
-
|
|
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}`]
|