@cdc/chart 4.22.11 → 4.23.1
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/495.js +3 -0
- package/dist/703.js +1 -0
- package/dist/cdcchart.js +723 -6
- package/examples/box-plot-data.json +71 -0
- package/examples/box-plot.csv +5 -0
- package/examples/{private/yaxis-test.json → box-plot.json} +47 -56
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +3 -1
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +85 -16
- package/examples/new-data.csv +17 -0
- package/examples/newdata.json +90 -0
- package/package.json +3 -2
- package/src/CdcChart.tsx +150 -94
- package/src/components/BarChart.tsx +156 -226
- package/src/components/BoxPlot.js +92 -0
- package/src/components/DataTable.tsx +28 -12
- package/src/components/EditorPanel.js +151 -104
- package/src/components/Filters.js +131 -0
- package/src/components/Legend.js +8 -1
- package/src/components/LineChart.tsx +64 -13
- package/src/components/LinearChart.tsx +120 -81
- package/src/components/PairedBarChart.tsx +1 -1
- package/src/components/PieChart.tsx +12 -2
- package/src/components/useIntersectionObserver.tsx +9 -7
- package/src/data/initial-state.js +14 -8
- package/src/hooks/useReduceData.ts +8 -5
- package/src/index.html +51 -51
- package/src/scss/DataTable.scss +1 -1
- package/src/scss/main.scss +53 -22
- package/examples/private/filters.json +0 -170
- package/examples/private/line-test-data.json +0 -22
- package/examples/private/line-test-two.json +0 -210
- package/examples/private/line-test.json +0 -102
- package/examples/private/new.json +0 -48800
- package/examples/private/newtest.csv +0 -101
- package/examples/private/shawn.json +0 -1106
- package/examples/private/test.json +0 -10124
- package/examples/private/yaxis-testing.csv +0 -27
- package/examples/private/yaxis.json +0 -28
package/src/CdcChart.tsx
CHANGED
|
@@ -4,13 +4,16 @@ import React, { useState, useEffect, useCallback } from 'react'
|
|
|
4
4
|
import 'core-js/stable'
|
|
5
5
|
import ResizeObserver from 'resize-observer-polyfill'
|
|
6
6
|
import 'whatwg-fetch'
|
|
7
|
+
import * as d3 from 'd3-array'
|
|
7
8
|
|
|
8
9
|
// External Libraries
|
|
9
10
|
import { scaleOrdinal } from '@visx/scale'
|
|
10
11
|
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
11
12
|
import { timeParse, timeFormat } from 'd3-time-format'
|
|
13
|
+
import { format } from 'd3-format'
|
|
12
14
|
import Papa from 'papaparse'
|
|
13
15
|
import parse from 'html-react-parser'
|
|
16
|
+
import { Base64 } from 'js-base64'
|
|
14
17
|
|
|
15
18
|
// Primary Components
|
|
16
19
|
import Context from './context'
|
|
@@ -29,6 +32,8 @@ import DataTable from './components/DataTable'
|
|
|
29
32
|
import defaults from './data/initial-state'
|
|
30
33
|
import EditorPanel from './components/EditorPanel'
|
|
31
34
|
import Loading from '@cdc/core/components/Loading'
|
|
35
|
+
import Filters from './components/Filters'
|
|
36
|
+
import CoveMediaControls from '@cdc/core/helpers/CoveMediaControls'
|
|
32
37
|
|
|
33
38
|
// helpers
|
|
34
39
|
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
@@ -40,7 +45,6 @@ import './scss/main.scss'
|
|
|
40
45
|
|
|
41
46
|
export default function CdcChart({ configUrl, config: configObj, isEditor = false, isDashboard = false, setConfig: setParentConfig, setEditing, hostname, link }: { configUrl?: string; config?: any; isEditor?: boolean; isDashboard?: boolean; setConfig?; setEditing?; hostname?; link?: any }) {
|
|
42
47
|
const transform = new DataTransform()
|
|
43
|
-
|
|
44
48
|
interface keyable {
|
|
45
49
|
[key: string]: any
|
|
46
50
|
}
|
|
@@ -58,10 +62,13 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
58
62
|
const [container, setContainer] = useState()
|
|
59
63
|
const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
|
|
60
64
|
const [dynamicLegendItems, setDynamicLegendItems] = useState([])
|
|
65
|
+
const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
61
66
|
|
|
62
67
|
const legendGlyphSize = 15
|
|
63
68
|
const legendGlyphSizeHalf = legendGlyphSize / 2
|
|
64
69
|
|
|
70
|
+
// Destructure items from config for more readable JSX
|
|
71
|
+
const { legend, title, description, visualizationType } = config
|
|
65
72
|
const { barBorderClass, lineDatapointClass, contentClasses, innerContainerClasses, sparkLineStyles } = useDataVizClasses(config)
|
|
66
73
|
|
|
67
74
|
const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
|
|
@@ -211,6 +218,66 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
211
218
|
: []
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
if (newConfig.visualizationType === 'Box Plot' && newConfig.series) {
|
|
222
|
+
console.log('hit', newConfig)
|
|
223
|
+
|
|
224
|
+
// stats
|
|
225
|
+
let allKeys = data.map(d => d[newConfig.xAxis.dataKey])
|
|
226
|
+
let allValues = data.map(d => Number(d[newConfig?.series[0]?.dataKey]))
|
|
227
|
+
|
|
228
|
+
const uniqueArray = function (arrArg) {
|
|
229
|
+
return arrArg.filter(function (elem, pos, arr) {
|
|
230
|
+
return arr.indexOf(elem) === pos
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const groups = uniqueArray(allKeys)
|
|
235
|
+
const plots = []
|
|
236
|
+
|
|
237
|
+
console.log('d', data)
|
|
238
|
+
console.log('newConfig', newConfig)
|
|
239
|
+
console.log('groups', groups)
|
|
240
|
+
console.log('allKeys', allKeys)
|
|
241
|
+
console.log('allValues', allValues)
|
|
242
|
+
|
|
243
|
+
// group specific statistics
|
|
244
|
+
// prevent re-renders
|
|
245
|
+
groups.map((g, index) => {
|
|
246
|
+
if (!g) return
|
|
247
|
+
// filter data by group
|
|
248
|
+
let filteredData = data.filter(item => item[newConfig.xAxis.dataKey] === g)
|
|
249
|
+
let filteredDataValues = filteredData.map(item => Number(item[newConfig?.series[0]?.dataKey]))
|
|
250
|
+
console.log('g', g)
|
|
251
|
+
console.log('item', filteredData)
|
|
252
|
+
console.log('item', newConfig)
|
|
253
|
+
// let filteredDataValues = filteredData.map(item => Number(item[newConfig.yAxis.dataKey]))
|
|
254
|
+
|
|
255
|
+
const q1 = d3.quantile(filteredDataValues, 0.25)
|
|
256
|
+
const q3 = d3.quantile(filteredDataValues, 0.75)
|
|
257
|
+
const iqr = q3 - q1
|
|
258
|
+
const lowerBounds = q1 - (q3 - q1) * 1.5
|
|
259
|
+
const upperBounds = q3 + (q3 - q1) * 1.5
|
|
260
|
+
const outliers = filteredDataValues.filter(v => v < lowerBounds || v > upperBounds)
|
|
261
|
+
plots.push({
|
|
262
|
+
columnCategory: g,
|
|
263
|
+
columnMean: d3.mean(filteredDataValues),
|
|
264
|
+
columnMedian: d3.median(filteredDataValues),
|
|
265
|
+
columnFirstQuartile: q1,
|
|
266
|
+
columnThirdQuartile: q3,
|
|
267
|
+
columnMin: q1 - 1.5 * iqr,
|
|
268
|
+
columnMax: q3 + 1.5 * iqr,
|
|
269
|
+
columnIqr: iqr,
|
|
270
|
+
columnOutliers: outliers,
|
|
271
|
+
values: filteredDataValues
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// any other data we can add to boxplots
|
|
276
|
+
newConfig.boxplot['allValues'] = allValues
|
|
277
|
+
newConfig.boxplot['categories'] = groups
|
|
278
|
+
newConfig.boxplot.push(...plots)
|
|
279
|
+
}
|
|
280
|
+
|
|
214
281
|
if (newConfig.visualizationType === 'Combo' && newConfig.series) {
|
|
215
282
|
newConfig.runtime.barSeriesKeys = []
|
|
216
283
|
newConfig.runtime.lineSeriesKeys = []
|
|
@@ -470,28 +537,63 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
470
537
|
return timeFormat(config.runtime[section].dateDisplayFormat)(date)
|
|
471
538
|
}
|
|
472
539
|
|
|
540
|
+
const DownloadButton = ({ data }: any, type = 'link') => {
|
|
541
|
+
const fileName = `${config.title.substring(0, 50)}.csv`
|
|
542
|
+
|
|
543
|
+
const csvData = Papa.unparse(data)
|
|
544
|
+
|
|
545
|
+
const saveBlob = () => {
|
|
546
|
+
//@ts-ignore
|
|
547
|
+
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
548
|
+
const dataBlob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
|
|
549
|
+
//@ts-ignore
|
|
550
|
+
window.navigator.msSaveBlob(dataBlob, fileName)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (type === 'download') {
|
|
555
|
+
return (
|
|
556
|
+
<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`}>
|
|
557
|
+
Download Data (CSV)
|
|
558
|
+
</a>
|
|
559
|
+
)
|
|
560
|
+
} else {
|
|
561
|
+
return (
|
|
562
|
+
<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`}>
|
|
563
|
+
Download Data (CSV)
|
|
564
|
+
</a>
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
473
569
|
// Format numeric data based on settings in config
|
|
474
570
|
const formatNumber = (num, axis) => {
|
|
475
|
-
// check if value contains comma and remove it. later will add comma below.
|
|
476
|
-
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
477
571
|
// if num is NaN return num
|
|
478
572
|
if (isNaN(num) || !num) return num
|
|
479
573
|
|
|
574
|
+
// destructure dataFormat values
|
|
575
|
+
let {
|
|
576
|
+
dataFormat: { commas, abbreviated, roundTo, prefix, suffix, rightRoundTo, rightPrefix, rightSuffix }
|
|
577
|
+
} = config
|
|
578
|
+
let formatSuffix = format('.2s')
|
|
579
|
+
|
|
580
|
+
// check if value contains comma and remove it. later will add comma below.
|
|
581
|
+
if (String(num).indexOf(',') !== -1) num = num.replaceAll(',', '')
|
|
582
|
+
|
|
480
583
|
let original = num
|
|
481
|
-
let prefix = config.dataFormat.prefix
|
|
482
584
|
let stringFormattingOptions
|
|
483
585
|
|
|
484
586
|
if (axis !== 'right') {
|
|
485
587
|
stringFormattingOptions = {
|
|
486
588
|
useGrouping: config.dataFormat.commas ? true : false,
|
|
487
|
-
minimumFractionDigits:
|
|
488
|
-
maximumFractionDigits:
|
|
589
|
+
minimumFractionDigits: roundTo ? Number(roundTo) : 0,
|
|
590
|
+
maximumFractionDigits: roundTo ? Number(roundTo) : 0
|
|
489
591
|
}
|
|
490
592
|
} else {
|
|
491
593
|
stringFormattingOptions = {
|
|
492
594
|
useGrouping: config.dataFormat.rightCommas ? true : false,
|
|
493
|
-
minimumFractionDigits:
|
|
494
|
-
maximumFractionDigits:
|
|
595
|
+
minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
|
|
596
|
+
maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
|
|
495
597
|
}
|
|
496
598
|
}
|
|
497
599
|
|
|
@@ -510,109 +612,55 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
510
612
|
num = cutoff
|
|
511
613
|
}
|
|
512
614
|
}
|
|
513
|
-
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
514
615
|
|
|
616
|
+
// When we're formatting the left axis
|
|
617
|
+
// Use commas also updates bars and the data table
|
|
618
|
+
// We can't use commas when we're formatting the dataFormatted number
|
|
619
|
+
// Example: commas -> 12,000; abbreviated -> 12k (correct); abbreviated & commas -> 12 (incorrect)
|
|
620
|
+
if (axis === 'left' && commas && abbreviated) {
|
|
621
|
+
num = num
|
|
622
|
+
} else {
|
|
623
|
+
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
624
|
+
}
|
|
515
625
|
let result = ''
|
|
516
626
|
|
|
627
|
+
if (abbreviated && axis === 'left') {
|
|
628
|
+
num = formatSuffix(parseFloat(num)).replace('G', 'B')
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (abbreviated && axis === 'bottom') {
|
|
632
|
+
num = formatSuffix(parseFloat(num)).replace('G', 'B')
|
|
633
|
+
}
|
|
634
|
+
|
|
517
635
|
if (prefix && axis !== 'right') {
|
|
518
636
|
result += prefix
|
|
519
637
|
}
|
|
520
638
|
|
|
521
|
-
if (
|
|
522
|
-
result +=
|
|
639
|
+
if (rightPrefix && axis === 'right') {
|
|
640
|
+
result += rightPrefix
|
|
523
641
|
}
|
|
524
642
|
|
|
525
643
|
result += num
|
|
526
644
|
|
|
527
|
-
if (
|
|
528
|
-
result +=
|
|
645
|
+
if (suffix && axis !== 'right') {
|
|
646
|
+
result += suffix
|
|
529
647
|
}
|
|
530
648
|
|
|
531
|
-
if (
|
|
532
|
-
result +=
|
|
649
|
+
if (rightSuffix && axis === 'right') {
|
|
650
|
+
result += rightSuffix
|
|
533
651
|
}
|
|
534
652
|
|
|
535
653
|
return String(result)
|
|
536
654
|
}
|
|
537
655
|
|
|
538
|
-
// Destructure items from config for more readable JSX
|
|
539
|
-
const { legend, title, description, visualizationType } = config
|
|
540
|
-
|
|
541
656
|
// Select appropriate chart type
|
|
542
657
|
const chartComponents = {
|
|
543
658
|
'Paired Bar': <LinearChart />,
|
|
544
659
|
Bar: <LinearChart />,
|
|
545
660
|
Line: <LinearChart />,
|
|
546
661
|
Combo: <LinearChart />,
|
|
547
|
-
Pie: <PieChart
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const Filters = () => {
|
|
551
|
-
const changeFilterActive = (index, value) => {
|
|
552
|
-
let newFilters = config.filters
|
|
553
|
-
|
|
554
|
-
newFilters[index].active = value
|
|
555
|
-
|
|
556
|
-
setConfig({ ...config, filters: newFilters })
|
|
557
|
-
|
|
558
|
-
setFilteredData(filterData(newFilters, excludedData))
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const announceChange = text => {}
|
|
562
|
-
|
|
563
|
-
let filterList = ''
|
|
564
|
-
if (config.filters) {
|
|
565
|
-
filterList = config.filters.map((singleFilter, index) => {
|
|
566
|
-
const values = []
|
|
567
|
-
const sortAsc = (a, b) => {
|
|
568
|
-
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const sortDesc = (a, b) => {
|
|
572
|
-
return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (!singleFilter.order || singleFilter.order === '') {
|
|
576
|
-
singleFilter.order = 'asc'
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (singleFilter.order === 'desc') {
|
|
580
|
-
singleFilter.values = singleFilter.values.sort(sortDesc)
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
if (singleFilter.order === 'asc') {
|
|
584
|
-
singleFilter.values = singleFilter.values.sort(sortAsc)
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
singleFilter.values.forEach((filterOption, index) => {
|
|
588
|
-
values.push(
|
|
589
|
-
<option key={index} value={filterOption}>
|
|
590
|
-
{filterOption}
|
|
591
|
-
</option>
|
|
592
|
-
)
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
return (
|
|
596
|
-
<div className='single-filter' key={index}>
|
|
597
|
-
<label htmlFor={`filter-${index}`}>{singleFilter.label}</label>
|
|
598
|
-
<select
|
|
599
|
-
id={`filter-${index}`}
|
|
600
|
-
className='filter-select'
|
|
601
|
-
data-index='0'
|
|
602
|
-
value={singleFilter.active}
|
|
603
|
-
onChange={val => {
|
|
604
|
-
changeFilterActive(index, val.target.value)
|
|
605
|
-
announceChange(`Filter ${singleFilter.label} value has been changed to ${val.target.value}, please reference the data table to see updated values.`)
|
|
606
|
-
}}
|
|
607
|
-
>
|
|
608
|
-
{values}
|
|
609
|
-
</select>
|
|
610
|
-
</div>
|
|
611
|
-
)
|
|
612
|
-
})
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return <section className='filters-section'>{filterList}</section>
|
|
662
|
+
Pie: <PieChart />,
|
|
663
|
+
'Box Plot': <LinearChart />
|
|
616
664
|
}
|
|
617
665
|
|
|
618
666
|
const missingRequiredSections = () => {
|
|
@@ -656,11 +704,10 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
656
704
|
{/* Filters */}
|
|
657
705
|
{config.filters && !externalFilters && <Filters />}
|
|
658
706
|
{/* Visualization */}
|
|
659
|
-
{config?.introText && <section className=
|
|
707
|
+
{config?.introText && <section className='introText'>{parse(config.introText)}</section>}
|
|
660
708
|
<div
|
|
661
|
-
|
|
662
|
-
}${config.legend.hide ?
|
|
663
|
-
}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
|
|
709
|
+
style={{ marginBottom: config.legend.position !== 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px' }}
|
|
710
|
+
className={`chart-container ${config.legend.position === 'bottom' ? 'bottom' : ''}${config.legend.hide ? ' legend-hidden' : ''}${lineDatapointClass}${barBorderClass} ${contentClasses.join(' ')}`}
|
|
664
711
|
>
|
|
665
712
|
{/* All charts except sparkline */}
|
|
666
713
|
{config.visualizationType !== 'Spark Line' && chartComponents[visualizationType]}
|
|
@@ -686,10 +733,17 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
686
733
|
{link && link}
|
|
687
734
|
{/* Description */}
|
|
688
735
|
{description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
|
|
689
|
-
{/* Data Table */}
|
|
690
736
|
|
|
737
|
+
{/* buttons */}
|
|
738
|
+
<CoveMediaControls.Section classes={['download-buttons']}>
|
|
739
|
+
{config.table.showDownloadImgButton && <CoveMediaControls.Button text='Download Image' title='Download Chart as Image' type='image' state={config} elementToCapture={imageId} />}
|
|
740
|
+
{config.table.showDownloadPdfButton && <CoveMediaControls.Button text='Download PDF' title='Download Chart as PDF' type='pdf' state={config} elementToCapture={imageId} />}
|
|
741
|
+
</CoveMediaControls.Section>
|
|
742
|
+
|
|
743
|
+
{/* Data Table */}
|
|
691
744
|
{config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && <DataTable />}
|
|
692
745
|
{config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
|
|
746
|
+
{/* show pdf or image button */}
|
|
693
747
|
</div>
|
|
694
748
|
)}
|
|
695
749
|
</>
|
|
@@ -729,7 +783,9 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
729
783
|
legend,
|
|
730
784
|
setSeriesHighlight,
|
|
731
785
|
dynamicLegendItems,
|
|
732
|
-
setDynamicLegendItems
|
|
786
|
+
setDynamicLegendItems,
|
|
787
|
+
filterData,
|
|
788
|
+
imageId
|
|
733
789
|
}
|
|
734
790
|
|
|
735
791
|
const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
|
|
@@ -740,7 +796,7 @@ export default function CdcChart({ configUrl, config: configObj, isEditor = fals
|
|
|
740
796
|
|
|
741
797
|
return (
|
|
742
798
|
<Context.Provider value={contextValues}>
|
|
743
|
-
<div className={`${classes.join(' ')}`} ref={outerContainerRef} data-lollipop={config.isLollipopChart}>
|
|
799
|
+
<div className={`${classes.join(' ')}`} ref={outerContainerRef} data-lollipop={config.isLollipopChart} data-download-id={imageId}>
|
|
744
800
|
{body}
|
|
745
801
|
</div>
|
|
746
802
|
</Context.Provider>
|