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