@cdc/chart 4.23.3 → 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 +24397 -24193
- package/examples/feature/__data__/area-chart.json +56 -0
- package/examples/{planet-example-data.json → feature/__data__/planet-example-data.json} +16 -4
- package/examples/{area-chart.json → feature/area/area-chart.json} +70 -13
- package/examples/{horizontal-chart-max-increase.json → feature/bar/horizontal-chart-max-increase.json} +10 -4
- 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/{planet-example-config.json → feature/bar/planet-example-config.json} +2 -2
- package/examples/{box-plot.json → feature/boxplot/boxplot.json} +7 -7
- 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/{planet-deviation-config.json → feature/deviation/planet-deviation-config.json} +2 -2
- package/examples/{planet-deviation-data.json → feature/deviation/planet-deviation-data.json} +9 -9
- 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.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.json → feature/scatterplot/scatterplot.json} +1 -1
- 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/{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} +1 -1
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +145 -7
- package/examples/gallery/paired-bar/paired-bar-chart.json +1 -0
- package/index.html +73 -49
- package/package.json +2 -2
- package/src/CdcChart.jsx +111 -26
- package/src/components/AreaChart.jsx +105 -70
- package/src/components/BarChart.jsx +45 -28
- package/src/components/BoxPlot.jsx +28 -20
- package/src/components/DataTable.jsx +7 -6
- package/src/components/DeviationBar.jsx +2 -2
- package/src/components/EditorPanel.jsx +252 -193
- package/src/components/Legend.jsx +1 -1
- package/src/components/LineChart.jsx +10 -16
- package/src/components/LinearChart.jsx +30 -34
- package/src/components/PairedBarChart.jsx +6 -6
- package/src/components/PieChart.jsx +2 -4
- package/src/components/SparkLine.jsx +6 -42
- package/src/data/initial-state.js +7 -3
- package/src/index.jsx +2 -1
- package/src/scss/editor-panel.scss +15 -0
- package/src/scss/main.scss +8 -6
- package/examples/box-plot.csv +0 -5
- package/examples/dynamic-legends.json +0 -125
- package/examples/line-chart.json +0 -34
- package/examples/temp-example-config.json +0 -64
- package/examples/temp-example-data.json +0 -130
- package/src/components/Filters.jsx +0 -126
- /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/{planet-example-data-max-increase.json → feature/__data__/planet-example-data-max-increase.json} +0 -0
- /package/examples/{Barchart_with_negative.json → feature/bar/Barchart_with_negative.json} +0 -0
- /package/examples/{example-bar-chart.json → feature/bar/example-bar-chart.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/{line-chart-max-increase.json → feature/line/line-chart-max-increase.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/{scatterplot-continuous.csv → feature/scatterplot/scatterplot-continuous.csv} +0 -0
- /package/examples/{example-sparkline.json → feature/sparkline/example-sparkline.json} +0 -0
- /package/examples/{big-small-test-bar.json → feature/tests-big-small/big-small-test-bar.json} +0 -0
- /package/examples/{big-small-test-line.json → feature/tests-big-small/big-small-test-line.json} +0 -0
- /package/examples/{big-small-test-negative.json → feature/tests-big-small/big-small-test-negative.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/{line-chart-nonnumeric.json → feature/tests-non-numerics/line-chart-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
|
@@ -31,7 +31,7 @@ import DataTable from './components/DataTable'
|
|
|
31
31
|
import defaults from './data/initial-state'
|
|
32
32
|
import EditorPanel from './components/EditorPanel'
|
|
33
33
|
import Loading from '@cdc/core/components/Loading'
|
|
34
|
-
import Filters from '
|
|
34
|
+
import Filters from '@cdc/core/components/Filters'
|
|
35
35
|
import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
|
|
36
36
|
|
|
37
37
|
// Helpers
|
|
@@ -40,11 +40,10 @@ import getViewport from '@cdc/core/helpers/getViewport'
|
|
|
40
40
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
41
41
|
import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
|
|
42
42
|
import isNumber from '@cdc/core/helpers/isNumber'
|
|
43
|
-
import cleanData from '@cdc/core/helpers/cleanData'
|
|
44
43
|
|
|
45
44
|
import './scss/main.scss'
|
|
46
45
|
|
|
47
|
-
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 }) {
|
|
48
47
|
const transform = new DataTransform()
|
|
49
48
|
const [loading, setLoading] = useState(true)
|
|
50
49
|
const [colorScale, setColorScale] = useState(null)
|
|
@@ -84,7 +83,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
const handleChartAriaLabels = (state, testing = false) => {
|
|
87
|
-
if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
|
|
86
|
+
if (testing) console.log(`handleChartAriaLabels Testing On:`, state) // eslint-disable-line
|
|
88
87
|
try {
|
|
89
88
|
if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
|
|
90
89
|
let ariaLabel = ''
|
|
@@ -144,7 +143,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
144
143
|
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
|
|
145
144
|
}
|
|
146
145
|
} catch {
|
|
147
|
-
console.error(`COVE: Cannot parse URL: ${response.dataUrl}`)
|
|
146
|
+
console.error(`COVE: Cannot parse URL: ${response.dataUrl}`) // eslint-disable-line
|
|
148
147
|
data = []
|
|
149
148
|
}
|
|
150
149
|
|
|
@@ -164,6 +163,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
164
163
|
newConfig.legend.hide = true
|
|
165
164
|
}
|
|
166
165
|
if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
|
|
166
|
+
|
|
167
167
|
updateConfig(newConfig, data)
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -220,6 +220,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
220
220
|
newConfig.filters[index].values = filterValues
|
|
221
221
|
// Initial filter should be active
|
|
222
222
|
newConfig.filters[index].active = filterValues[0]
|
|
223
|
+
newConfig.filters[index].filterStyle = newConfig.filters[index].filterStyle ? newConfig.filters[index].filterStyle : 'dropdown'
|
|
223
224
|
})
|
|
224
225
|
currentData = filterData(newConfig.filters, newExcludedData)
|
|
225
226
|
setFilteredData(currentData)
|
|
@@ -258,6 +259,37 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
258
259
|
let tableData = []
|
|
259
260
|
const plots = []
|
|
260
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
|
+
|
|
261
293
|
// group specific statistics
|
|
262
294
|
// prevent re-renders
|
|
263
295
|
if (!groups) return
|
|
@@ -268,10 +300,16 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
268
300
|
// filter data by group
|
|
269
301
|
let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
|
|
270
302
|
let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
|
|
271
|
-
|
|
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)
|
|
272
309
|
|
|
273
310
|
if (!filteredData) throw new Error('boxplots dont have data yet')
|
|
274
311
|
if (!plots) throw new Error('boxplots dont have plots yet')
|
|
312
|
+
|
|
275
313
|
if (newConfig.boxplot.firstQuartilePercentage === '') {
|
|
276
314
|
newConfig.boxplot.firstQuartilePercentage = 0
|
|
277
315
|
}
|
|
@@ -280,27 +318,30 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
280
318
|
newConfig.boxplot.thirdQuartilePercentage = 0
|
|
281
319
|
}
|
|
282
320
|
|
|
283
|
-
const q1 =
|
|
284
|
-
const q3 =
|
|
321
|
+
const q1 = quartiles.q1
|
|
322
|
+
const q3 = quartiles.q3
|
|
285
323
|
const iqr = q3 - q1
|
|
286
324
|
const lowerBounds = q1 - (q3 - q1) * 1.5
|
|
287
325
|
const upperBounds = q3 + (q3 - q1) * 1.5
|
|
288
|
-
|
|
326
|
+
|
|
327
|
+
const outliers = sortedData.filter(v => v < lowerBounds || v > upperBounds)
|
|
289
328
|
let nonOutliers = filteredDataValues
|
|
290
329
|
|
|
291
330
|
nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
|
|
292
331
|
|
|
293
332
|
plots.push({
|
|
294
333
|
columnCategory: g,
|
|
295
|
-
columnMax:
|
|
334
|
+
columnMax: d3.min([d3.max(filteredDataValues), q1 + 1.5 * iqr]),
|
|
296
335
|
columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
|
|
297
336
|
columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
298
337
|
columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
|
|
299
|
-
columnMin:
|
|
338
|
+
columnMin: d3.max([d3.min(filteredDataValues), q1 - 1.5 * iqr]),
|
|
300
339
|
columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
|
|
301
340
|
columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
302
341
|
columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
303
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]),
|
|
304
345
|
columnOutliers: outliers,
|
|
305
346
|
values: filteredDataValues,
|
|
306
347
|
nonOutlierValues: nonOutliers
|
|
@@ -316,6 +357,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
316
357
|
tableData.map(table => {
|
|
317
358
|
delete table.columnIqr
|
|
318
359
|
delete table.nonOutlierValues
|
|
360
|
+
delete table.columnLowerBounds
|
|
361
|
+
delete table.columnUpperBounds
|
|
319
362
|
return null // resolve eslint
|
|
320
363
|
})
|
|
321
364
|
|
|
@@ -329,7 +372,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
329
372
|
if (newConfig.visualizationType === 'Combo' && newConfig.series) {
|
|
330
373
|
newConfig.runtime.barSeriesKeys = []
|
|
331
374
|
newConfig.runtime.lineSeriesKeys = []
|
|
375
|
+
newConfig.runtime.areaSeriesKeys = []
|
|
376
|
+
|
|
332
377
|
newConfig.series.forEach(series => {
|
|
378
|
+
if (series.type === 'Area Chart') {
|
|
379
|
+
newConfig.runtime.areaSeriesKeys.push(series)
|
|
380
|
+
}
|
|
333
381
|
if (series.type === 'Bar') {
|
|
334
382
|
newConfig.runtime.barSeriesKeys.push(series.dataKey)
|
|
335
383
|
}
|
|
@@ -338,6 +386,15 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
338
386
|
}
|
|
339
387
|
})
|
|
340
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
|
+
}
|
|
341
398
|
|
|
342
399
|
if (((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Deviation Bar') && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
|
|
343
400
|
newConfig.runtime.xAxis = newConfig.yAxis
|
|
@@ -370,7 +427,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
370
427
|
return filteredData
|
|
371
428
|
}
|
|
372
429
|
|
|
373
|
-
// Gets
|
|
430
|
+
// Gets filter values from dataset
|
|
374
431
|
const generateValuesForFilter = (columnName, data = this.state.data) => {
|
|
375
432
|
const values = []
|
|
376
433
|
|
|
@@ -599,8 +656,26 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
599
656
|
return Math.ceil(context.measureText(text).width)
|
|
600
657
|
}
|
|
601
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
|
+
|
|
602
677
|
// Format numeric data based on settings in config
|
|
603
|
-
const formatNumber = (num, axis) => {
|
|
678
|
+
const formatNumber = (num, axis, shouldAbbreviate = false) => {
|
|
604
679
|
// if num is NaN return num
|
|
605
680
|
if (isNaN(num) || !num) return num
|
|
606
681
|
// Check if the input number is negative
|
|
@@ -616,8 +691,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
616
691
|
dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
|
|
617
692
|
} = config
|
|
618
693
|
|
|
619
|
-
let formatSuffix = format('.2s')
|
|
620
|
-
|
|
621
694
|
// check if value contains comma and remove it. later will add comma below.
|
|
622
695
|
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
623
696
|
|
|
@@ -670,19 +743,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
670
743
|
//
|
|
671
744
|
// Edge case for small numbers with decimals
|
|
672
745
|
// - if roundTo undefined which means it is blank, then do not round
|
|
673
|
-
|
|
746
|
+
|
|
747
|
+
if ((axis === 'left' && commas && abbreviated && shouldAbbreviate) || (axis === 'bottom' && commas && abbreviated && shouldAbbreviate)) {
|
|
674
748
|
num = num // eslint-disable-line
|
|
675
749
|
} else {
|
|
676
750
|
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
677
751
|
}
|
|
678
752
|
let result = ''
|
|
679
753
|
|
|
680
|
-
if (abbreviated && axis === 'left') {
|
|
681
|
-
num =
|
|
754
|
+
if (abbreviated && axis === 'left' && shouldAbbreviate) {
|
|
755
|
+
num = abbreviateNumber(parseFloat(num))
|
|
682
756
|
}
|
|
683
757
|
|
|
684
|
-
if (bottomAbbreviated && axis === 'bottom') {
|
|
685
|
-
num =
|
|
758
|
+
if (bottomAbbreviated && axis === 'bottom' && shouldAbbreviate) {
|
|
759
|
+
num = abbreviateNumber(parseFloat(num))
|
|
686
760
|
}
|
|
687
761
|
|
|
688
762
|
if (prefix && axis === 'left') {
|
|
@@ -748,10 +822,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
748
822
|
return false
|
|
749
823
|
}
|
|
750
824
|
|
|
825
|
+
const clean = data => {
|
|
826
|
+
return config?.xAxis?.dataKey ? transform.cleanData(data, config.xAxis.dataKey) : data
|
|
827
|
+
}
|
|
828
|
+
|
|
751
829
|
// Prevent render if loading
|
|
752
830
|
let body = <Loading />
|
|
753
831
|
|
|
754
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
|
+
)
|
|
755
838
|
body = (
|
|
756
839
|
<>
|
|
757
840
|
{isEditor && <EditorPanel />}
|
|
@@ -769,11 +852,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
769
852
|
Skip Over Chart Container
|
|
770
853
|
</a>
|
|
771
854
|
{/* Filters */}
|
|
772
|
-
{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} />}
|
|
773
856
|
{/* Visualization */}
|
|
774
857
|
{config?.introText && <section className='introText'>{parse(config.introText)}</section>}
|
|
775
858
|
<div
|
|
776
|
-
style={{ marginBottom: config.legend.position !== 'bottom' &&
|
|
859
|
+
style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
|
|
777
860
|
className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
|
|
778
861
|
>
|
|
779
862
|
{/* All charts except sparkline */}
|
|
@@ -797,7 +880,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
797
880
|
{!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
|
|
798
881
|
</div>
|
|
799
882
|
{/* Link */}
|
|
800
|
-
{link && link}
|
|
883
|
+
{isDashboard && config.table && config.table.show && config.table.showDataTableLink ? tableLink : link && link}
|
|
884
|
+
|
|
801
885
|
{/* Description */}
|
|
802
886
|
{description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
|
|
803
887
|
|
|
@@ -827,7 +911,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
827
911
|
setConfig,
|
|
828
912
|
rawData: stateData ?? {},
|
|
829
913
|
excludedData: excludedData,
|
|
830
|
-
transformedData: filteredData || excludedData,
|
|
914
|
+
transformedData: clean(filteredData || excludedData), // do this right before passing to components
|
|
915
|
+
tableData: filteredData || excludedData, // do not clean table data
|
|
831
916
|
unfilteredData: stateData,
|
|
832
917
|
seriesHighlight,
|
|
833
918
|
colorScale,
|
|
@@ -855,9 +940,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
855
940
|
imageId,
|
|
856
941
|
handleLineType,
|
|
857
942
|
isNumber,
|
|
858
|
-
cleanData,
|
|
859
943
|
getTextWidth,
|
|
860
|
-
twoColorPalette
|
|
944
|
+
twoColorPalette,
|
|
945
|
+
isDebug
|
|
861
946
|
}
|
|
862
947
|
|
|
863
948
|
const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext,
|
|
1
|
+
import React, { useContext, useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
// cdc
|
|
4
4
|
import ConfigContext from '../ConfigContext'
|
|
@@ -6,26 +6,29 @@ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
|
6
6
|
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
7
7
|
|
|
8
8
|
// visx & d3
|
|
9
|
+
import * as allCurves from '@visx/curve'
|
|
9
10
|
import { AreaClosed, LinePath, Bar } from '@visx/shape'
|
|
10
11
|
import { Group } from '@visx/group'
|
|
11
|
-
import
|
|
12
|
-
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'
|
|
12
|
+
import { useTooltip, useTooltipInPortal, defaultStyles, Tooltip } from '@visx/tooltip'
|
|
13
13
|
import { localPoint } from '@visx/event'
|
|
14
14
|
import { bisector } from 'd3-array'
|
|
15
15
|
|
|
16
|
-
const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
16
|
+
const CoveAreaChart = ({ xScale, yScale, yMax, xMax, chartRef }) => {
|
|
17
17
|
// enable various console logs in the file
|
|
18
18
|
const DEBUG = false
|
|
19
|
+
const [chartPosition, setChartPosition] = useState(null)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setChartPosition(chartRef.current.getBoundingClientRect())
|
|
23
|
+
}, [chartRef])
|
|
19
24
|
|
|
20
25
|
// import data from context
|
|
21
|
-
const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight } = useContext(ConfigContext)
|
|
26
|
+
const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight, colorScale } = useContext(ConfigContext)
|
|
27
|
+
const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
|
|
22
28
|
|
|
23
29
|
// import tooltip helpers
|
|
24
30
|
const { tooltipData, showTooltip } = useTooltip()
|
|
25
31
|
|
|
26
|
-
// used for offset on tooltip hover
|
|
27
|
-
let isEditor = window.location.href.includes('editor=true')
|
|
28
|
-
|
|
29
32
|
// here we're inside of the svg,
|
|
30
33
|
// it appears we need to use TooltipInPortal.
|
|
31
34
|
const { TooltipInPortal } = useTooltipInPortal({
|
|
@@ -36,7 +39,8 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
36
39
|
|
|
37
40
|
// Draw transparent bars over the chart to get tooltip data
|
|
38
41
|
// Turn DEBUG on for additional context.
|
|
39
|
-
|
|
42
|
+
if (!data) return
|
|
43
|
+
let barThickness = xMax / data
|
|
40
44
|
let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
|
|
41
45
|
let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
|
|
42
46
|
|
|
@@ -51,6 +55,8 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
51
55
|
|
|
52
56
|
if (config.xAxis.type === 'date') {
|
|
53
57
|
const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
|
|
58
|
+
if (!x) return
|
|
59
|
+
if (!xScale) return
|
|
54
60
|
const x0 = xScale.invert(x)
|
|
55
61
|
const index = bisectDate(config.data, x0, 1)
|
|
56
62
|
const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
|
|
@@ -58,51 +64,55 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
58
64
|
}
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
const handleMouseOver =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const { x, y } = eventSvgCoords
|
|
67
|
-
|
|
68
|
-
let closestXScaleValue = getXValueFromCoordinate(x)
|
|
69
|
-
let formattedDate = formatDate(closestXScaleValue)
|
|
70
|
-
|
|
71
|
-
let yScaleValues
|
|
72
|
-
if (config.xAxis.type === 'categorical') {
|
|
73
|
-
yScaleValues = data.filter(d => d[config.xAxis.dataKey] === closestXScaleValue)
|
|
74
|
-
} else {
|
|
75
|
-
yScaleValues = data.filter(d => d[config.xAxis.dataKey] === formattedDate)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let seriesToInclude = []
|
|
79
|
-
let yScaleMaxValues = []
|
|
80
|
-
let itemsToLoop = [config.runtime.xAxis.dataKey, ...config.runtime.seriesKeys]
|
|
67
|
+
const handleMouseOver = (e, data) => {
|
|
68
|
+
// get the svg coordinates of the mouse
|
|
69
|
+
// and get the closest values
|
|
70
|
+
const eventSvgCoords = localPoint(e)
|
|
71
|
+
const { x, y } = eventSvgCoords
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})
|
|
73
|
+
let closestXScaleValue = getXValueFromCoordinate(x)
|
|
74
|
+
let formattedDate = formatDate(closestXScaleValue)
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
tooltipData.dataXPosition = isEditor ? 300 + x + 20 : x + 20
|
|
93
|
-
tooltipData.dataYPosition = y - 20
|
|
76
|
+
let yScaleValues
|
|
77
|
+
if (config.xAxis.type === 'categorical') {
|
|
78
|
+
yScaleValues = data.filter(d => d[config.xAxis.dataKey] === closestXScaleValue)
|
|
79
|
+
} else {
|
|
80
|
+
yScaleValues = data.filter(d => formatDate(parseDate(d[config.xAxis.dataKey])) === formattedDate)
|
|
81
|
+
}
|
|
94
82
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
let seriesToInclude = []
|
|
84
|
+
let yScaleMaxValues = []
|
|
85
|
+
let itemsToLoop = [config.runtime.xAxis.dataKey, ...config.runtime.seriesKeys]
|
|
86
|
+
|
|
87
|
+
itemsToLoop.map(seriesKey => {
|
|
88
|
+
if (!seriesKey) return
|
|
89
|
+
if (!yScaleValues[0]) return
|
|
90
|
+
for (const item of Object.entries(yScaleValues[0])) {
|
|
91
|
+
if (item[0] === seriesKey) {
|
|
92
|
+
seriesToInclude.push(item)
|
|
93
|
+
}
|
|
100
94
|
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// filter out the series that aren't added to the map.
|
|
98
|
+
seriesToInclude.map(series => yScaleMaxValues.push(Number(yScaleValues[0][series])))
|
|
99
|
+
if (!seriesToInclude) return
|
|
100
|
+
let tooltipDataFromSeries = Object.fromEntries(seriesToInclude) ? Object.fromEntries(seriesToInclude) : {}
|
|
101
|
+
|
|
102
|
+
let tooltipData = {}
|
|
103
|
+
tooltipData.data = tooltipDataFromSeries
|
|
104
|
+
tooltipData.dataXPosition = x + 20
|
|
105
|
+
tooltipData.dataYPosition = y - 100
|
|
106
|
+
|
|
107
|
+
let tooltipInformation = {
|
|
108
|
+
tooltipData: tooltipData,
|
|
109
|
+
tooltipTop: 0,
|
|
110
|
+
tooltipValues: yScaleValues,
|
|
111
|
+
tooltipLeft: x
|
|
112
|
+
}
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
[showTooltip] // eslint-disable-line
|
|
105
|
-
)
|
|
114
|
+
showTooltip(tooltipInformation)
|
|
115
|
+
}
|
|
106
116
|
|
|
107
117
|
const TooltipListItem = ({ item }) => {
|
|
108
118
|
const [label, value] = item
|
|
@@ -120,8 +130,8 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
120
130
|
return (
|
|
121
131
|
data && (
|
|
122
132
|
<ErrorBoundary component='AreaChart'>
|
|
123
|
-
<Group className='area-chart' key='area-wrapper' left={config.yAxis.size}>
|
|
124
|
-
{config.
|
|
133
|
+
<Group className='area-chart' key='area-wrapper' left={Number(config.yAxis.size)}>
|
|
134
|
+
{(config.runtime.areaSeriesKeys || config.runtime.seriesKeys).map((s, index) => {
|
|
125
135
|
let seriesColor = colorPalettesChart[config.palette][index]
|
|
126
136
|
let curveType = allCurves[s.lineType]
|
|
127
137
|
let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s.dataKey) === -1
|
|
@@ -130,16 +140,42 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
130
140
|
data.map(d => xScale(parseDate(d[config.xAxis.dataKey])))
|
|
131
141
|
|
|
132
142
|
return (
|
|
133
|
-
|
|
143
|
+
<React.Fragment key={index}>
|
|
134
144
|
{/* prettier-ignore */}
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
<LinePath
|
|
146
|
+
data={data}
|
|
147
|
+
x={d => handleX(d)}
|
|
148
|
+
y={d => yScale(d[config.series[index].dataKey])}
|
|
149
|
+
stroke={displayArea ? seriesColor : 'transparent'}
|
|
150
|
+
strokeWidth={2}
|
|
151
|
+
strokeOpacity={1}
|
|
152
|
+
shapeRendering='geometricPrecision'
|
|
153
|
+
curve={curveType}
|
|
154
|
+
strokeDasharray={s.type ? handleLineType(s.type) : 0}
|
|
155
|
+
/>
|
|
137
156
|
|
|
138
157
|
{/* prettier-ignore */}
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
<AreaClosed
|
|
159
|
+
key={'area-chart'}
|
|
160
|
+
fill={ displayArea ? colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000' : 'transparent'}
|
|
161
|
+
fillOpacity={transparentArea ? 0.25 : 0.5}
|
|
162
|
+
data={data} x={d => handleX(d)}
|
|
163
|
+
y={d => handleY(d, index)}
|
|
164
|
+
yScale={yScale}
|
|
165
|
+
curve={curveType}
|
|
166
|
+
strokeDasharray={s.type ? handleLineType(s.typ) : 0}
|
|
167
|
+
/>
|
|
141
168
|
|
|
142
|
-
|
|
169
|
+
{/* Transparent bar for tooltips */}
|
|
170
|
+
{/* prettier-ignore */}
|
|
171
|
+
<Bar
|
|
172
|
+
width={ Number(xMax)}
|
|
173
|
+
height={ Number(yMax)}
|
|
174
|
+
fill={DEBUG ? 'red' : 'transparent'}
|
|
175
|
+
fillOpacity={0.05}
|
|
176
|
+
style={DEBUG ? { stroke: 'black', strokeWidth: 2 } : {}}
|
|
177
|
+
onMouseMove={e => handleMouseOver(e, data)}
|
|
178
|
+
/>
|
|
143
179
|
|
|
144
180
|
{/* circles that appear on hover */}
|
|
145
181
|
{tooltipData && (
|
|
@@ -149,19 +185,19 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
149
185
|
r={4.5}
|
|
150
186
|
opacity={1}
|
|
151
187
|
fillOpacity={1}
|
|
152
|
-
fill={
|
|
188
|
+
fill={displayArea ? (colorScale ? colorScale(config.runtime.seriesLabels ? config.runtime.seriesLabels[s.dataKey] : s.dataKey) : '#000') : 'transparent'}
|
|
153
189
|
style={{ filter: 'unset', opacity: 1 }}
|
|
154
190
|
/>
|
|
155
191
|
)}
|
|
156
192
|
|
|
157
|
-
{/* bars
|
|
193
|
+
{/* another tool for showing bars during debug mode. */}
|
|
158
194
|
{DEBUG &&
|
|
159
|
-
|
|
195
|
+
data.map((item, index) => {
|
|
160
196
|
return (
|
|
161
197
|
<Bar
|
|
162
198
|
className='bar-here'
|
|
163
|
-
x={barThickness * index + offset}
|
|
164
|
-
y={d => yScale(d[config.series[index].dataKey])}
|
|
199
|
+
x={Number(barThickness * index + offset)}
|
|
200
|
+
y={d => Number(yScale(d[config.series[index].dataKey]))}
|
|
165
201
|
yScale={yScale}
|
|
166
202
|
width={barThicknessAdjusted}
|
|
167
203
|
height={yMax}
|
|
@@ -174,19 +210,18 @@ const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
|
174
210
|
})}
|
|
175
211
|
|
|
176
212
|
{tooltipData && (
|
|
177
|
-
<TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition} left={tooltipData.dataXPosition} style={defaultStyles}>
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<li>
|
|
213
|
+
<TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition + chartPosition?.top} left={tooltipData.dataXPosition + chartPosition?.left} style={defaultStyles}>
|
|
214
|
+
<ul style={{ listStyle: 'none', paddingLeft: 'unset', fontFamily: 'sans-serif', margin: 'auto', lineHeight: '1rem' }} data-tooltip-id={tooltip_id}>
|
|
215
|
+
{typeof tooltipData === 'object' &&
|
|
216
|
+
Object.entries(tooltipData.data).map(item => (
|
|
217
|
+
<li style={{ padding: '2.5px 0' }}>
|
|
182
218
|
<TooltipListItem item={item} />
|
|
183
219
|
</li>
|
|
184
220
|
))}
|
|
185
|
-
|
|
186
|
-
</Group>
|
|
221
|
+
</ul>
|
|
187
222
|
</TooltipInPortal>
|
|
188
223
|
)}
|
|
189
|
-
|
|
224
|
+
</React.Fragment>
|
|
190
225
|
)
|
|
191
226
|
})}
|
|
192
227
|
</Group>
|