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