@cdc/chart 4.23.2 → 4.23.3
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 +41198 -39447
- package/examples/area-chart.json +187 -0
- package/examples/big-small-test-bar.json +328 -0
- package/examples/big-small-test-line.json +328 -0
- package/examples/big-small-test-negative.json +328 -0
- package/examples/box-plot.json +0 -1
- package/examples/example-bar-chart.json +4 -1
- package/examples/example-sparkline.json +76 -0
- package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
- 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/horizontal-chart-max-increase.json +38 -0
- package/examples/line-chart-max-increase.json +32 -0
- package/examples/line-chart-nonnumeric.json +5 -5
- package/examples/line-chart.json +6 -6
- package/examples/planet-deviation-config.json +168 -0
- package/examples/planet-deviation-data.json +38 -0
- package/examples/planet-example-config.json +139 -20
- package/examples/planet-example-data-max-increase.json +56 -0
- package/examples/planet-example-data.json +10 -10
- package/examples/scatterplot-continuous.csv +3 -3
- package/examples/scatterplot.json +2 -2
- package/examples/sparkline-chart-nonnumeric.json +3 -3
- package/index.html +26 -9
- package/package.json +6 -3
- package/src/CdcChart.jsx +146 -92
- package/src/components/AreaChart.jsx +198 -0
- package/src/components/BarChart.jsx +58 -34
- package/src/components/BoxPlot.jsx +28 -15
- package/src/components/DataTable.jsx +21 -17
- package/src/components/DeviationBar.jsx +191 -0
- package/src/components/EditorPanel.jsx +473 -168
- package/src/components/Filters.jsx +3 -2
- package/src/components/Legend.jsx +59 -46
- package/src/components/LineChart.jsx +3 -21
- package/src/components/LinearChart.jsx +158 -55
- package/src/components/PairedBarChart.jsx +0 -1
- package/src/components/PieChart.jsx +11 -14
- package/src/components/ScatterPlot.jsx +19 -16
- package/src/components/SparkLine.jsx +87 -85
- package/src/components/useIntersectionObserver.jsx +1 -1
- package/src/data/initial-state.js +20 -4
- package/src/hooks/useColorPalette.js +58 -48
- package/src/hooks/useReduceData.js +3 -4
- package/src/index.jsx +1 -1
- package/src/scss/editor-panel.scss +5 -0
- package/src/test/CdcChart.test.jsx +6 -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
|
|
|
@@ -47,7 +46,6 @@ import './scss/main.scss'
|
|
|
47
46
|
|
|
48
47
|
export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }) {
|
|
49
48
|
const transform = new DataTransform()
|
|
50
|
-
|
|
51
49
|
const [loading, setLoading] = useState(true)
|
|
52
50
|
const [colorScale, setColorScale] = useState(null)
|
|
53
51
|
const [config, setConfig] = useState({})
|
|
@@ -63,15 +61,28 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
63
61
|
const [dynamicLegendItems, setDynamicLegendItems] = useState([])
|
|
64
62
|
const [imageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
65
63
|
|
|
66
|
-
const legendGlyphSize = 15
|
|
67
|
-
const legendGlyphSizeHalf = legendGlyphSize / 2
|
|
68
|
-
|
|
69
64
|
// Destructure items from config for more readable JSX
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
let { legend, title, description, visualizationType } = config
|
|
66
|
+
|
|
67
|
+
// set defaults on titles if blank AND only in editor
|
|
68
|
+
if (isEditor) {
|
|
69
|
+
if (!title || title === '') title = 'Chart Title'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (config.table && (!config.table?.label || config.table?.label === '')) config.table.label = 'Data Table'
|
|
73
|
+
|
|
74
|
+
const { barBorderClass, lineDatapointClass, contentClasses, sparkLineStyles } = useDataVizClasses(config)
|
|
72
75
|
|
|
73
76
|
const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
|
|
74
77
|
|
|
78
|
+
const sortAsc = (a, b) => {
|
|
79
|
+
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sortDesc = (a, b) => {
|
|
83
|
+
return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
const handleChartAriaLabels = (state, testing = false) => {
|
|
76
87
|
if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
|
|
77
88
|
try {
|
|
@@ -88,7 +99,20 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
88
99
|
|
|
89
100
|
return ariaLabel
|
|
90
101
|
} catch (e) {
|
|
91
|
-
console.error(e.message)
|
|
102
|
+
console.error('COVE: ', e.message) // eslint-disable-line
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const handleLineType = lineType => {
|
|
107
|
+
switch (lineType) {
|
|
108
|
+
case 'dashed-sm':
|
|
109
|
+
return '5 5'
|
|
110
|
+
case 'dashed-md':
|
|
111
|
+
return '10 5'
|
|
112
|
+
case 'dashed-lg':
|
|
113
|
+
return '15 5'
|
|
114
|
+
default:
|
|
115
|
+
return 0
|
|
92
116
|
}
|
|
93
117
|
}
|
|
94
118
|
|
|
@@ -120,7 +144,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
120
144
|
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
|
|
121
145
|
}
|
|
122
146
|
} catch {
|
|
123
|
-
console.error(`Cannot parse URL: ${response.dataUrl}`)
|
|
147
|
+
console.error(`COVE: Cannot parse URL: ${response.dataUrl}`)
|
|
124
148
|
data = []
|
|
125
149
|
}
|
|
126
150
|
|
|
@@ -136,6 +160,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
let newConfig = { ...defaults, ...response }
|
|
163
|
+
if (newConfig.visualizationType === 'Box Plot') {
|
|
164
|
+
newConfig.legend.hide = true
|
|
165
|
+
}
|
|
139
166
|
if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
|
|
140
167
|
updateConfig(newConfig, data)
|
|
141
168
|
}
|
|
@@ -188,7 +215,7 @@ 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
|
|
@@ -218,9 +245,8 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
218
245
|
}
|
|
219
246
|
|
|
220
247
|
if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
|
|
221
|
-
|
|
222
|
-
let
|
|
223
|
-
let allValues = data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
|
|
248
|
+
let allKeys = newExcludedData ? newExcludedData.map(d => d[newConfig.xAxis.dataKey]) : data.map(d => d[newConfig.xAxis.dataKey])
|
|
249
|
+
let allValues = newExcludedData ? newExcludedData.map(d => Number(d[newConfig?.series[0]?.dataKey])) : data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
|
|
224
250
|
|
|
225
251
|
const uniqueArray = function (arrArg) {
|
|
226
252
|
return arrArg.filter(function (elem, pos, arr) {
|
|
@@ -234,37 +260,54 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
234
260
|
|
|
235
261
|
// group specific statistics
|
|
236
262
|
// prevent re-renders
|
|
263
|
+
if (!groups) return
|
|
237
264
|
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
|
-
|
|
265
|
+
try {
|
|
266
|
+
if (!g) throw new Error('No groups resolved in box plots')
|
|
267
|
+
|
|
268
|
+
// filter data by group
|
|
269
|
+
let filteredData = newExcludedData ? newExcludedData.filter(item => item[newConfig.xAxis.dataKey] === g) : data.filter(item => item[newConfig.xAxis.dataKey] === g)
|
|
270
|
+
let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
|
|
271
|
+
// let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
|
|
272
|
+
|
|
273
|
+
if (!filteredData) throw new Error('boxplots dont have data yet')
|
|
274
|
+
if (!plots) throw new Error('boxplots dont have plots yet')
|
|
275
|
+
if (newConfig.boxplot.firstQuartilePercentage === '') {
|
|
276
|
+
newConfig.boxplot.firstQuartilePercentage = 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (newConfig.boxplot.thirdQuartilePercentage === '') {
|
|
280
|
+
newConfig.boxplot.thirdQuartilePercentage = 0
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const q1 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.firstQuartilePercentage) / 100)
|
|
284
|
+
const q3 = d3.quantile(filteredDataValues, parseFloat(newConfig.boxplot.thirdQuartilePercentage) / 100)
|
|
285
|
+
const iqr = q3 - q1
|
|
286
|
+
const lowerBounds = q1 - (q3 - q1) * 1.5
|
|
287
|
+
const upperBounds = q3 + (q3 - q1) * 1.5
|
|
288
|
+
const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
|
|
289
|
+
let nonOutliers = filteredDataValues
|
|
290
|
+
|
|
291
|
+
nonOutliers = nonOutliers.filter(item => !outliers.includes(item))
|
|
292
|
+
|
|
293
|
+
plots.push({
|
|
294
|
+
columnCategory: g,
|
|
295
|
+
columnMax: Number(q3 + 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
|
|
296
|
+
columnThirdQuartile: Number(q3).toFixed(newConfig.dataFormat.roundTo),
|
|
297
|
+
columnMedian: Number(d3.median(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
298
|
+
columnFirstQuartile: q1.toFixed(newConfig.dataFormat.roundTo),
|
|
299
|
+
columnMin: Number(q1 - 1.5 * iqr).toFixed(newConfig.dataFormat.roundTo),
|
|
300
|
+
columnTotal: filteredDataValues.reduce((partialSum, a) => partialSum + a, 0),
|
|
301
|
+
columnSd: Number(d3.deviation(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
302
|
+
columnMean: Number(d3.mean(filteredDataValues)).toFixed(newConfig.dataFormat.roundTo),
|
|
303
|
+
columnIqr: Number(iqr).toFixed(newConfig.dataFormat.roundTo),
|
|
304
|
+
columnOutliers: outliers,
|
|
305
|
+
values: filteredDataValues,
|
|
306
|
+
nonOutlierValues: nonOutliers
|
|
307
|
+
})
|
|
308
|
+
} catch (e) {
|
|
309
|
+
console.error('COVE: ', e.message) // eslint-disable-line
|
|
310
|
+
}
|
|
268
311
|
})
|
|
269
312
|
|
|
270
313
|
// make deep copy so we can remove some fields for data
|
|
@@ -273,6 +316,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
273
316
|
tableData.map(table => {
|
|
274
317
|
delete table.columnIqr
|
|
275
318
|
delete table.nonOutlierValues
|
|
319
|
+
return null // resolve eslint
|
|
276
320
|
})
|
|
277
321
|
|
|
278
322
|
// any other data we can add to boxplots
|
|
@@ -295,7 +339,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
295
339
|
})
|
|
296
340
|
}
|
|
297
341
|
|
|
298
|
-
if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
|
|
342
|
+
if (((newConfig.visualizationType === 'Bar' || newConfig.visualizationType === 'Deviation Bar') && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
|
|
299
343
|
newConfig.runtime.xAxis = newConfig.yAxis
|
|
300
344
|
newConfig.runtime.yAxis = newConfig.xAxis
|
|
301
345
|
newConfig.runtime.horizontal = true
|
|
@@ -386,7 +430,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
386
430
|
}
|
|
387
431
|
|
|
388
432
|
setContainer(node)
|
|
389
|
-
}, [])
|
|
433
|
+
}, []) // eslint-disable-line
|
|
390
434
|
|
|
391
435
|
function isEmpty(obj) {
|
|
392
436
|
return Object.keys(obj).length === 0
|
|
@@ -395,7 +439,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
395
439
|
// Load data when component first mounts
|
|
396
440
|
useEffect(() => {
|
|
397
441
|
loadConfig()
|
|
398
|
-
}, [])
|
|
442
|
+
}, []) // eslint-disable-line
|
|
399
443
|
|
|
400
444
|
/**
|
|
401
445
|
* When cove has a config and container ref publish the cove_loaded event.
|
|
@@ -405,7 +449,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
405
449
|
publish('cove_loaded', { config: config })
|
|
406
450
|
setCoveLoadedEventRan(true)
|
|
407
451
|
}
|
|
408
|
-
}, [container, config])
|
|
452
|
+
}, [container, config]) // eslint-disable-line
|
|
409
453
|
|
|
410
454
|
/**
|
|
411
455
|
* Handles filter change events outside of COVE
|
|
@@ -448,20 +492,22 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
448
492
|
setConfig(newConfigHere)
|
|
449
493
|
setFilteredData(filterData(externalFilters, excludedData))
|
|
450
494
|
}
|
|
451
|
-
}, [externalFilters])
|
|
495
|
+
}, [externalFilters]) // eslint-disable-line
|
|
452
496
|
|
|
453
497
|
// Load data when configObj data changes
|
|
454
498
|
if (configObj) {
|
|
455
499
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
456
500
|
useEffect(() => {
|
|
457
501
|
loadConfig()
|
|
458
|
-
}, [configObj.data])
|
|
502
|
+
}, [configObj.data]) // eslint-disable-line
|
|
459
503
|
}
|
|
460
504
|
|
|
461
505
|
// Generates color palette to pass to child chart component
|
|
462
506
|
useEffect(() => {
|
|
463
507
|
if (stateData && config.xAxis && config.runtime.seriesKeys) {
|
|
464
|
-
|
|
508
|
+
const configPalette = config.visualizationType === 'Paired Bar' || config.visualizationType === 'Deviation Bar' ? config.twoColor.palette : config.palette
|
|
509
|
+
const allPalettes = { ...colorPalettes, ...twoColorPalette }
|
|
510
|
+
let palette = config.customColors || allPalettes[configPalette]
|
|
465
511
|
let numberOfKeys = config.runtime.seriesKeys.length
|
|
466
512
|
let newColorScale
|
|
467
513
|
|
|
@@ -484,7 +530,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
484
530
|
if (config && stateData && config.sortData) {
|
|
485
531
|
stateData.sort(sortData)
|
|
486
532
|
}
|
|
487
|
-
}, [config, stateData])
|
|
533
|
+
}, [config, stateData]) // eslint-disable-line
|
|
488
534
|
|
|
489
535
|
// Called on legend click, highlights/unhighlights the data series with the given label
|
|
490
536
|
const highlight = label => {
|
|
@@ -543,33 +589,6 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
543
589
|
return timeFormat(config.runtime[section].dateDisplayFormat)(date)
|
|
544
590
|
}
|
|
545
591
|
|
|
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
592
|
// function calculates the width of given text and its font-size
|
|
574
593
|
function getTextWidth(text, font) {
|
|
575
594
|
const canvas = document.createElement('canvas')
|
|
@@ -584,11 +603,19 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
584
603
|
const formatNumber = (num, axis) => {
|
|
585
604
|
// if num is NaN return num
|
|
586
605
|
if (isNaN(num) || !num) return num
|
|
606
|
+
// Check if the input number is negative
|
|
607
|
+
const isNegative = num < 0
|
|
608
|
+
|
|
609
|
+
// If the input number is negative, take the absolute value
|
|
610
|
+
if (isNegative) {
|
|
611
|
+
num = Math.abs(num)
|
|
612
|
+
}
|
|
587
613
|
|
|
588
614
|
// destructure dataFormat values
|
|
589
615
|
let {
|
|
590
|
-
dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, rightPrefix, rightSuffix }
|
|
616
|
+
dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, bottomRoundTo, rightPrefix, rightSuffix, bottomPrefix, bottomSuffix, bottomAbbreviated }
|
|
591
617
|
} = config
|
|
618
|
+
|
|
592
619
|
let formatSuffix = format('.2s')
|
|
593
620
|
|
|
594
621
|
// check if value contains comma and remove it. later will add comma below.
|
|
@@ -596,14 +623,15 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
596
623
|
|
|
597
624
|
let original = num
|
|
598
625
|
let stringFormattingOptions
|
|
599
|
-
|
|
600
|
-
if (axis !== 'right') {
|
|
626
|
+
if (axis === 'left') {
|
|
601
627
|
stringFormattingOptions = {
|
|
602
628
|
useGrouping: config.dataFormat.commas ? true : false,
|
|
603
629
|
minimumFractionDigits: roundTo ? Number(roundTo) : 0,
|
|
604
630
|
maximumFractionDigits: roundTo ? Number(roundTo) : 0
|
|
605
631
|
}
|
|
606
|
-
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (axis === 'right') {
|
|
607
635
|
stringFormattingOptions = {
|
|
608
636
|
useGrouping: config.dataFormat.rightCommas ? true : false,
|
|
609
637
|
minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
|
|
@@ -611,6 +639,14 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
611
639
|
}
|
|
612
640
|
}
|
|
613
641
|
|
|
642
|
+
if (axis === 'bottom') {
|
|
643
|
+
stringFormattingOptions = {
|
|
644
|
+
useGrouping: config.dataFormat.bottomCommas ? true : false,
|
|
645
|
+
minimumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0,
|
|
646
|
+
maximumFractionDigits: bottomRoundTo ? Number(bottomRoundTo) : 0
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
614
650
|
num = numberFromString(num)
|
|
615
651
|
|
|
616
652
|
if (isNaN(num)) {
|
|
@@ -631,8 +667,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
631
667
|
// Use commas also updates bars and the data table
|
|
632
668
|
// We can't use commas when we're formatting the dataFormatted number
|
|
633
669
|
// Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
|
|
670
|
+
//
|
|
671
|
+
// Edge case for small numbers with decimals
|
|
672
|
+
// - if roundTo undefined which means it is blank, then do not round
|
|
634
673
|
if ((axis === 'left' && commas && abbreviated) || (axis === 'bottom' && commas && abbreviated)) {
|
|
635
|
-
num = num
|
|
674
|
+
num = num // eslint-disable-line
|
|
636
675
|
} else {
|
|
637
676
|
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
638
677
|
}
|
|
@@ -642,11 +681,11 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
642
681
|
num = formatSuffix(parseFloat(num)).replace('G', 'B')
|
|
643
682
|
}
|
|
644
683
|
|
|
645
|
-
if (
|
|
684
|
+
if (bottomAbbreviated && axis === 'bottom') {
|
|
646
685
|
num = formatSuffix(parseFloat(num)).replace('G', 'B')
|
|
647
686
|
}
|
|
648
687
|
|
|
649
|
-
if (prefix && axis
|
|
688
|
+
if (prefix && axis === 'left') {
|
|
650
689
|
result += prefix
|
|
651
690
|
}
|
|
652
691
|
|
|
@@ -654,9 +693,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
654
693
|
result += rightPrefix
|
|
655
694
|
}
|
|
656
695
|
|
|
696
|
+
if (bottomPrefix && axis === 'bottom') {
|
|
697
|
+
result += bottomPrefix
|
|
698
|
+
}
|
|
699
|
+
|
|
657
700
|
result += num
|
|
658
701
|
|
|
659
|
-
if (suffix && axis
|
|
702
|
+
if (suffix && axis === 'left') {
|
|
660
703
|
result += suffix
|
|
661
704
|
}
|
|
662
705
|
|
|
@@ -664,6 +707,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
664
707
|
result += rightSuffix
|
|
665
708
|
}
|
|
666
709
|
|
|
710
|
+
if (bottomSuffix && axis === 'bottom') {
|
|
711
|
+
result += bottomSuffix
|
|
712
|
+
}
|
|
713
|
+
if (isNegative) {
|
|
714
|
+
result = '-' + result
|
|
715
|
+
}
|
|
716
|
+
|
|
667
717
|
return String(result)
|
|
668
718
|
}
|
|
669
719
|
|
|
@@ -675,7 +725,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
675
725
|
Combo: <LinearChart />,
|
|
676
726
|
Pie: <PieChart />,
|
|
677
727
|
'Box Plot': <LinearChart />,
|
|
678
|
-
'
|
|
728
|
+
'Area Chart': <LinearChart />,
|
|
729
|
+
'Scatter Plot': <LinearChart />,
|
|
730
|
+
'Deviation Bar': <LinearChart />
|
|
679
731
|
}
|
|
680
732
|
|
|
681
733
|
const missingRequiredSections = () => {
|
|
@@ -707,7 +759,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
707
759
|
<div className='cdc-chart-inner-container'>
|
|
708
760
|
{/* Title */}
|
|
709
761
|
|
|
710
|
-
{title && (
|
|
762
|
+
{title && config.showTitle && (
|
|
711
763
|
<div role='heading' className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>
|
|
712
764
|
{config && <sup className='superTitle'>{parse(config.superTitle || '')}</sup>}
|
|
713
765
|
<div>{parse(title)}</div>
|
|
@@ -721,7 +773,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
721
773
|
{/* Visualization */}
|
|
722
774
|
{config?.introText && <section className='introText'>{parse(config.introText)}</section>}
|
|
723
775
|
<div
|
|
724
|
-
style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
|
|
776
|
+
style={{ marginBottom: config.legend.position !== 'bottom' && currentViewport !== 'sm' && currentViewport !== 'xs' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
|
|
725
777
|
className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
|
|
726
778
|
>
|
|
727
779
|
{/* All charts except sparkline */}
|
|
@@ -800,10 +852,12 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
800
852
|
dynamicLegendItems,
|
|
801
853
|
setDynamicLegendItems,
|
|
802
854
|
filterData,
|
|
855
|
+
imageId,
|
|
856
|
+
handleLineType,
|
|
803
857
|
isNumber,
|
|
804
858
|
cleanData,
|
|
805
|
-
|
|
806
|
-
|
|
859
|
+
getTextWidth,
|
|
860
|
+
twoColorPalette
|
|
807
861
|
}
|
|
808
862
|
|
|
809
863
|
const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React, { useContext, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
// cdc
|
|
4
|
+
import ConfigContext from '../ConfigContext'
|
|
5
|
+
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
6
|
+
import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
|
|
7
|
+
|
|
8
|
+
// visx & d3
|
|
9
|
+
import { AreaClosed, LinePath, Bar } from '@visx/shape'
|
|
10
|
+
import { Group } from '@visx/group'
|
|
11
|
+
import * as allCurves from '@visx/curve'
|
|
12
|
+
import { useTooltip, useTooltipInPortal, defaultStyles } from '@visx/tooltip'
|
|
13
|
+
import { localPoint } from '@visx/event'
|
|
14
|
+
import { bisector } from 'd3-array'
|
|
15
|
+
|
|
16
|
+
const CoveAreaChart = ({ xScale, yScale, yMax, xMax }) => {
|
|
17
|
+
// enable various console logs in the file
|
|
18
|
+
const DEBUG = false
|
|
19
|
+
|
|
20
|
+
// import data from context
|
|
21
|
+
const { transformedData: data, config, handleLineType, parseDate, formatDate, formatNumber, seriesHighlight } = useContext(ConfigContext)
|
|
22
|
+
|
|
23
|
+
// import tooltip helpers
|
|
24
|
+
const { tooltipData, showTooltip } = useTooltip()
|
|
25
|
+
|
|
26
|
+
// used for offset on tooltip hover
|
|
27
|
+
let isEditor = window.location.href.includes('editor=true')
|
|
28
|
+
|
|
29
|
+
// here we're inside of the svg,
|
|
30
|
+
// it appears we need to use TooltipInPortal.
|
|
31
|
+
const { TooltipInPortal } = useTooltipInPortal({
|
|
32
|
+
detectBounds: true,
|
|
33
|
+
// when tooltip containers are scrolled, this will correctly update the Tooltip position
|
|
34
|
+
scroll: true
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Draw transparent bars over the chart to get tooltip data
|
|
38
|
+
// Turn DEBUG on for additional context.
|
|
39
|
+
let barThickness = xMax / config.data.length
|
|
40
|
+
let barThicknessAdjusted = barThickness * (config.barThickness || 0.8)
|
|
41
|
+
let offset = (barThickness * (1 - (config.barThickness || 0.8))) / 2
|
|
42
|
+
|
|
43
|
+
// Tooltip helper for getting data to the closest date/category hovered.
|
|
44
|
+
const getXValueFromCoordinate = x => {
|
|
45
|
+
if (config.xAxis.type === 'categorical') {
|
|
46
|
+
let eachBand = xScale.step()
|
|
47
|
+
let numerator = x
|
|
48
|
+
const index = Math.floor(Number(numerator) / eachBand)
|
|
49
|
+
return xScale.domain()[index - 1] // fixes off by 1 error
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config.xAxis.type === 'date') {
|
|
53
|
+
const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
|
|
54
|
+
const x0 = xScale.invert(x)
|
|
55
|
+
const index = bisectDate(config.data, x0, 1)
|
|
56
|
+
const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
|
|
57
|
+
return val
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const handleMouseOver = useCallback(
|
|
62
|
+
(e, data) => {
|
|
63
|
+
// get the svg coordinates of the mouse
|
|
64
|
+
// and get the closest values
|
|
65
|
+
const eventSvgCoords = localPoint(e)
|
|
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]
|
|
81
|
+
|
|
82
|
+
itemsToLoop.map(seriesKey => {
|
|
83
|
+
return Object.entries(yScaleValues[0]).forEach(item => item[0] === seriesKey && seriesToInclude.push(item))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// filter out the series that aren't added to the map.
|
|
87
|
+
seriesToInclude.map(series => yScaleMaxValues.push(Number(yScaleValues[0][series])))
|
|
88
|
+
seriesToInclude = Object.fromEntries(seriesToInclude)
|
|
89
|
+
|
|
90
|
+
let tooltipData = {}
|
|
91
|
+
tooltipData.data = seriesToInclude
|
|
92
|
+
tooltipData.dataXPosition = isEditor ? 300 + x + 20 : x + 20
|
|
93
|
+
tooltipData.dataYPosition = y - 20
|
|
94
|
+
|
|
95
|
+
let tooltipInformation = {
|
|
96
|
+
tooltipData: tooltipData,
|
|
97
|
+
tooltipTop: 0,
|
|
98
|
+
tooltipValues: yScaleValues,
|
|
99
|
+
tooltipLeft: x
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
showTooltip(tooltipInformation)
|
|
103
|
+
},
|
|
104
|
+
[showTooltip] // eslint-disable-line
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const TooltipListItem = ({ item }) => {
|
|
108
|
+
const [label, value] = item
|
|
109
|
+
return label === config.xAxis.dataKey ? `${label}: ${value}` : `${label}: ${formatNumber(value, 'left')}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handleX = d => {
|
|
113
|
+
return config.xAxis.type === 'date' ? xScale(parseDate(d[config.xAxis.dataKey])) : xScale(d[config.xAxis.dataKey])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const handleY = (d, index) => {
|
|
117
|
+
return yScale(d[config.series[index].dataKey])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
data && (
|
|
122
|
+
<ErrorBoundary component='AreaChart'>
|
|
123
|
+
<Group className='area-chart' key='area-wrapper' left={config.yAxis.size}>
|
|
124
|
+
{config.series.map((s, index) => {
|
|
125
|
+
let seriesColor = colorPalettesChart[config.palette][index]
|
|
126
|
+
let curveType = allCurves[s.lineType]
|
|
127
|
+
let transparentArea = config.legend.behavior === 'highlight' && seriesHighlight.length > 0 && seriesHighlight.indexOf(s.dataKey) === -1
|
|
128
|
+
let displayArea = config.legend.behavior === 'highlight' || seriesHighlight.length === 0 || seriesHighlight.indexOf(s.dataKey) !== -1
|
|
129
|
+
|
|
130
|
+
data.map(d => xScale(parseDate(d[config.xAxis.dataKey])))
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
{/* prettier-ignore */}
|
|
135
|
+
{/* this is the line that appears on top of the area chart */}
|
|
136
|
+
<LinePath data={data} x={d => handleX(d)} y={d => yScale(d[config.series[index].dataKey])} stroke={seriesColor} strokeWidth={2} strokeOpacity={1} shapeRendering='geometricPrecision' curve={curveType} strokeDasharray={s.type ? handleLineType(s.type) : 0} />
|
|
137
|
+
|
|
138
|
+
{/* prettier-ignore */}
|
|
139
|
+
{/* filled in sections */}
|
|
140
|
+
<AreaClosed key={'area-chart'} fill={displayArea ? seriesColor : 'transparent'} fillOpacity={transparentArea ? 0.25 : 0.5} data={data} x={d => handleX(d)} y={d => handleY(d, index)} yScale={yScale} curve={curveType} strokeDasharray={s.type ? handleLineType(s.typ) : 0} />
|
|
141
|
+
|
|
142
|
+
<Bar x={d => handleX(d)} y={d => yScale(d[config.series[index].dataKey])} yScale={yScale} width={xMax} height={yMax} fill={DEBUG ? 'red' : 'transparent'} fillOpacity={0.05} style={DEBUG ? { stroke: 'black', strokeWidth: 2 } : {}} onMouseMove={e => handleMouseOver(e, data)} />
|
|
143
|
+
|
|
144
|
+
{/* circles that appear on hover */}
|
|
145
|
+
{tooltipData && (
|
|
146
|
+
<circle
|
|
147
|
+
cx={config.xAxis.type === 'categorical' ? xScale(tooltipData.data[config.xAxis.dataKey]) : xScale(parseDate(tooltipData.data[config.xAxis.dataKey]))}
|
|
148
|
+
cy={yScale(tooltipData.data[s.dataKey])}
|
|
149
|
+
r={4.5}
|
|
150
|
+
opacity={1}
|
|
151
|
+
fillOpacity={1}
|
|
152
|
+
fill={seriesColor}
|
|
153
|
+
style={{ filter: 'unset', opacity: 1 }}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* bars to handle tooltips */}
|
|
158
|
+
{DEBUG &&
|
|
159
|
+
config.data.map((item, index) => {
|
|
160
|
+
return (
|
|
161
|
+
<Bar
|
|
162
|
+
className='bar-here'
|
|
163
|
+
x={barThickness * index + offset}
|
|
164
|
+
y={d => yScale(d[config.series[index].dataKey])}
|
|
165
|
+
yScale={yScale}
|
|
166
|
+
width={barThicknessAdjusted}
|
|
167
|
+
height={yMax}
|
|
168
|
+
fill={'transparent'}
|
|
169
|
+
fillOpacity={1}
|
|
170
|
+
style={{ stroke: 'black', strokeWidth: 2 }}
|
|
171
|
+
onMouseMove={e => handleMouseOver(e, data)}
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
})}
|
|
175
|
+
|
|
176
|
+
{tooltipData && (
|
|
177
|
+
<TooltipInPortal key={Math.random()} top={tooltipData.dataYPosition} left={tooltipData.dataXPosition} style={defaultStyles}>
|
|
178
|
+
<Group x={config.yAxis.size + 10} y={0}>
|
|
179
|
+
<ul style={{ listStyle: 'none', paddingLeft: 'unset' }}>
|
|
180
|
+
{Object.entries(tooltipData.data).map(item => (
|
|
181
|
+
<li>
|
|
182
|
+
<TooltipListItem item={item} />
|
|
183
|
+
</li>
|
|
184
|
+
))}
|
|
185
|
+
</ul>
|
|
186
|
+
</Group>
|
|
187
|
+
</TooltipInPortal>
|
|
188
|
+
)}
|
|
189
|
+
</>
|
|
190
|
+
)
|
|
191
|
+
})}
|
|
192
|
+
</Group>
|
|
193
|
+
</ErrorBoundary>
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default CoveAreaChart
|