@cdc/chart 4.22.10 → 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/README.md +5 -5
- package/dist/495.js +3 -0
- package/dist/703.js +1 -0
- package/dist/cdcchart.js +723 -6
- package/examples/age-adjusted-rates.json +1486 -1218
- package/examples/box-plot-data.json +71 -0
- package/examples/box-plot.csv +5 -0
- package/examples/{private/yaxis-test.json → box-plot.json} +46 -54
- package/examples/case-rate-example-config.json +1 -1
- package/examples/covid-confidence-example-config.json +33 -33
- package/examples/covid-example-config.json +34 -34
- package/examples/covid-example-data-confidence.json +30 -30
- package/examples/covid-example-data.json +20 -20
- package/examples/cutoff-example-config.json +36 -36
- package/examples/cutoff-example-data.json +36 -36
- package/examples/date-exclusions-config.json +1 -1
- package/examples/dynamic-legends.json +124 -124
- package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart-with-numbers-on-bar.json +191 -197
- package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +230 -240
- package/examples/gallery/bar-chart-horizontal/horizontal-stacked.json +239 -247
- package/examples/gallery/bar-chart-vertical/combo-line-chart.json +138 -136
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +79 -79
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-stacked.json +80 -80
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +67 -67
- package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +179 -110
- package/examples/gallery/lollipop/lollipop-style-horizontal.json +215 -219
- package/examples/gallery/paired-bar/paired-bar-chart.json +195 -195
- package/examples/horizontal-chart.json +35 -35
- package/examples/horizontal-stacked-bar-chart.json +34 -34
- package/examples/line-chart.json +75 -75
- package/examples/new-data.csv +17 -0
- package/examples/newdata.json +90 -0
- package/examples/paired-bar-data.json +16 -14
- package/examples/paired-bar-example.json +48 -48
- package/examples/paired-bar-formatted.json +36 -36
- package/examples/planet-chart-horizontal-example-config.json +33 -33
- package/examples/planet-combo-example-config.json +34 -31
- package/examples/planet-example-config.json +35 -33
- package/examples/planet-example-data.json +56 -56
- package/examples/planet-pie-example-config.json +28 -28
- package/examples/stacked-vertical-bar-example.json +1 -1
- package/examples/temp-example-config.json +61 -54
- package/examples/temp-example-data.json +1 -1
- package/package.json +3 -2
- package/src/CdcChart.tsx +449 -434
- package/src/components/BarChart.tsx +383 -497
- package/src/components/BoxPlot.js +92 -0
- package/src/components/DataTable.tsx +182 -197
- package/src/components/EditorPanel.js +1068 -722
- package/src/components/Filters.js +131 -0
- package/src/components/Legend.js +286 -329
- package/src/components/LineChart.tsx +143 -81
- package/src/components/LinearChart.tsx +432 -451
- package/src/components/PairedBarChart.tsx +197 -213
- package/src/components/PieChart.tsx +105 -151
- package/src/components/SparkLine.js +179 -201
- package/src/components/useIntersectionObserver.tsx +19 -20
- package/src/context.tsx +3 -3
- package/src/data/initial-state.js +44 -17
- package/src/hooks/useActiveElement.js +13 -13
- package/src/hooks/useChartClasses.js +34 -28
- package/src/hooks/useColorPalette.ts +56 -63
- package/src/hooks/useLegendClasses.js +18 -10
- package/src/hooks/useReduceData.ts +64 -77
- package/src/hooks/useRightAxis.js +25 -0
- package/src/hooks/useTopAxis.js +6 -0
- package/src/index.html +19 -19
- package/src/index.tsx +13 -16
- package/src/scss/DataTable.scss +6 -5
- package/src/scss/editor-panel.scss +71 -69
- package/src/scss/main.scss +188 -114
- package/src/scss/variables.scss +1 -1
- package/examples/private/line-test-data.json +0 -22
- package/examples/private/line-test-two.json +0 -216
- package/examples/private/line-test.json +0 -102
- package/examples/private/newtest.csv +0 -101
- package/examples/private/shawn.json +0 -1296
- 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
|
@@ -1,156 +1,147 @@
|
|
|
1
|
-
import React, { useState, useEffect, useCallback } from 'react'
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react'
|
|
2
2
|
|
|
3
3
|
// IE11
|
|
4
|
-
import 'core-js/stable'
|
|
5
|
-
import ResizeObserver from 'resize-observer-polyfill'
|
|
6
|
-
import 'whatwg-fetch'
|
|
4
|
+
import 'core-js/stable'
|
|
5
|
+
import ResizeObserver from 'resize-observer-polyfill'
|
|
6
|
+
import 'whatwg-fetch'
|
|
7
|
+
import * as d3 from 'd3-array'
|
|
7
8
|
|
|
8
9
|
// External Libraries
|
|
9
|
-
import { scaleOrdinal } from '@visx/scale'
|
|
10
|
-
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
11
|
-
import { timeParse, timeFormat } from 'd3-time-format'
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
10
|
+
import { scaleOrdinal } from '@visx/scale'
|
|
11
|
+
import ParentSize from '@visx/responsive/lib/components/ParentSize'
|
|
12
|
+
import { timeParse, timeFormat } from 'd3-time-format'
|
|
13
|
+
import { format } from 'd3-format'
|
|
14
|
+
import Papa from 'papaparse'
|
|
15
|
+
import parse from 'html-react-parser'
|
|
16
|
+
import { Base64 } from 'js-base64'
|
|
15
17
|
|
|
16
18
|
// Primary Components
|
|
17
|
-
import Context from './context'
|
|
18
|
-
import PieChart from './components/PieChart'
|
|
19
|
-
import LinearChart from './components/LinearChart'
|
|
19
|
+
import Context from './context'
|
|
20
|
+
import PieChart from './components/PieChart'
|
|
21
|
+
import LinearChart from './components/LinearChart'
|
|
20
22
|
|
|
21
|
-
import {colorPalettesChart as colorPalettes} from '../../core/data/colorPalettes'
|
|
23
|
+
import { colorPalettesChart as colorPalettes } from '../../core/data/colorPalettes'
|
|
22
24
|
|
|
23
|
-
import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
|
|
25
|
+
import { publish, subscribe, unsubscribe } from '@cdc/core/helpers/events'
|
|
24
26
|
|
|
25
|
-
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
27
|
+
import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
|
|
26
28
|
|
|
27
|
-
import SparkLine from './components/SparkLine'
|
|
28
|
-
import Legend from './components/Legend'
|
|
29
|
-
import DataTable from './components/DataTable'
|
|
30
|
-
import defaults from './data/initial-state'
|
|
31
|
-
import EditorPanel from './components/EditorPanel'
|
|
32
|
-
import Loading from '@cdc/core/components/Loading'
|
|
29
|
+
import SparkLine from './components/SparkLine'
|
|
30
|
+
import Legend from './components/Legend'
|
|
31
|
+
import DataTable from './components/DataTable'
|
|
32
|
+
import defaults from './data/initial-state'
|
|
33
|
+
import EditorPanel from './components/EditorPanel'
|
|
34
|
+
import Loading from '@cdc/core/components/Loading'
|
|
35
|
+
import Filters from './components/Filters'
|
|
36
|
+
import CoveMediaControls from '@cdc/core/helpers/CoveMediaControls'
|
|
33
37
|
|
|
34
38
|
// helpers
|
|
35
39
|
import numberFromString from '@cdc/core/helpers/numberFromString'
|
|
36
|
-
import getViewport from '@cdc/core/helpers/getViewport'
|
|
37
|
-
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
38
|
-
import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
|
|
39
|
-
|
|
40
|
-
import './scss/main.scss'
|
|
41
|
-
|
|
42
|
-
export default function CdcChart(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const [
|
|
52
|
-
const [
|
|
53
|
-
const [
|
|
54
|
-
const [
|
|
55
|
-
const [
|
|
56
|
-
const [
|
|
57
|
-
const [
|
|
58
|
-
const [currentViewport, setCurrentViewport] = useState<String>('lg');
|
|
59
|
-
const [dimensions, setDimensions] = useState<Array<Number>>([]);
|
|
60
|
-
const [externalFilters, setExternalFilters] = useState(null);
|
|
40
|
+
import getViewport from '@cdc/core/helpers/getViewport'
|
|
41
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
42
|
+
import cacheBustingString from '@cdc/core/helpers/cacheBustingString'
|
|
43
|
+
|
|
44
|
+
import './scss/main.scss'
|
|
45
|
+
|
|
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 }) {
|
|
47
|
+
const transform = new DataTransform()
|
|
48
|
+
interface keyable {
|
|
49
|
+
[key: string]: any
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [loading, setLoading] = useState<Boolean>(true)
|
|
53
|
+
const [colorScale, setColorScale] = useState<any>(null)
|
|
54
|
+
const [config, setConfig] = useState<keyable>({})
|
|
55
|
+
const [stateData, setStateData] = useState<Array<Object>>(config.data || [])
|
|
56
|
+
const [excludedData, setExcludedData] = useState<Array<Object>>()
|
|
57
|
+
const [filteredData, setFilteredData] = useState<Array<Object>>()
|
|
58
|
+
const [seriesHighlight, setSeriesHighlight] = useState<Array<String>>([])
|
|
59
|
+
const [currentViewport, setCurrentViewport] = useState<String>('lg')
|
|
60
|
+
const [dimensions, setDimensions] = useState<Array<Number>>([])
|
|
61
|
+
const [externalFilters, setExternalFilters] = useState(null)
|
|
61
62
|
const [container, setContainer] = useState()
|
|
62
63
|
const [coveLoadedEventRan, setCoveLoadedEventRan] = useState(false)
|
|
63
64
|
const [dynamicLegendItems, setDynamicLegendItems] = useState([])
|
|
65
|
+
const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`)
|
|
64
66
|
|
|
65
|
-
const legendGlyphSize = 15
|
|
66
|
-
const legendGlyphSizeHalf = legendGlyphSize / 2
|
|
67
|
+
const legendGlyphSize = 15
|
|
68
|
+
const legendGlyphSizeHalf = legendGlyphSize / 2
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
contentClasses,
|
|
72
|
-
innerContainerClasses,
|
|
73
|
-
sparkLineStyles
|
|
74
|
-
} = useDataVizClasses(config)
|
|
70
|
+
// Destructure items from config for more readable JSX
|
|
71
|
+
const { legend, title, description, visualizationType } = config
|
|
72
|
+
const { barBorderClass, lineDatapointClass, contentClasses, innerContainerClasses, sparkLineStyles } = useDataVizClasses(config)
|
|
75
73
|
|
|
76
74
|
const handleChartTabbing = config.showSidebar ? `#legend` : config?.title ? `#dataTableSection__${config.title.replace(/\s/g, '')}` : `#dataTableSection`
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
76
|
const handleChartAriaLabels = (state, testing = false) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if(state.visualizationType) {
|
|
88
|
-
ariaLabel += `${state.visualizationType} chart`
|
|
89
|
-
}
|
|
77
|
+
if (testing) console.log(`handleChartAriaLabels Testing On:`, state)
|
|
78
|
+
try {
|
|
79
|
+
if (!state.visualizationType) throw Error('handleChartAriaLabels: no visualization type found in state')
|
|
80
|
+
let ariaLabel = ''
|
|
90
81
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
82
|
+
if (state.visualizationType) {
|
|
83
|
+
ariaLabel += `${state.visualizationType} chart`
|
|
84
|
+
}
|
|
94
85
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.error(e.message)
|
|
86
|
+
if (state.title && state.visualizationType) {
|
|
87
|
+
ariaLabel += ` with the title: ${state.title}`
|
|
98
88
|
}
|
|
89
|
+
|
|
90
|
+
return ariaLabel
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(e.message)
|
|
93
|
+
}
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
const loadConfig = async () => {
|
|
102
|
-
let response = configObj || await (await fetch(configUrl)).json()
|
|
97
|
+
let response = configObj || (await (await fetch(configUrl)).json())
|
|
103
98
|
|
|
104
99
|
// If data is included through a URL, fetch that and store
|
|
105
|
-
let data = response.formattedData || response.data || {}
|
|
100
|
+
let data = response.formattedData || response.data || {}
|
|
106
101
|
|
|
107
102
|
if (response.dataUrl) {
|
|
108
|
-
|
|
109
103
|
try {
|
|
110
104
|
const regex = /(?:\.([^.]+))?$/
|
|
111
105
|
|
|
112
|
-
const ext =
|
|
106
|
+
const ext = regex.exec(response.dataUrl)[1]
|
|
113
107
|
if ('csv' === ext) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
108
|
+
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`)
|
|
109
|
+
.then(response => response.text())
|
|
110
|
+
.then(responseText => {
|
|
111
|
+
const parsedCsv = Papa.parse(responseText, {
|
|
112
|
+
header: true,
|
|
113
|
+
dynamicTyping: true,
|
|
114
|
+
skipEmptyLines: true
|
|
115
|
+
})
|
|
116
|
+
return parsedCsv.data
|
|
117
|
+
})
|
|
124
118
|
}
|
|
125
119
|
|
|
126
120
|
if ('json' === ext) {
|
|
127
|
-
|
|
128
|
-
.then(response => response.json())
|
|
121
|
+
data = await fetch(response.dataUrl + `?v=${cacheBustingString()}`).then(response => response.json())
|
|
129
122
|
}
|
|
130
123
|
} catch {
|
|
131
|
-
console.error(`Cannot parse URL: ${response.dataUrl}`)
|
|
132
|
-
data = []
|
|
124
|
+
console.error(`Cannot parse URL: ${response.dataUrl}`)
|
|
125
|
+
data = []
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
if(response.dataDescription) {
|
|
136
|
-
data = transform.autoStandardize(data)
|
|
137
|
-
data = transform.developerStandardize(data, response.dataDescription)
|
|
128
|
+
if (response.dataDescription) {
|
|
129
|
+
data = transform.autoStandardize(data)
|
|
130
|
+
data = transform.developerStandardize(data, response.dataDescription)
|
|
138
131
|
}
|
|
139
132
|
}
|
|
140
133
|
|
|
141
|
-
if(data) {
|
|
134
|
+
if (data) {
|
|
142
135
|
setStateData(data)
|
|
143
136
|
setExcludedData(data)
|
|
144
137
|
}
|
|
145
138
|
|
|
146
|
-
let newConfig = {...defaults, ...response}
|
|
147
|
-
if(undefined === newConfig.table.show) newConfig.table.show = !isDashboard
|
|
148
|
-
updateConfig(newConfig, data)
|
|
139
|
+
let newConfig = { ...defaults, ...response }
|
|
140
|
+
if (undefined === newConfig.table.show) newConfig.table.show = !isDashboard
|
|
141
|
+
updateConfig(newConfig, data)
|
|
149
142
|
}
|
|
150
143
|
|
|
151
|
-
|
|
152
144
|
const updateConfig = (newConfig, dataOverride = undefined) => {
|
|
153
|
-
|
|
154
145
|
let data = dataOverride || stateData
|
|
155
146
|
|
|
156
147
|
// Deeper copy
|
|
@@ -158,23 +149,17 @@ export default function CdcChart(
|
|
|
158
149
|
if (newConfig[key] && 'object' === typeof newConfig[key] && !Array.isArray(newConfig[key])) {
|
|
159
150
|
newConfig[key] = { ...defaults[key], ...newConfig[key] }
|
|
160
151
|
}
|
|
161
|
-
})
|
|
152
|
+
})
|
|
162
153
|
|
|
163
154
|
// Loop through and set initial data with exclusions - this should persist through any following data transformations (ie. filters)
|
|
164
155
|
let newExcludedData
|
|
165
156
|
|
|
166
157
|
if (newConfig.exclusions && newConfig.exclusions.active) {
|
|
167
|
-
|
|
168
158
|
if (newConfig.xAxis.type === 'categorical' && newConfig.exclusions.keys?.length > 0) {
|
|
169
159
|
newExcludedData = data.filter(e => !newConfig.exclusions.keys.includes(e[newConfig.xAxis.dataKey]))
|
|
170
|
-
} else if (
|
|
171
|
-
newConfig.xAxis.type === 'date' &&
|
|
172
|
-
(newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) &&
|
|
173
|
-
newConfig.xAxis.dateParseFormat
|
|
174
|
-
) {
|
|
175
|
-
|
|
160
|
+
} else if (newConfig.xAxis.type === 'date' && (newConfig.exclusions.dateStart || newConfig.exclusions.dateEnd) && newConfig.xAxis.dateParseFormat) {
|
|
176
161
|
// Filter dates
|
|
177
|
-
const timestamp =
|
|
162
|
+
const timestamp = e => new Date(e).getTime()
|
|
178
163
|
|
|
179
164
|
let startDate = timestamp(newConfig.exclusions.dateStart)
|
|
180
165
|
let endDate = timestamp(newConfig.exclusions.dateEnd) + 86399999 //Increase by 24h in ms (86400000ms - 1ms) to include selected end date for .getTime() comparative
|
|
@@ -183,16 +168,12 @@ export default function CdcChart(
|
|
|
183
168
|
let endDateValid = undefined !== typeof endDate && false === isNaN(endDate)
|
|
184
169
|
|
|
185
170
|
if (startDateValid && endDateValid) {
|
|
186
|
-
newExcludedData = data.filter(e =>
|
|
187
|
-
(timestamp(e[newConfig.xAxis.dataKey]) >= startDate) &&
|
|
188
|
-
(timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
|
|
189
|
-
)
|
|
171
|
+
newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate && timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
|
|
190
172
|
} else if (startDateValid) {
|
|
191
173
|
newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) >= startDate)
|
|
192
174
|
} else if (endDateValid) {
|
|
193
175
|
newExcludedData = data.filter(e => timestamp(e[newConfig.xAxis.dataKey]) <= endDate)
|
|
194
176
|
}
|
|
195
|
-
|
|
196
177
|
} else {
|
|
197
178
|
newExcludedData = dataOverride || stateData
|
|
198
179
|
}
|
|
@@ -203,134 +184,192 @@ export default function CdcChart(
|
|
|
203
184
|
setExcludedData(newExcludedData)
|
|
204
185
|
|
|
205
186
|
// After data is grabbed, loop through and generate filter column values if there are any
|
|
206
|
-
let currentData
|
|
187
|
+
let currentData
|
|
207
188
|
if (newConfig.filters) {
|
|
208
|
-
|
|
209
189
|
newConfig.filters.forEach((filter, index) => {
|
|
190
|
+
let filterValues = []
|
|
210
191
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
filterValues = generateValuesForFilter(filter.columnName, newExcludedData);
|
|
214
|
-
|
|
215
|
-
newConfig.filters[index].values = filterValues;
|
|
216
|
-
// Initial filter should be active
|
|
217
|
-
newConfig.filters[index].active = filterValues[0];
|
|
192
|
+
filterValues = filter.orderedValues || generateValuesForFilter(filter.columnName, newExcludedData)
|
|
218
193
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
194
|
+
newConfig.filters[index].values = filterValues
|
|
195
|
+
// Initial filter should be active
|
|
196
|
+
newConfig.filters[index].active = filterValues[0]
|
|
197
|
+
})
|
|
198
|
+
currentData = filterData(newConfig.filters, newExcludedData)
|
|
199
|
+
setFilteredData(currentData)
|
|
222
200
|
}
|
|
223
201
|
|
|
224
202
|
//Enforce default values that need to be calculated at runtime
|
|
225
|
-
newConfig.runtime = {}
|
|
226
|
-
newConfig.runtime.seriesLabels = {}
|
|
227
|
-
newConfig.runtime.seriesLabelsAll = []
|
|
228
|
-
newConfig.runtime.originalXAxis = newConfig.xAxis
|
|
229
|
-
|
|
203
|
+
newConfig.runtime = {}
|
|
204
|
+
newConfig.runtime.seriesLabels = {}
|
|
205
|
+
newConfig.runtime.seriesLabelsAll = []
|
|
206
|
+
newConfig.runtime.originalXAxis = newConfig.xAxis
|
|
230
207
|
|
|
231
208
|
if (newConfig.visualizationType === 'Pie') {
|
|
232
|
-
newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
|
|
233
|
-
newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
|
|
209
|
+
newConfig.runtime.seriesKeys = (dataOverride || data).map(d => d[newConfig.xAxis.dataKey])
|
|
210
|
+
newConfig.runtime.seriesLabelsAll = newConfig.runtime.seriesKeys
|
|
234
211
|
} else {
|
|
235
|
-
newConfig.runtime.seriesKeys = newConfig.series
|
|
236
|
-
newConfig.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
212
|
+
newConfig.runtime.seriesKeys = newConfig.series
|
|
213
|
+
? newConfig.series.map(series => {
|
|
214
|
+
newConfig.runtime.seriesLabels[series.dataKey] = series.label || series.dataKey
|
|
215
|
+
newConfig.runtime.seriesLabelsAll.push(series.label || series.dataKey)
|
|
216
|
+
return series.dataKey
|
|
217
|
+
})
|
|
218
|
+
: []
|
|
219
|
+
}
|
|
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)
|
|
240
279
|
}
|
|
241
280
|
|
|
242
281
|
if (newConfig.visualizationType === 'Combo' && newConfig.series) {
|
|
243
|
-
newConfig.runtime.barSeriesKeys = []
|
|
244
|
-
newConfig.runtime.lineSeriesKeys = []
|
|
245
|
-
newConfig.series.forEach(
|
|
246
|
-
if(series.type === 'Bar'){
|
|
247
|
-
newConfig.runtime.barSeriesKeys.push(series.dataKey)
|
|
282
|
+
newConfig.runtime.barSeriesKeys = []
|
|
283
|
+
newConfig.runtime.lineSeriesKeys = []
|
|
284
|
+
newConfig.series.forEach(series => {
|
|
285
|
+
if (series.type === 'Bar') {
|
|
286
|
+
newConfig.runtime.barSeriesKeys.push(series.dataKey)
|
|
248
287
|
}
|
|
249
|
-
if(series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg'){
|
|
250
|
-
newConfig.runtime.lineSeriesKeys.push(series.dataKey)
|
|
288
|
+
if (series.type === 'Line' || series.type === 'dashed-sm' || series.type === 'dashed-md' || series.type === 'dashed-lg') {
|
|
289
|
+
newConfig.runtime.lineSeriesKeys.push(series.dataKey)
|
|
251
290
|
}
|
|
252
|
-
})
|
|
291
|
+
})
|
|
253
292
|
}
|
|
254
293
|
|
|
255
|
-
if (
|
|
256
|
-
newConfig.runtime.xAxis = newConfig.yAxis
|
|
257
|
-
newConfig.runtime.yAxis = newConfig.xAxis
|
|
258
|
-
newConfig.runtime.horizontal = true
|
|
294
|
+
if ((newConfig.visualizationType === 'Bar' && newConfig.orientation === 'horizontal') || newConfig.visualizationType === 'Paired Bar') {
|
|
295
|
+
newConfig.runtime.xAxis = newConfig.yAxis
|
|
296
|
+
newConfig.runtime.yAxis = newConfig.xAxis
|
|
297
|
+
newConfig.runtime.horizontal = true
|
|
259
298
|
} else {
|
|
260
|
-
newConfig.runtime.xAxis = newConfig.xAxis
|
|
261
|
-
newConfig.runtime.yAxis = newConfig.yAxis
|
|
262
|
-
newConfig.runtime.horizontal = false
|
|
299
|
+
newConfig.runtime.xAxis = newConfig.xAxis
|
|
300
|
+
newConfig.runtime.yAxis = newConfig.yAxis
|
|
301
|
+
newConfig.runtime.horizontal = false
|
|
263
302
|
}
|
|
264
|
-
newConfig.runtime.uniqueId = Date.now()
|
|
265
|
-
newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey ? 'Data Key property in Y Axis section must be set for pie charts.' : ''
|
|
303
|
+
newConfig.runtime.uniqueId = Date.now()
|
|
304
|
+
newConfig.runtime.editorErrorMessage = newConfig.visualizationType === 'Pie' && !newConfig.yAxis.dataKey ? 'Data Key property in Y Axis section must be set for pie charts.' : ''
|
|
266
305
|
|
|
267
|
-
setConfig(newConfig)
|
|
268
|
-
}
|
|
306
|
+
setConfig(newConfig)
|
|
307
|
+
}
|
|
269
308
|
|
|
270
309
|
const filterData = (filters, data) => {
|
|
271
|
-
let filteredData = []
|
|
310
|
+
let filteredData = []
|
|
272
311
|
|
|
273
|
-
data.forEach(
|
|
274
|
-
let add = true
|
|
275
|
-
filters.forEach(
|
|
312
|
+
data.forEach(row => {
|
|
313
|
+
let add = true
|
|
314
|
+
filters.forEach(filter => {
|
|
276
315
|
if (row[filter.columnName] !== filter.active) {
|
|
277
|
-
add = false
|
|
316
|
+
add = false
|
|
278
317
|
}
|
|
279
|
-
})
|
|
318
|
+
})
|
|
280
319
|
|
|
281
|
-
if(add) filteredData.push(row)
|
|
282
|
-
})
|
|
283
|
-
return filteredData
|
|
320
|
+
if (add) filteredData.push(row)
|
|
321
|
+
})
|
|
322
|
+
return filteredData
|
|
284
323
|
}
|
|
285
324
|
|
|
286
325
|
// Gets filer values from dataset
|
|
287
326
|
const generateValuesForFilter = (columnName, data = this.state.data) => {
|
|
288
|
-
const values = []
|
|
327
|
+
const values = []
|
|
289
328
|
|
|
290
|
-
data.forEach(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
})
|
|
329
|
+
data.forEach(row => {
|
|
330
|
+
const value = row[columnName]
|
|
331
|
+
if (value && false === values.includes(value)) {
|
|
332
|
+
values.push(value)
|
|
333
|
+
}
|
|
334
|
+
})
|
|
296
335
|
|
|
297
|
-
return values
|
|
336
|
+
return values
|
|
298
337
|
}
|
|
299
338
|
|
|
300
339
|
// Sorts data series for horizontal bar charts
|
|
301
340
|
const sortData = (a, b) => {
|
|
302
|
-
let sortKey = config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal' ? config.xAxis.dataKey : config.yAxis.sortKey
|
|
303
|
-
let aData = parseFloat(a[sortKey])
|
|
304
|
-
let bData = parseFloat(b[sortKey])
|
|
305
|
-
|
|
306
|
-
if(aData < bData){
|
|
307
|
-
return config.sortData === 'ascending' ? 1 : -1
|
|
308
|
-
} else if (aData > bData){
|
|
309
|
-
return config.sortData === 'ascending' ? -1 : 1
|
|
341
|
+
let sortKey = config.visualizationType === 'Bar' && config.visualizationSubType === 'horizontal' ? config.xAxis.dataKey : config.yAxis.sortKey
|
|
342
|
+
let aData = parseFloat(a[sortKey])
|
|
343
|
+
let bData = parseFloat(b[sortKey])
|
|
344
|
+
|
|
345
|
+
if (aData < bData) {
|
|
346
|
+
return config.sortData === 'ascending' ? 1 : -1
|
|
347
|
+
} else if (aData > bData) {
|
|
348
|
+
return config.sortData === 'ascending' ? -1 : 1
|
|
310
349
|
} else {
|
|
311
|
-
return 0
|
|
350
|
+
return 0
|
|
312
351
|
}
|
|
313
352
|
}
|
|
314
353
|
|
|
315
354
|
// Observes changes to outermost container and changes viewport size in state
|
|
316
|
-
const resizeObserver:ResizeObserver = new ResizeObserver(entries => {
|
|
355
|
+
const resizeObserver: ResizeObserver = new ResizeObserver(entries => {
|
|
317
356
|
for (let entry of entries) {
|
|
318
357
|
let { width, height } = entry.contentRect
|
|
319
358
|
let newViewport = getViewport(width)
|
|
320
|
-
let svgMarginWidth = 32
|
|
321
|
-
let editorWidth = 350
|
|
359
|
+
let svgMarginWidth = 32
|
|
360
|
+
let editorWidth = 350
|
|
322
361
|
|
|
323
362
|
setCurrentViewport(newViewport)
|
|
324
363
|
|
|
325
|
-
if(isEditor) {
|
|
326
|
-
width = width - editorWidth
|
|
364
|
+
if (isEditor) {
|
|
365
|
+
width = width - editorWidth
|
|
327
366
|
}
|
|
328
367
|
|
|
329
|
-
if(entry.target.dataset.lollipop === 'true') {
|
|
330
|
-
width = width - 2.5
|
|
368
|
+
if (entry.target.dataset.lollipop === 'true') {
|
|
369
|
+
width = width - 2.5
|
|
331
370
|
}
|
|
332
371
|
|
|
333
|
-
width = width - svgMarginWidth
|
|
372
|
+
width = width - svgMarginWidth
|
|
334
373
|
|
|
335
374
|
setDimensions([width, height])
|
|
336
375
|
}
|
|
@@ -338,374 +377,348 @@ export default function CdcChart(
|
|
|
338
377
|
|
|
339
378
|
const outerContainerRef = useCallback(node => {
|
|
340
379
|
if (node !== null) {
|
|
341
|
-
|
|
380
|
+
resizeObserver.observe(node)
|
|
342
381
|
}
|
|
343
382
|
|
|
344
383
|
setContainer(node)
|
|
345
|
-
},[])
|
|
384
|
+
}, [])
|
|
346
385
|
|
|
347
386
|
function isEmpty(obj) {
|
|
348
|
-
return Object.keys(obj).length === 0
|
|
387
|
+
return Object.keys(obj).length === 0
|
|
349
388
|
}
|
|
350
389
|
|
|
351
390
|
// Load data when component first mounts
|
|
352
391
|
useEffect(() => {
|
|
353
|
-
loadConfig()
|
|
354
|
-
}, [])
|
|
392
|
+
loadConfig()
|
|
393
|
+
}, [])
|
|
355
394
|
|
|
356
395
|
/**
|
|
357
396
|
* When cove has a config and container ref publish the cove_loaded event.
|
|
358
397
|
*/
|
|
359
398
|
useEffect(() => {
|
|
360
|
-
if(container && !isEmpty(config) && !coveLoadedEventRan) {
|
|
399
|
+
if (container && !isEmpty(config) && !coveLoadedEventRan) {
|
|
361
400
|
publish('cove_loaded', { config: config })
|
|
362
401
|
setCoveLoadedEventRan(true)
|
|
363
402
|
}
|
|
403
|
+
}, [container, config])
|
|
364
404
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
* Updates externalFilters state
|
|
371
|
-
* Another useEffect listens to externalFilterChanges and updates the config.
|
|
372
|
-
*/
|
|
405
|
+
/**
|
|
406
|
+
* Handles filter change events outside of COVE
|
|
407
|
+
* Updates externalFilters state
|
|
408
|
+
* Another useEffect listens to externalFilterChanges and updates the config.
|
|
409
|
+
*/
|
|
373
410
|
useEffect(() => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
setExternalFilters(tmp)
|
|
411
|
+
const handleFilterData = (e: CustomEvent) => {
|
|
412
|
+
let tmp = []
|
|
413
|
+
tmp.push(e.detail)
|
|
414
|
+
setExternalFilters(tmp)
|
|
379
415
|
}
|
|
380
|
-
|
|
381
|
-
subscribe('cove_filterData', (e:CustomEvent) => handleFilterData(e))
|
|
416
|
+
|
|
417
|
+
subscribe('cove_filterData', (e: CustomEvent) => handleFilterData(e))
|
|
382
418
|
|
|
383
419
|
return () => {
|
|
384
|
-
unsubscribe('cove_filterData', handleFilterData)
|
|
420
|
+
unsubscribe('cove_filterData', handleFilterData)
|
|
385
421
|
}
|
|
422
|
+
}, [config])
|
|
386
423
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
* For some reason e.detail is returning [order: "asc"] even though
|
|
393
|
-
* we're not passing that in. The code here checks for an active prop instead of an empty array.
|
|
394
|
-
*/
|
|
424
|
+
/**
|
|
425
|
+
* Handles changes to externalFilters
|
|
426
|
+
* For some reason e.detail is returning [order: "asc"] even though
|
|
427
|
+
* we're not passing that in. The code here checks for an active prop instead of an empty array.
|
|
428
|
+
*/
|
|
395
429
|
useEffect(() => {
|
|
396
|
-
|
|
397
|
-
if(externalFilters && externalFilters[0]) {
|
|
430
|
+
if (externalFilters && externalFilters[0]) {
|
|
398
431
|
const hasActiveProperty = externalFilters[0].hasOwnProperty('active')
|
|
399
432
|
|
|
400
|
-
if(!hasActiveProperty) {
|
|
401
|
-
let configCopy = {...config }
|
|
433
|
+
if (!hasActiveProperty) {
|
|
434
|
+
let configCopy = { ...config }
|
|
402
435
|
delete configCopy['filters']
|
|
403
436
|
setConfig(configCopy)
|
|
404
|
-
setFilteredData(filterData(externalFilters, excludedData))
|
|
437
|
+
setFilteredData(filterData(externalFilters, excludedData))
|
|
405
438
|
}
|
|
406
439
|
}
|
|
407
440
|
|
|
408
|
-
if(externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
|
|
409
|
-
let newConfigHere = {...config, filters: externalFilters }
|
|
441
|
+
if (externalFilters && externalFilters.length > 0 && externalFilters.length > 0 && externalFilters[0].hasOwnProperty('active')) {
|
|
442
|
+
let newConfigHere = { ...config, filters: externalFilters }
|
|
410
443
|
setConfig(newConfigHere)
|
|
411
|
-
setFilteredData(filterData(externalFilters, excludedData))
|
|
444
|
+
setFilteredData(filterData(externalFilters, excludedData))
|
|
412
445
|
}
|
|
413
|
-
|
|
414
|
-
}, [externalFilters]);
|
|
446
|
+
}, [externalFilters])
|
|
415
447
|
|
|
416
|
-
|
|
417
448
|
// Load data when configObj data changes
|
|
418
|
-
if(configObj){
|
|
449
|
+
if (configObj) {
|
|
419
450
|
useEffect(() => {
|
|
420
|
-
loadConfig()
|
|
421
|
-
}, [configObj.data])
|
|
451
|
+
loadConfig()
|
|
452
|
+
}, [configObj.data])
|
|
422
453
|
}
|
|
423
454
|
|
|
424
455
|
// Generates color palette to pass to child chart component
|
|
425
456
|
useEffect(() => {
|
|
426
|
-
if(stateData && config.xAxis && config.runtime.seriesKeys) {
|
|
457
|
+
if (stateData && config.xAxis && config.runtime.seriesKeys) {
|
|
427
458
|
let palette = config.customColors || colorPalettes[config.palette]
|
|
428
459
|
let numberOfKeys = config.runtime.seriesKeys.length
|
|
429
|
-
let newColorScale
|
|
460
|
+
let newColorScale
|
|
430
461
|
|
|
431
|
-
while(numberOfKeys > palette.length) {
|
|
432
|
-
palette = palette.concat(palette)
|
|
462
|
+
while (numberOfKeys > palette.length) {
|
|
463
|
+
palette = palette.concat(palette)
|
|
433
464
|
}
|
|
434
465
|
|
|
435
|
-
palette = palette.slice(0, numberOfKeys)
|
|
466
|
+
palette = palette.slice(0, numberOfKeys)
|
|
436
467
|
|
|
437
|
-
newColorScale = () =>
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
468
|
+
newColorScale = () =>
|
|
469
|
+
scaleOrdinal({
|
|
470
|
+
domain: config.runtime.seriesLabelsAll,
|
|
471
|
+
range: palette
|
|
472
|
+
})
|
|
441
473
|
|
|
442
|
-
setColorScale(newColorScale)
|
|
443
|
-
setLoading(false)
|
|
474
|
+
setColorScale(newColorScale)
|
|
475
|
+
setLoading(false)
|
|
444
476
|
}
|
|
445
477
|
|
|
446
|
-
if(config && stateData && config.sortData){
|
|
447
|
-
stateData.sort(sortData)
|
|
478
|
+
if (config && stateData && config.sortData) {
|
|
479
|
+
stateData.sort(sortData)
|
|
448
480
|
}
|
|
449
481
|
}, [config, stateData])
|
|
450
482
|
|
|
451
483
|
// Called on legend click, highlights/unhighlights the data series with the given label
|
|
452
|
-
const highlight =
|
|
453
|
-
const newSeriesHighlight = []
|
|
484
|
+
const highlight = label => {
|
|
485
|
+
const newSeriesHighlight = []
|
|
454
486
|
|
|
455
487
|
// If we're highlighting all the series, reset them
|
|
456
|
-
if
|
|
488
|
+
if (seriesHighlight.length + 1 === config.runtime.seriesKeys.length && !config.legend.dynamicLegend) {
|
|
457
489
|
highlightReset()
|
|
458
490
|
return
|
|
459
491
|
}
|
|
460
492
|
|
|
461
|
-
seriesHighlight.forEach(
|
|
462
|
-
newSeriesHighlight.push(value)
|
|
463
|
-
})
|
|
493
|
+
seriesHighlight.forEach(value => {
|
|
494
|
+
newSeriesHighlight.push(value)
|
|
495
|
+
})
|
|
464
496
|
|
|
465
|
-
let newHighlight = label.datum
|
|
466
|
-
if(config.runtime.seriesLabels){
|
|
467
|
-
for(let i = 0; i < config.runtime.seriesKeys.length; i++) {
|
|
468
|
-
if(config.runtime.seriesLabels[config.runtime.seriesKeys[i]] === label.datum){
|
|
469
|
-
newHighlight = config.runtime.seriesKeys[i]
|
|
470
|
-
break
|
|
497
|
+
let newHighlight = label.datum
|
|
498
|
+
if (config.runtime.seriesLabels) {
|
|
499
|
+
for (let i = 0; i < config.runtime.seriesKeys.length; i++) {
|
|
500
|
+
if (config.runtime.seriesLabels[config.runtime.seriesKeys[i]] === label.datum) {
|
|
501
|
+
newHighlight = config.runtime.seriesKeys[i]
|
|
502
|
+
break
|
|
471
503
|
}
|
|
472
504
|
}
|
|
473
505
|
}
|
|
474
506
|
|
|
475
507
|
if (newSeriesHighlight.indexOf(newHighlight) !== -1) {
|
|
476
|
-
newSeriesHighlight.splice(newSeriesHighlight.indexOf(newHighlight), 1)
|
|
508
|
+
newSeriesHighlight.splice(newSeriesHighlight.indexOf(newHighlight), 1)
|
|
477
509
|
} else {
|
|
478
|
-
newSeriesHighlight.push(newHighlight)
|
|
510
|
+
newSeriesHighlight.push(newHighlight)
|
|
479
511
|
}
|
|
480
|
-
setSeriesHighlight(newSeriesHighlight)
|
|
481
|
-
}
|
|
512
|
+
setSeriesHighlight(newSeriesHighlight)
|
|
513
|
+
}
|
|
482
514
|
|
|
483
515
|
// Called on reset button click, unhighlights all data series
|
|
484
516
|
const highlightReset = () => {
|
|
485
|
-
if(config.legend.dynamicLegend && dynamicLegendItems) {
|
|
486
|
-
setSeriesHighlight(dynamicLegendItems.map(
|
|
517
|
+
if (config.legend.dynamicLegend && dynamicLegendItems) {
|
|
518
|
+
setSeriesHighlight(dynamicLegendItems.map(item => item.text))
|
|
487
519
|
} else {
|
|
488
|
-
setSeriesHighlight([])
|
|
520
|
+
setSeriesHighlight([])
|
|
489
521
|
}
|
|
490
522
|
}
|
|
491
523
|
|
|
492
|
-
const section = config.orientation ==='horizontal' ? 'yAxis' :'xAxis'
|
|
524
|
+
const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
|
|
493
525
|
|
|
494
526
|
const parseDate = (dateString: string) => {
|
|
495
|
-
let date = timeParse(config.runtime[section].dateParseFormat)(dateString)
|
|
496
|
-
if(!date) {
|
|
497
|
-
config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section
|
|
498
|
-
return new Date()
|
|
527
|
+
let date = timeParse(config.runtime[section].dateParseFormat)(dateString)
|
|
528
|
+
if (!date) {
|
|
529
|
+
config.runtime.editorErrorMessage = `Error parsing date "${dateString}". Try reviewing your data and date parse settings in the X Axis section.`
|
|
530
|
+
return new Date()
|
|
499
531
|
} else {
|
|
500
|
-
return date
|
|
532
|
+
return date
|
|
501
533
|
}
|
|
502
|
-
}
|
|
503
|
-
|
|
534
|
+
}
|
|
504
535
|
|
|
505
536
|
const formatDate = (date: Date) => {
|
|
506
|
-
return timeFormat(config.runtime[section].dateDisplayFormat)(date)
|
|
507
|
-
}
|
|
537
|
+
return timeFormat(config.runtime[section].dateDisplayFormat)(date)
|
|
538
|
+
}
|
|
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
|
+
}
|
|
508
568
|
|
|
509
569
|
// Format numeric data based on settings in config
|
|
510
|
-
const formatNumber = (num) => {
|
|
511
|
-
// check if value contains comma and remove it. later will add comma below.
|
|
512
|
-
if(String(num).indexOf(',') !== -1) num = num.replaceAll(',', '');
|
|
570
|
+
const formatNumber = (num, axis) => {
|
|
513
571
|
// if num is NaN return num
|
|
514
|
-
if(isNaN(num)|| !num) return num
|
|
572
|
+
if (isNaN(num) || !num) return num
|
|
515
573
|
|
|
516
|
-
|
|
517
|
-
let
|
|
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(',', '')
|
|
518
582
|
|
|
519
|
-
let
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
583
|
+
let original = num
|
|
584
|
+
let stringFormattingOptions
|
|
585
|
+
|
|
586
|
+
if (axis !== 'right') {
|
|
587
|
+
stringFormattingOptions = {
|
|
588
|
+
useGrouping: config.dataFormat.commas ? true : false,
|
|
589
|
+
minimumFractionDigits: roundTo ? Number(roundTo) : 0,
|
|
590
|
+
maximumFractionDigits: roundTo ? Number(roundTo) : 0
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
stringFormattingOptions = {
|
|
594
|
+
useGrouping: config.dataFormat.rightCommas ? true : false,
|
|
595
|
+
minimumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0,
|
|
596
|
+
maximumFractionDigits: rightRoundTo ? Number(rightRoundTo) : 0
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
num = numberFromString(num)
|
|
524
601
|
|
|
525
|
-
num = numberFromString(num);
|
|
526
|
-
|
|
527
602
|
if (isNaN(num)) {
|
|
528
|
-
config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section
|
|
603
|
+
config.runtime.editorErrorMessage = `Unable to parse number from data ${original}. Try reviewing your data and selections in the Data Series section.`
|
|
529
604
|
return original
|
|
530
605
|
}
|
|
531
606
|
|
|
532
|
-
if (!config.dataFormat) return num
|
|
533
|
-
if (config.dataCutoff){
|
|
607
|
+
if (!config.dataFormat) return num
|
|
608
|
+
if (config.dataCutoff) {
|
|
534
609
|
let cutoff = numberFromString(config.dataCutoff)
|
|
535
610
|
|
|
536
|
-
if(num < cutoff) {
|
|
537
|
-
num = cutoff
|
|
611
|
+
if (num < cutoff) {
|
|
612
|
+
num = cutoff
|
|
538
613
|
}
|
|
539
614
|
}
|
|
540
|
-
num = num.toLocaleString('en-US', stringFormattingOptions)
|
|
541
615
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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)
|
|
546
624
|
}
|
|
625
|
+
let result = ''
|
|
547
626
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if(config.dataFormat.suffix) {
|
|
551
|
-
result += config.dataFormat.suffix
|
|
627
|
+
if (abbreviated && axis === 'left') {
|
|
628
|
+
num = formatSuffix(parseFloat(num)).replace('G', 'B')
|
|
552
629
|
}
|
|
553
|
-
return String(result)
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
// Destructure items from config for more readable JSX
|
|
557
|
-
const { legend, title, description, visualizationType } = config;
|
|
558
|
-
|
|
559
|
-
// Select appropriate chart type
|
|
560
|
-
const chartComponents = {
|
|
561
|
-
'Paired Bar' : <LinearChart />,
|
|
562
|
-
'Bar' : <LinearChart />,
|
|
563
|
-
'Line' : <LinearChart />,
|
|
564
|
-
'Combo': <LinearChart />,
|
|
565
|
-
'Pie' : <PieChart />,
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const Filters = () => {
|
|
569
|
-
const changeFilterActive = (index, value) => {
|
|
570
|
-
let newFilters = config.filters;
|
|
571
|
-
|
|
572
|
-
newFilters[index].active = value;
|
|
573
|
-
|
|
574
|
-
setConfig({...config, filters: newFilters});
|
|
575
|
-
|
|
576
|
-
setFilteredData(filterData(newFilters, excludedData));
|
|
577
|
-
};
|
|
578
630
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (config.filters) {
|
|
583
|
-
|
|
584
|
-
filterList = config.filters.map((singleFilter, index) => {
|
|
585
|
-
const values = [];
|
|
586
|
-
const sortAsc = (a, b) => {
|
|
587
|
-
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
588
|
-
};
|
|
631
|
+
if (abbreviated && axis === 'bottom') {
|
|
632
|
+
num = formatSuffix(parseFloat(num)).replace('G', 'B')
|
|
633
|
+
}
|
|
589
634
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
635
|
+
if (prefix && axis !== 'right') {
|
|
636
|
+
result += prefix
|
|
637
|
+
}
|
|
593
638
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
639
|
+
if (rightPrefix && axis === 'right') {
|
|
640
|
+
result += rightPrefix
|
|
641
|
+
}
|
|
597
642
|
|
|
598
|
-
|
|
599
|
-
singleFilter.values = singleFilter.values.sort(sortDesc)
|
|
600
|
-
}
|
|
643
|
+
result += num
|
|
601
644
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
645
|
+
if (suffix && axis !== 'right') {
|
|
646
|
+
result += suffix
|
|
647
|
+
}
|
|
605
648
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
<option key={index} value={filterOption}>
|
|
609
|
-
{filterOption}
|
|
610
|
-
</option>
|
|
611
|
-
);
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
return (
|
|
615
|
-
<div className="single-filter" key={index}>
|
|
616
|
-
<label htmlFor={`filter-${index}`}>{singleFilter.label}</label>
|
|
617
|
-
<select
|
|
618
|
-
id={`filter-${index}`}
|
|
619
|
-
className="filter-select"
|
|
620
|
-
data-index="0"
|
|
621
|
-
value={singleFilter.active}
|
|
622
|
-
onChange={(val) => {
|
|
623
|
-
changeFilterActive(index, val.target.value);
|
|
624
|
-
announceChange(`Filter ${singleFilter.label} value has been changed to ${val.target.value}, please reference the data table to see updated values.`);
|
|
625
|
-
}}
|
|
626
|
-
>
|
|
627
|
-
{values}
|
|
628
|
-
</select>
|
|
629
|
-
</div>
|
|
630
|
-
);
|
|
631
|
-
});
|
|
649
|
+
if (rightSuffix && axis === 'right') {
|
|
650
|
+
result += rightSuffix
|
|
632
651
|
}
|
|
633
652
|
|
|
634
|
-
return (
|
|
653
|
+
return String(result)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Select appropriate chart type
|
|
657
|
+
const chartComponents = {
|
|
658
|
+
'Paired Bar': <LinearChart />,
|
|
659
|
+
Bar: <LinearChart />,
|
|
660
|
+
Line: <LinearChart />,
|
|
661
|
+
Combo: <LinearChart />,
|
|
662
|
+
Pie: <PieChart />,
|
|
663
|
+
'Box Plot': <LinearChart />
|
|
635
664
|
}
|
|
636
665
|
|
|
637
666
|
const missingRequiredSections = () => {
|
|
638
667
|
if (config.visualizationType === 'Pie') {
|
|
639
668
|
if (undefined === config?.yAxis.dataKey) {
|
|
640
|
-
return true
|
|
669
|
+
return true
|
|
641
670
|
}
|
|
642
671
|
} else {
|
|
643
672
|
if (undefined === config?.series || false === config?.series.length > 0) {
|
|
644
|
-
return true
|
|
673
|
+
return true
|
|
645
674
|
}
|
|
646
675
|
}
|
|
647
676
|
|
|
648
677
|
if (!config.xAxis.dataKey) {
|
|
649
|
-
return true
|
|
678
|
+
return true
|
|
650
679
|
}
|
|
651
680
|
|
|
652
|
-
return false
|
|
653
|
-
}
|
|
681
|
+
return false
|
|
682
|
+
}
|
|
654
683
|
|
|
655
684
|
// Prevent render if loading
|
|
656
|
-
let body =
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if(!loading) {
|
|
660
|
-
|
|
685
|
+
let body = <Loading />
|
|
661
686
|
|
|
687
|
+
if (!loading) {
|
|
662
688
|
body = (
|
|
663
689
|
<>
|
|
664
690
|
{isEditor && <EditorPanel />}
|
|
665
691
|
{!missingRequiredSections() && !config.newViz && (
|
|
666
|
-
<div className=
|
|
692
|
+
<div className='cdc-chart-inner-container'>
|
|
667
693
|
{/* Title */}
|
|
668
|
-
|
|
694
|
+
|
|
669
695
|
{title && (
|
|
670
|
-
<div
|
|
671
|
-
|
|
672
|
-
className={`chart-title ${config.theme} cove-component__header`}
|
|
673
|
-
aria-level={2}
|
|
674
|
-
>
|
|
675
|
-
{config && (
|
|
676
|
-
<sup className="superTitle">{parse(config.superTitle || '')}</sup>
|
|
677
|
-
)}
|
|
696
|
+
<div role='heading' className={`chart-title ${config.theme} cove-component__header`} aria-level={2}>
|
|
697
|
+
{config && <sup className='superTitle'>{parse(config.superTitle || '')}</sup>}
|
|
678
698
|
<div>{parse(title)}</div>
|
|
679
699
|
</div>
|
|
680
700
|
)}
|
|
681
|
-
<a
|
|
682
|
-
id="skip-chart-container"
|
|
683
|
-
className="cdcdataviz-sr-only-focusable"
|
|
684
|
-
href={handleChartTabbing}
|
|
685
|
-
>
|
|
701
|
+
<a id='skip-chart-container' className='cdcdataviz-sr-only-focusable' href={handleChartTabbing}>
|
|
686
702
|
Skip Over Chart Container
|
|
687
703
|
</a>
|
|
688
704
|
{/* Filters */}
|
|
689
705
|
{config.filters && !externalFilters && <Filters />}
|
|
690
706
|
{/* Visualization */}
|
|
691
|
-
{config?.introText && <section className=
|
|
707
|
+
{config?.introText && <section className='introText'>{parse(config.introText)}</section>}
|
|
692
708
|
<div
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}${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(' ')}`}
|
|
696
711
|
>
|
|
697
712
|
{/* All charts except sparkline */}
|
|
698
|
-
{config.visualizationType !==
|
|
699
|
-
chartComponents[visualizationType]
|
|
700
|
-
}
|
|
713
|
+
{config.visualizationType !== 'Spark Line' && chartComponents[visualizationType]}
|
|
701
714
|
|
|
702
715
|
{/* Sparkline */}
|
|
703
|
-
{config.visualizationType ===
|
|
716
|
+
{config.visualizationType === 'Spark Line' && (
|
|
704
717
|
<>
|
|
705
|
-
{
|
|
718
|
+
{description && <div className='subtext'>{parse(description)}</div>}
|
|
706
719
|
<div style={sparkLineStyles}>
|
|
707
720
|
<ParentSize>
|
|
708
|
-
{
|
|
721
|
+
{parent => (
|
|
709
722
|
<>
|
|
710
723
|
<SparkLine width={parent.width} height={parent.height} />
|
|
711
724
|
</>
|
|
@@ -713,26 +726,32 @@ export default function CdcChart(
|
|
|
713
726
|
</ParentSize>
|
|
714
727
|
</div>
|
|
715
728
|
</>
|
|
716
|
-
)
|
|
717
|
-
}
|
|
718
|
-
{!config.legend.hide && config.visualizationType !== "Spark Line" && <Legend />}
|
|
729
|
+
)}
|
|
730
|
+
{!config.legend.hide && config.visualizationType !== 'Spark Line' && <Legend />}
|
|
719
731
|
</div>
|
|
720
732
|
{/* Link */}
|
|
721
733
|
{link && link}
|
|
722
734
|
{/* Description */}
|
|
723
|
-
{description && config.visualizationType !==
|
|
724
|
-
|
|
735
|
+
{description && config.visualizationType !== 'Spark Line' && <div className='subtext'>{parse(description)}</div>}
|
|
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>
|
|
725
742
|
|
|
726
|
-
{
|
|
727
|
-
{config
|
|
743
|
+
{/* Data Table */}
|
|
744
|
+
{config.xAxis.dataKey && config.table.show && config.visualizationType !== 'Spark Line' && <DataTable />}
|
|
745
|
+
{config?.footnotes && <section className='footnotes'>{parse(config.footnotes)}</section>}
|
|
746
|
+
{/* show pdf or image button */}
|
|
728
747
|
</div>
|
|
729
748
|
)}
|
|
730
749
|
</>
|
|
731
|
-
)
|
|
750
|
+
)
|
|
732
751
|
}
|
|
733
752
|
|
|
734
|
-
const getXAxisData = (d: any) => config.runtime.xAxis.type === 'date' ?
|
|
735
|
-
const getYAxisData = (d: any, seriesKey: string) => d[seriesKey]
|
|
753
|
+
const getXAxisData = (d: any) => (config.runtime.xAxis.type === 'date' ? parseDate(d[config.runtime.originalXAxis.dataKey]).getTime() : d[config.runtime.originalXAxis.dataKey])
|
|
754
|
+
const getYAxisData = (d: any, seriesKey: string) => d[seriesKey]
|
|
736
755
|
|
|
737
756
|
const contextValues = {
|
|
738
757
|
getXAxisData,
|
|
@@ -764,26 +783,22 @@ export default function CdcChart(
|
|
|
764
783
|
legend,
|
|
765
784
|
setSeriesHighlight,
|
|
766
785
|
dynamicLegendItems,
|
|
767
|
-
setDynamicLegendItems
|
|
786
|
+
setDynamicLegendItems,
|
|
787
|
+
filterData,
|
|
788
|
+
imageId
|
|
768
789
|
}
|
|
769
790
|
|
|
770
|
-
const classes = [
|
|
771
|
-
'cdc-open-viz-module',
|
|
772
|
-
'type-chart',
|
|
773
|
-
`${currentViewport}`,
|
|
774
|
-
`font-${config.fontSize}`,
|
|
775
|
-
`${config.theme}`
|
|
776
|
-
]
|
|
791
|
+
const classes = ['cdc-open-viz-module', 'type-chart', `${currentViewport}`, `font-${config.fontSize}`, `${config.theme}`]
|
|
777
792
|
|
|
778
|
-
config.visualizationType ===
|
|
793
|
+
config.visualizationType === 'Spark Line' && classes.push(`type-sparkline`)
|
|
779
794
|
isEditor && classes.push('spacing-wrapper')
|
|
780
795
|
isEditor && classes.push('isEditor')
|
|
781
796
|
|
|
782
797
|
return (
|
|
783
798
|
<Context.Provider value={contextValues}>
|
|
784
|
-
<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}>
|
|
785
800
|
{body}
|
|
786
801
|
</div>
|
|
787
802
|
</Context.Provider>
|
|
788
|
-
)
|
|
803
|
+
)
|
|
789
804
|
}
|