@cdc/editor 1.4.4 → 4.22.10-alpha.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/LICENSE +201 -0
- package/dist/cdceditor.js +70 -52
- package/example/supported-cities.csv +107 -0
- package/example/valid-data-map.csv +2 -1
- package/package.json +9 -9
- package/src/CdcEditor.js +67 -6
- package/src/components/ChooseTab.js +49 -38
- package/src/components/DataImport.js +330 -335
- package/src/scss/configure-tab.scss +4 -1
- package/src/scss/data-import.scss +32 -55
- package/src/scss/main.scss +11 -1
- package/src/assets/icons/dashboard.svg +0 -8
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import React, { useState, useContext, useEffect } from 'react'
|
|
2
2
|
import { useDropzone } from 'react-dropzone'
|
|
3
3
|
import { csvParse } from 'd3'
|
|
4
|
-
import { useDebounce } from 'use-debounce'
|
|
5
4
|
import { get } from 'axios'
|
|
6
5
|
|
|
7
|
-
import { DataTransform } from '@cdc/core/
|
|
8
|
-
import Modal from '@cdc/core/components/ui/Modal'
|
|
6
|
+
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
9
7
|
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
10
8
|
|
|
11
9
|
import GlobalState from '../context'
|
|
@@ -17,15 +15,18 @@ import LinkIcon from '../assets/icons/link.svg'
|
|
|
17
15
|
|
|
18
16
|
import FileUploadIcon from '../assets/icons/file-upload-solid.svg'
|
|
19
17
|
import CloseIcon from '@cdc/core/assets/icon-close.svg'
|
|
20
|
-
import validMapData from '../../example/valid-data-map.csv'
|
|
21
18
|
|
|
19
|
+
|
|
20
|
+
import validMapData from '../../example/valid-data-map.csv'
|
|
22
21
|
import validChartData from '../../example/valid-data-chart.csv'
|
|
23
22
|
import validCountyMapData from '../../example/valid-county-data.csv'
|
|
23
|
+
import sampleGeoPoints from '../../example/supported-cities.csv'
|
|
24
24
|
|
|
25
|
+
import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
25
26
|
|
|
26
27
|
import '../scss/data-import.scss'
|
|
27
|
-
|
|
28
|
-
import
|
|
28
|
+
|
|
29
|
+
import '@cdc/core/styles/v2/components/data-designer.scss'
|
|
29
30
|
|
|
30
31
|
export default function DataImport() {
|
|
31
32
|
const {
|
|
@@ -47,24 +48,35 @@ export default function DataImport() {
|
|
|
47
48
|
|
|
48
49
|
const [ externalURL, setExternalURL ] = useState(config.dataFileSourceType === 'url' ? config.dataFileName : (config.dataUrl || ''))
|
|
49
50
|
|
|
50
|
-
const [ debouncedExternalURL ] = useDebounce(externalURL, 200)
|
|
51
|
-
|
|
52
51
|
const [ keepURL, setKeepURL ] = useState(!!config.dataUrl)
|
|
53
52
|
|
|
53
|
+
const [ addingDataset, setAddingDataset ] = useState(config.type === 'dashboard' || !config.data);
|
|
54
|
+
|
|
55
|
+
const [ editingDataset, setEditingDataset ] = useState();
|
|
56
|
+
|
|
54
57
|
const supportedDataTypes = {
|
|
55
58
|
'.csv': 'text/csv',
|
|
56
59
|
'.json': 'application/json'
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const displayFileName = (name) => {
|
|
63
|
+
const nameParts = name.split('/');
|
|
64
|
+
return nameParts[nameParts.length - 1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const displaySize = (size) => {
|
|
68
|
+
if(size === undefined) return '';
|
|
69
|
+
|
|
70
|
+
if(size > Math.pow(1024, 3)){
|
|
71
|
+
return Math.round(size / Math.pow(1024, 3) * 100) / 100 + ' GB';
|
|
72
|
+
} else if(size > Math.pow(1024, 2)){
|
|
73
|
+
return Math.round(size / Math.pow(1024, 2) * 100) / 100 + ' MB';
|
|
74
|
+
} else if(size > 1024){
|
|
75
|
+
return Math.round(size / 1024 * 100) / 100 + ' KB';
|
|
62
76
|
} else {
|
|
63
|
-
|
|
64
|
-
delete newConfig.dataUrl;
|
|
65
|
-
setConfig(newConfig);
|
|
77
|
+
return size + ' B'
|
|
66
78
|
}
|
|
67
|
-
}
|
|
79
|
+
}
|
|
68
80
|
|
|
69
81
|
/**
|
|
70
82
|
* Check to see all series for the viz exists in the new dataset
|
|
@@ -126,12 +138,12 @@ export default function DataImport() {
|
|
|
126
138
|
return responseBlob
|
|
127
139
|
}
|
|
128
140
|
|
|
129
|
-
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile)
|
|
141
|
+
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile, editingDataset, editingDataset)
|
|
130
142
|
|
|
131
143
|
/**
|
|
132
144
|
* Handle loading data
|
|
133
145
|
*/
|
|
134
|
-
const loadData = async (fileBlob = null, fileName) => {
|
|
146
|
+
const loadData = async (fileBlob = null, fileName, editingDatasetKey) => {
|
|
135
147
|
let fileData = fileBlob
|
|
136
148
|
let fileSource = fileData?.path ?? fileName ?? null
|
|
137
149
|
let fileSourceType = 'file'
|
|
@@ -156,8 +168,10 @@ export default function DataImport() {
|
|
|
156
168
|
}
|
|
157
169
|
}
|
|
158
170
|
|
|
171
|
+
let fileSize = fileData.size;
|
|
172
|
+
|
|
159
173
|
// Check if file is too big
|
|
160
|
-
if (
|
|
174
|
+
if (fileSize > (maxFileSize * 1048576)) {
|
|
161
175
|
setErrors([ errorMessages.fileTooLarge ])
|
|
162
176
|
return
|
|
163
177
|
}
|
|
@@ -207,13 +221,43 @@ export default function DataImport() {
|
|
|
207
221
|
|
|
208
222
|
if (config.data && config.series) {
|
|
209
223
|
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
210
|
-
|
|
211
|
-
...config
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
if(config.type === 'dashboard'){
|
|
225
|
+
let newDatasets = {...config.datasets};
|
|
226
|
+
|
|
227
|
+
Object.keys(newDatasets).forEach(datasetKey => newDatasets[datasetKey].preview = false);
|
|
228
|
+
|
|
229
|
+
newDatasets[editingDatasetKey || fileSource] = {
|
|
230
|
+
data: text, // new data
|
|
231
|
+
dataFileSize: fileSize,
|
|
232
|
+
dataFileName: fileSource, // new file source
|
|
233
|
+
dataFileSourceType: fileSourceType,// new file source type
|
|
234
|
+
dataFileFormat: fileExtension.replace('.', '').toUpperCase(),
|
|
235
|
+
preview: true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if(keepURL){
|
|
239
|
+
newDatasets[editingDatasetKey || fileSource].dataUrl = fileSource;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setConfig({
|
|
243
|
+
...config,
|
|
244
|
+
...tempConfig,
|
|
245
|
+
dataset: newDatasets
|
|
246
|
+
})
|
|
247
|
+
} else {
|
|
248
|
+
let newConfig = {
|
|
249
|
+
...config,
|
|
250
|
+
...tempConfig,
|
|
251
|
+
data: text, // new data
|
|
252
|
+
dataFileName: fileSource, // new file source
|
|
253
|
+
dataFileSourceType: fileSourceType, // new file source type
|
|
254
|
+
formattedData: transform.developerStandardize(text, config.dataDescription)
|
|
255
|
+
}
|
|
256
|
+
if(keepURL){
|
|
257
|
+
newConfig.dataUrl = fileSource;
|
|
258
|
+
}
|
|
259
|
+
setConfig(newConfig)
|
|
260
|
+
}
|
|
217
261
|
} else {
|
|
218
262
|
resetEditor({
|
|
219
263
|
data: text,
|
|
@@ -222,8 +266,47 @@ export default function DataImport() {
|
|
|
222
266
|
}, 'It appears that your data does not contain all of the columns that your last dataset contained. Continuing will reset your configuration. Do you want to continue?')
|
|
223
267
|
}
|
|
224
268
|
} else {
|
|
225
|
-
|
|
269
|
+
if(config.type === 'dashboard') {
|
|
270
|
+
let newDatasets = {...config.datasets};
|
|
271
|
+
|
|
272
|
+
Object.keys(newDatasets).forEach(datasetKey => newDatasets[datasetKey].preview = false);
|
|
273
|
+
|
|
274
|
+
newDatasets[editingDatasetKey || fileSource] = {
|
|
275
|
+
data: text, // new data
|
|
276
|
+
dataFileSize: fileSize,
|
|
277
|
+
dataFileName: fileSource, // new file source
|
|
278
|
+
dataFileSourceType: fileSourceType,// new file source type
|
|
279
|
+
dataFileFormat: fileExtension.replace('.', '').toUpperCase(),
|
|
280
|
+
preview: true
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if(keepURL){
|
|
284
|
+
newDatasets[editingDatasetKey || fileSource].dataUrl = fileSource;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
setConfig({ ...config, datasets: newDatasets })
|
|
288
|
+
} else {
|
|
289
|
+
let newConfig = {
|
|
290
|
+
...config,
|
|
291
|
+
...tempConfig,
|
|
292
|
+
data: text, // new data
|
|
293
|
+
dataFileName: fileSource, // new file source
|
|
294
|
+
dataFileSourceType: fileSourceType, // new file source type
|
|
295
|
+
formattedData: transform.developerStandardize(text, config.dataDescription)// new file source type
|
|
296
|
+
}
|
|
297
|
+
if(keepURL){
|
|
298
|
+
newConfig.dataUrl = fileSource;
|
|
299
|
+
}
|
|
300
|
+
setConfig(newConfig)
|
|
301
|
+
}
|
|
226
302
|
}
|
|
303
|
+
|
|
304
|
+
if(editingDataset){
|
|
305
|
+
setEditingDataset(undefined);
|
|
306
|
+
}
|
|
307
|
+
setAddingDataset(false);
|
|
308
|
+
setExternalURL('');
|
|
309
|
+
setKeepURL(false);
|
|
227
310
|
} catch (err) {
|
|
228
311
|
setErrors(err)
|
|
229
312
|
}
|
|
@@ -249,16 +332,48 @@ export default function DataImport() {
|
|
|
249
332
|
setConfig(newConfig)
|
|
250
333
|
}, [])
|
|
251
334
|
|
|
252
|
-
const updateDescriptionProp = (key, value) => {
|
|
253
|
-
|
|
254
|
-
|
|
335
|
+
const updateDescriptionProp = (visualizationKey, datasetKey, key, value) => {
|
|
336
|
+
if(config.type === 'dashboard') {
|
|
337
|
+
let dataDescription = { ...config.datasets[datasetKey].dataDescription, [key]: value }
|
|
338
|
+
let formattedData = transform.developerStandardize(config.datasets[datasetKey].data, dataDescription)
|
|
255
339
|
|
|
256
|
-
|
|
340
|
+
let newDatasets = {...config.datasets}
|
|
341
|
+
newDatasets[datasetKey] = {...newDatasets[datasetKey], dataDescription, formattedData};
|
|
342
|
+
|
|
343
|
+
setConfig({ ...config, datasets: newDatasets })
|
|
344
|
+
} else {
|
|
345
|
+
let dataDescription = { ...config.dataDescription, [key]: value }
|
|
346
|
+
let formattedData = transform.developerStandardize(config.data, dataDescription)
|
|
347
|
+
|
|
348
|
+
setConfig({ ...config, formattedData, dataDescription })
|
|
349
|
+
}
|
|
257
350
|
}
|
|
258
351
|
|
|
352
|
+
const changeKeepURL = (value, editingDatasetKey) => {
|
|
353
|
+
if(editingDatasetKey){
|
|
354
|
+
let newDatasets = {...config.datasets};
|
|
355
|
+
if(value === false){
|
|
356
|
+
delete newDatasets[editingDatasetKey].dataUrl;
|
|
357
|
+
} else {
|
|
358
|
+
newDatasets[editingDatasetKey].dataUrl = newDatasets[editingDatasetKey].dataFileName;
|
|
359
|
+
}
|
|
360
|
+
setConfig({...config, datasets: newDatasets});
|
|
361
|
+
} else if(config.type !== 'dashboard') {
|
|
362
|
+
let newConfig = {...config};
|
|
363
|
+
if(value === false){
|
|
364
|
+
delete newConfig.dataUrl;
|
|
365
|
+
} else {
|
|
366
|
+
newConfig.dataUrl = newConfig.dataFileName;
|
|
367
|
+
}
|
|
368
|
+
setConfig(newConfig);
|
|
369
|
+
}
|
|
370
|
+
setKeepURL(value);
|
|
371
|
+
};
|
|
372
|
+
|
|
259
373
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
|
|
374
|
+
const { getRootProps: getRootProps2, getInputProps: getInputProps2, isDragActive: isDragActive2 } = useDropzone({ onDrop })
|
|
260
375
|
|
|
261
|
-
const loadFileFromUrl = (url) => {
|
|
376
|
+
const loadFileFromUrl = (url, editingDatasetKey) => {
|
|
262
377
|
// const extUrl = (url) ? url : config.dataFileName // set url to what is saved in config unless the user has entered something
|
|
263
378
|
|
|
264
379
|
return (
|
|
@@ -268,70 +383,193 @@ export default function DataImport() {
|
|
|
268
383
|
placeholder="e.g., https://data.cdc.gov/resources/file.json" aria-label="Load data from external URL"
|
|
269
384
|
aria-describedby="load-data" value={externalURL} onChange={(e) => setExternalURL(e.target.value)}/>
|
|
270
385
|
<button className="input-group-text btn btn-primary px-4" type="submit" id="load-data"
|
|
271
|
-
onClick={() => loadData(null, externalURL)}>Load
|
|
386
|
+
onClick={() => loadData(null, externalURL, editingDatasetKey)}>Load
|
|
272
387
|
</button>
|
|
273
388
|
</form>
|
|
274
389
|
<label htmlFor="keep-url" className="mt-1 d-flex keep-url">
|
|
275
|
-
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() =>
|
|
390
|
+
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() => changeKeepURL(!keepURL, editingDatasetKey)}/> Always
|
|
276
391
|
load from URL (normally will only pull once)
|
|
277
392
|
</label>
|
|
278
393
|
</>
|
|
279
394
|
)
|
|
280
395
|
}
|
|
281
396
|
|
|
282
|
-
const warningModal = () => {
|
|
283
|
-
return (
|
|
284
|
-
<Modal fontTheme={'light'} headerBgColor={'#d73636'} showClose={false}>
|
|
285
|
-
<Modal.Header>
|
|
286
|
-
<center>Warning</center>
|
|
287
|
-
</Modal.Header>
|
|
288
|
-
<Modal.Content>
|
|
289
|
-
<center>
|
|
290
|
-
<p style={{ fontSize: '1rem' }}>Reseting will remove your data and settings.</p>
|
|
291
|
-
</center>
|
|
292
|
-
</Modal.Content>
|
|
293
|
-
<Modal.Footer>
|
|
294
|
-
<div style={{ textAlign: 'center' }}>
|
|
295
|
-
<p style={{
|
|
296
|
-
marginBottom: '1rem',
|
|
297
|
-
fontSize: '1rem'
|
|
298
|
-
}}>
|
|
299
|
-
Are you sure you want to continue?
|
|
300
|
-
</p>
|
|
301
|
-
<Button className="warn" style={{ marginRight: '1rem' }}
|
|
302
|
-
onClick={() => overlay.actions.toggleOverlay(false)}
|
|
303
|
-
>No, Cancel</Button>
|
|
304
|
-
<Button className="success" onClick={() => {
|
|
305
|
-
resetEditor({})
|
|
306
|
-
overlay.actions.toggleOverlay(false)
|
|
307
|
-
}}>Yes, Continue</Button>
|
|
308
|
-
</div>
|
|
309
|
-
</Modal.Footer>
|
|
310
|
-
</Modal>
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
397
|
const resetEditor = (config = {}, message = 'Are you sure you want to do this?') => {
|
|
315
398
|
config.newViz = true
|
|
316
|
-
|
|
317
|
-
|
|
399
|
+
|
|
400
|
+
const confirmDataReset = window.confirm(message)
|
|
401
|
+
|
|
402
|
+
if (confirmDataReset === true) {
|
|
403
|
+
setTempConfig(null)
|
|
404
|
+
setConfig(config)
|
|
405
|
+
setAddingDataset(true)
|
|
406
|
+
}
|
|
318
407
|
}
|
|
319
408
|
|
|
320
409
|
const resetButton = () => {
|
|
321
|
-
return (
|
|
322
|
-
<
|
|
323
|
-
onClick={() =>
|
|
324
|
-
|
|
325
|
-
</
|
|
410
|
+
return ( //todo convert to modal
|
|
411
|
+
<button className="btn danger"
|
|
412
|
+
onClick={() => resetEditor({type: config.type, visualizationType: config.visualizationType}, 'Reseting will remove your data and settings. Do you want to continue?')}>Clear
|
|
413
|
+
<CloseIcon/>
|
|
414
|
+
</button>
|
|
326
415
|
)
|
|
327
416
|
}
|
|
328
417
|
|
|
418
|
+
const setGlobalDatasetProp = (datasetKey, prop, value) => {
|
|
419
|
+
let newDatasets = {...config.datasets};
|
|
420
|
+
|
|
421
|
+
if(value === true){
|
|
422
|
+
Object.keys(newDatasets).forEach(datasetKeyIter => {
|
|
423
|
+
if(datasetKeyIter !== datasetKey){
|
|
424
|
+
newDatasets[datasetKeyIter][prop] = false;
|
|
425
|
+
} else {
|
|
426
|
+
newDatasets[datasetKeyIter][prop] = true;
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
} else {
|
|
430
|
+
newDatasets[datasetKey][prop] = value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
setConfig({...config, datasets: newDatasets});
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const removeDataset = (datasetKey) => {
|
|
437
|
+
let newDatasets = {...config.datasets};
|
|
438
|
+
let newVisualizations = {...config.visualizations};
|
|
439
|
+
|
|
440
|
+
Object.keys(newVisualizations).forEach(vizKey => {
|
|
441
|
+
if(newVisualizations[vizKey].dataKey === datasetKey) {
|
|
442
|
+
delete newVisualizations[vizKey].dataKey;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
delete newDatasets[datasetKey];
|
|
447
|
+
|
|
448
|
+
setConfig({...config, datasets: newDatasets, visualizations: newVisualizations});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const renameDataset = (oldName, newName) => {
|
|
452
|
+
if(oldName === newName) return;
|
|
453
|
+
|
|
454
|
+
let newDatasets = {...config.datasets};
|
|
455
|
+
let newVisualizations = {...config.visualizations};
|
|
456
|
+
|
|
457
|
+
let suffix = 2;
|
|
458
|
+
let originalName = newName;
|
|
459
|
+
while(newDatasets[newName]){
|
|
460
|
+
newName = originalName + '-' + suffix;
|
|
461
|
+
suffix++;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
newDatasets[newName] = newDatasets[oldName];
|
|
465
|
+
delete newDatasets[oldName];
|
|
466
|
+
|
|
467
|
+
Object.keys(newVisualizations).forEach(vizKey => {
|
|
468
|
+
if(newVisualizations[vizKey].dataKey === oldName) {
|
|
469
|
+
newVisualizations[vizKey].dataKey = newName;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
setConfig({...config, datasets: newDatasets, visualizations: newVisualizations});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let previewData, configureData, readyToConfigure = false;
|
|
477
|
+
if(config.type === 'dashboard'){
|
|
478
|
+
readyToConfigure = Object.keys(config.datasets).length > 0;
|
|
479
|
+
Object.keys(config.datasets).forEach(datasetKey => {
|
|
480
|
+
if(config.datasets[datasetKey].preview){
|
|
481
|
+
previewData = config.datasets[datasetKey].data;
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
previewData = config.data;
|
|
486
|
+
configureData = config;
|
|
487
|
+
readyToConfigure = !!config.formattedData;
|
|
488
|
+
}
|
|
489
|
+
|
|
329
490
|
return (
|
|
330
491
|
<>
|
|
331
492
|
<div className="left-col">
|
|
332
|
-
{
|
|
493
|
+
{config.type === 'dashboard' && Object.keys(config.datasets).length > 0 && (
|
|
494
|
+
<>
|
|
495
|
+
<div className="heading-3">Data Sources</div>
|
|
496
|
+
<table>
|
|
497
|
+
<thead>
|
|
498
|
+
<tr>
|
|
499
|
+
<th>Name</th><th>Size</th><th>Type</th><th colSpan="4">Actions</th>
|
|
500
|
+
</tr>
|
|
501
|
+
</thead>
|
|
502
|
+
<tbody>
|
|
503
|
+
{Object.keys(config.datasets).map(datasetKey => config.datasets[datasetKey].dataFileName && (
|
|
504
|
+
<tr key={`tr-${datasetKey}`}>
|
|
505
|
+
<td><input className="dataset-name-input" type="text" defaultValue={datasetKey} onBlur={(e) => renameDataset(datasetKey, e.target.value)}/></td>
|
|
506
|
+
<td>{displaySize(config.datasets[datasetKey].dataFileSize)}</td>
|
|
507
|
+
<td>{config.datasets[datasetKey].dataFileFormat}</td>
|
|
508
|
+
<td><button className="btn btn-primary" onClick={() => setGlobalDatasetProp(datasetKey, 'preview', true)}>Preview Data</button></td>
|
|
509
|
+
<td><button className="btn btn-primary" onClick={() => {
|
|
510
|
+
if(editingDataset === datasetKey){
|
|
511
|
+
setEditingDataset(undefined);
|
|
512
|
+
setExternalURL('');
|
|
513
|
+
setKeepURL(false);
|
|
514
|
+
} else {
|
|
515
|
+
setEditingDataset(datasetKey);
|
|
516
|
+
setExternalURL(config.datasets[datasetKey].dataUrl || config.datasets[datasetKey].dataFileName);
|
|
517
|
+
setKeepURL(!!config.datasets[datasetKey].dataUrl);
|
|
518
|
+
}
|
|
519
|
+
}}>Edit Data</button></td>
|
|
520
|
+
<td><button className="btn btn-primary" onClick={() => removeDataset(datasetKey)}>X</button></td>
|
|
521
|
+
</tr>
|
|
522
|
+
))}
|
|
523
|
+
</tbody>
|
|
524
|
+
</table>
|
|
525
|
+
</>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
{configureData && configureData.data && (
|
|
529
|
+
<>
|
|
530
|
+
{config.type !== 'dashboard' && (
|
|
531
|
+
<>
|
|
532
|
+
<div className="heading-3">Data Source</div>
|
|
533
|
+
<div className="file-loaded-area">
|
|
534
|
+
{config.dataFileSourceType === 'file' && (
|
|
535
|
+
<div className="data-source-options">
|
|
536
|
+
<div
|
|
537
|
+
className={isDragActive2 ? 'drag-active cdcdataviz-file-selector loaded-file' : 'cdcdataviz-file-selector loaded-file'} {...getRootProps2()}>
|
|
538
|
+
<input {...getInputProps2()} />
|
|
539
|
+
{
|
|
540
|
+
isDragActive2 ?
|
|
541
|
+
<p>Drop file here</p> :
|
|
542
|
+
<p><FileUploadIcon/> <span>{config.dataFileName ?? 'Replace data file'}</span></p>
|
|
543
|
+
}
|
|
544
|
+
</div>
|
|
545
|
+
<div>
|
|
546
|
+
{resetButton()}
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
|
|
551
|
+
{config.dataFileSourceType === 'url' && (
|
|
552
|
+
<div className="url-source-options">
|
|
553
|
+
<div>
|
|
554
|
+
{loadFileFromUrl(externalURL)}
|
|
555
|
+
</div>
|
|
556
|
+
<div>
|
|
557
|
+
{resetButton()}
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
</>
|
|
563
|
+
)}
|
|
564
|
+
|
|
565
|
+
<DataDesigner visuzliationKey={null} dataKey={configureData.dataFileName} configureData={configureData} updateDescriptionProp={updateDescriptionProp} />
|
|
566
|
+
</>
|
|
567
|
+
)}
|
|
568
|
+
|
|
569
|
+
{(editingDataset || addingDataset) && ( // dataFileSourceType needs to be checked here since earlier versions did not track this state
|
|
333
570
|
<div className="load-data-area">
|
|
334
|
-
<
|
|
571
|
+
<div className="heading-3">{editingDataset ? `Editing ${editingDataset}` : 'Add Dataset'}</div>
|
|
572
|
+
<Tabs startingTab={editingDataset && config.datasets[editingDataset].dataFileSourceType === 'url' ? 1 : 0}>
|
|
335
573
|
<TabPane title="Upload File" icon={<FileUploadIcon className="inline-icon"/>}>
|
|
336
574
|
{sharepath &&
|
|
337
575
|
<p className="alert--info">
|
|
@@ -349,7 +587,7 @@ export default function DataImport() {
|
|
|
349
587
|
</div>
|
|
350
588
|
</TabPane>
|
|
351
589
|
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"/>}>
|
|
352
|
-
{loadFileFromUrl(externalURL)}
|
|
590
|
+
{loadFileFromUrl(editingDataset && config.datasets[editingDataset].dataFileSourceType === 'url' ? config.datasets[editingDataset].dataFileName : externalURL, editingDataset)}
|
|
353
591
|
</TabPane>
|
|
354
592
|
</Tabs>
|
|
355
593
|
{errors && (errors.map ? errors.map((message, index) => (
|
|
@@ -364,282 +602,39 @@ export default function DataImport() {
|
|
|
364
602
|
<span className="heading-3">Load Sample Data:</span>
|
|
365
603
|
<ul className="sample-data-list">
|
|
366
604
|
<li
|
|
367
|
-
onClick={() => loadData(new Blob([ validMapData ], { type: 'text/csv' }), 'valid-data-map.csv')}>United
|
|
605
|
+
onClick={() => loadData(new Blob([ validMapData ], { type: 'text/csv' }), 'valid-data-map.csv', editingDataset)}>United
|
|
368
606
|
States Sample Data #1
|
|
369
607
|
</li>
|
|
370
608
|
<li
|
|
371
|
-
onClick={() => loadData(new Blob([ validChartData ], { type: 'text/csv' }), 'valid-data-chart.csv')}>Chart
|
|
609
|
+
onClick={() => loadData(new Blob([ validChartData ], { type: 'text/csv' }), 'valid-data-chart.csv', editingDataset)}>Chart
|
|
372
610
|
Sample Data
|
|
373
611
|
</li>
|
|
374
612
|
<li
|
|
375
|
-
onClick={() => loadData(new Blob([ validCountyMapData ], { type: 'text/csv' }), 'valid-county-data.csv')}>United
|
|
613
|
+
onClick={() => loadData(new Blob([ validCountyMapData ], { type: 'text/csv' }), 'valid-county-data.csv', editingDataset)}>United
|
|
376
614
|
States Counties Sample Data
|
|
377
615
|
</li>
|
|
616
|
+
<li
|
|
617
|
+
onClick={() => loadData(new Blob([ sampleGeoPoints ], { type: 'text/csv' }), 'supported-cities.csv', editingDataset)}>
|
|
618
|
+
Sample Geo Points
|
|
619
|
+
</li>
|
|
378
620
|
</ul>
|
|
379
|
-
<a href="https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html" target="_blank"
|
|
380
|
-
rel="noopener noreferrer" className="guidance-link">
|
|
381
|
-
<div>
|
|
382
|
-
<h3>Get Help</h3>
|
|
383
|
-
<p>Documentation and examples on formatting data and configuring visualizations.</p>
|
|
384
|
-
</div>
|
|
385
|
-
</a>
|
|
386
621
|
</div>
|
|
387
622
|
)}
|
|
388
623
|
|
|
389
|
-
{config.
|
|
624
|
+
{config.type === 'dashboard' && !addingDataset && <p><button className="btn btn-primary" onClick={() => setAddingDataset(true)}>+ Add More Files</button></p>}
|
|
625
|
+
|
|
626
|
+
{readyToConfigure && <p><button className="btn btn-primary" onClick={() => setGlobalActive(2)}>Configure your visualization</button></p>}
|
|
627
|
+
|
|
628
|
+
<a href="https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html" target="_blank"
|
|
629
|
+
rel="noopener noreferrer" className="guidance-link">
|
|
390
630
|
<div>
|
|
391
|
-
<
|
|
392
|
-
<
|
|
393
|
-
{config.dataFileSourceType === 'file' && (
|
|
394
|
-
<div className="data-source-options">
|
|
395
|
-
<div
|
|
396
|
-
className={isDragActive ? 'drag-active cdcdataviz-file-selector loaded-file' : 'cdcdataviz-file-selector loaded-file'} {...getRootProps()}>
|
|
397
|
-
<input {...getInputProps()} />
|
|
398
|
-
{
|
|
399
|
-
isDragActive ?
|
|
400
|
-
<p>Drop file here</p> :
|
|
401
|
-
<p><FileUploadIcon/> <span>{config.dataFileName ?? 'Replace data file'}</span></p>
|
|
402
|
-
}
|
|
403
|
-
</div>
|
|
404
|
-
<div>
|
|
405
|
-
{resetButton()}
|
|
406
|
-
</div>
|
|
407
|
-
</div>
|
|
408
|
-
)}
|
|
409
|
-
|
|
410
|
-
{config.dataFileSourceType === 'url' && (
|
|
411
|
-
<div className="url-source-options">
|
|
412
|
-
<div>
|
|
413
|
-
{loadFileFromUrl(externalURL)}
|
|
414
|
-
</div>
|
|
415
|
-
<div>
|
|
416
|
-
{resetButton()}
|
|
417
|
-
</div>
|
|
418
|
-
</div>
|
|
419
|
-
)}
|
|
420
|
-
</div>
|
|
421
|
-
<div className="question">
|
|
422
|
-
<div className="heading-3">Describe Data</div>
|
|
423
|
-
<div className="heading-4 data-question">Data Orientation</div>
|
|
424
|
-
<div className="table-button-container">
|
|
425
|
-
<div
|
|
426
|
-
className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === false ? ' active' : '')}
|
|
427
|
-
onClick={() => {
|
|
428
|
-
updateDescriptionProp('horizontal', false)
|
|
429
|
-
}}>
|
|
430
|
-
<strong>Vertical</strong>
|
|
431
|
-
<p>Values for map geography or chart date/category axis are contained in a single <em>column</em>.</p>
|
|
432
|
-
<table>
|
|
433
|
-
<tbody>
|
|
434
|
-
<tr>
|
|
435
|
-
<th>Date</th>
|
|
436
|
-
<th>Value</th>
|
|
437
|
-
<th>...</th>
|
|
438
|
-
</tr>
|
|
439
|
-
<tr>
|
|
440
|
-
<td>01/01/2020</td>
|
|
441
|
-
<td>150</td>
|
|
442
|
-
<td>...</td>
|
|
443
|
-
</tr>
|
|
444
|
-
<tr>
|
|
445
|
-
<td>02/01/2020</td>
|
|
446
|
-
<td>150</td>
|
|
447
|
-
<td>...</td>
|
|
448
|
-
</tr>
|
|
449
|
-
</tbody>
|
|
450
|
-
</table>
|
|
451
|
-
<table>
|
|
452
|
-
<tbody>
|
|
453
|
-
<tr>
|
|
454
|
-
<th>State</th>
|
|
455
|
-
<th>Value</th>
|
|
456
|
-
<th>...</th>
|
|
457
|
-
</tr>
|
|
458
|
-
<tr>
|
|
459
|
-
<td>Georgia</td>
|
|
460
|
-
<td>150</td>
|
|
461
|
-
<td>...</td>
|
|
462
|
-
</tr>
|
|
463
|
-
<tr>
|
|
464
|
-
<td>Florida</td>
|
|
465
|
-
<td>150</td>
|
|
466
|
-
<td>...</td>
|
|
467
|
-
</tr>
|
|
468
|
-
</tbody>
|
|
469
|
-
</table>
|
|
470
|
-
</div>
|
|
471
|
-
<div
|
|
472
|
-
className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === true ? ' active' : '')}
|
|
473
|
-
onClick={() => {
|
|
474
|
-
updateDescriptionProp('horizontal', true)
|
|
475
|
-
}}>
|
|
476
|
-
<strong>Horizontal</strong>
|
|
477
|
-
<p>Values for map geography or chart date/category axis are contained in a single <em>row</em></p>
|
|
478
|
-
<table>
|
|
479
|
-
<tbody>
|
|
480
|
-
<tr>
|
|
481
|
-
<th>Date</th>
|
|
482
|
-
<td>01/01/2020</td>
|
|
483
|
-
<td>02/01/2020</td>
|
|
484
|
-
<td>...</td>
|
|
485
|
-
</tr>
|
|
486
|
-
<tr>
|
|
487
|
-
<th>Value</th>
|
|
488
|
-
<td>100</td>
|
|
489
|
-
<td>150</td>
|
|
490
|
-
<td>...</td>
|
|
491
|
-
</tr>
|
|
492
|
-
</tbody>
|
|
493
|
-
</table>
|
|
494
|
-
<table>
|
|
495
|
-
<tbody>
|
|
496
|
-
<tr>
|
|
497
|
-
<th>State</th>
|
|
498
|
-
<td>Georgia</td>
|
|
499
|
-
<td>Florida</td>
|
|
500
|
-
<td>...</td>
|
|
501
|
-
</tr>
|
|
502
|
-
<tr>
|
|
503
|
-
<th>Value</th>
|
|
504
|
-
<td>100</td>
|
|
505
|
-
<td>150</td>
|
|
506
|
-
<td>...</td>
|
|
507
|
-
</tr>
|
|
508
|
-
</tbody>
|
|
509
|
-
</table>
|
|
510
|
-
</div>
|
|
511
|
-
</div>
|
|
512
|
-
</div>
|
|
513
|
-
{config.dataDescription && (
|
|
514
|
-
<>
|
|
515
|
-
<div className="question">
|
|
516
|
-
<div className="heading-4 data-question">Are there multiple series represented in your data?</div>
|
|
517
|
-
<div>
|
|
518
|
-
<button className={config.dataDescription.series === true ? 'btn btn-primary active' : 'btn btn-primary'} style={{ marginRight: '.5em' }} onClick={() => { updateDescriptionProp('series', true) }}>Yes</button>
|
|
519
|
-
<button className={config.dataDescription.series === false ? 'btn btn-primary active' : 'btn btn-primary'} onClick={() => {updateDescriptionProp('series', false)}}>No</button>
|
|
520
|
-
</div>
|
|
521
|
-
</div>
|
|
522
|
-
{config.dataDescription.horizontal === true && config.dataDescription.series === true && (
|
|
523
|
-
<div className="question">
|
|
524
|
-
<div className="heading-4 data-question">Which property in the dataset represents which series the row is describing?</div>
|
|
525
|
-
<select onChange={(e) => {updateDescriptionProp('seriesKey', e.target.value)}} value={config.dataDescription.seriesKey}>
|
|
526
|
-
<option value="">Choose an option</option>
|
|
527
|
-
{Object.keys(config.data[0]).map((value, index) => <option value={value} key={index}>{value}</option>)}
|
|
528
|
-
</select>
|
|
529
|
-
</div>
|
|
530
|
-
)}
|
|
531
|
-
{config.dataDescription.horizontal === false && config.dataDescription.series === true && (
|
|
532
|
-
<>
|
|
533
|
-
<div className="question">
|
|
534
|
-
<div className="heading-4 data-question">Are the series values in your data represented in a single row, or across multiple rows?</div>
|
|
535
|
-
<div className="table-button-container">
|
|
536
|
-
<div className={'table-button' + (config.dataDescription.singleRow === true ? ' active' : '')} onClick={() => {updateDescriptionProp('singleRow', true)}}>
|
|
537
|
-
<p>Each row contains the data for an individual series in itself.</p>
|
|
538
|
-
<table>
|
|
539
|
-
<tbody>
|
|
540
|
-
<tr>
|
|
541
|
-
<th>Date</th>
|
|
542
|
-
<th>Virus 1</th>
|
|
543
|
-
<th>Virus 2</th>
|
|
544
|
-
<th>...</th>
|
|
545
|
-
</tr>
|
|
546
|
-
<tr>
|
|
547
|
-
<td>01/01/2020</td>
|
|
548
|
-
<td>100</td>
|
|
549
|
-
<td>150</td>
|
|
550
|
-
<td>...</td>
|
|
551
|
-
</tr>
|
|
552
|
-
<tr>
|
|
553
|
-
<td>02/01/2020</td>
|
|
554
|
-
<td>15</td>
|
|
555
|
-
<td>20</td>
|
|
556
|
-
<td>...</td>
|
|
557
|
-
</tr>
|
|
558
|
-
</tbody>
|
|
559
|
-
</table>
|
|
560
|
-
</div>
|
|
561
|
-
<div className={'table-button' + (config.dataDescription.singleRow === false ? ' active' : '')} onClick={() => {updateDescriptionProp('singleRow', false)}}>
|
|
562
|
-
<p>Each series data is broken out into multiple rows.</p>
|
|
563
|
-
<table>
|
|
564
|
-
<tbody>
|
|
565
|
-
<tr>
|
|
566
|
-
<th>Virus</th>
|
|
567
|
-
<th>Date</th>
|
|
568
|
-
<th>Value</th>
|
|
569
|
-
</tr>
|
|
570
|
-
<tr>
|
|
571
|
-
<td>Virus 1</td>
|
|
572
|
-
<td>01/01/2020</td>
|
|
573
|
-
<td>100</td>
|
|
574
|
-
</tr>
|
|
575
|
-
<tr>
|
|
576
|
-
<td>Virus 1</td>
|
|
577
|
-
<td>02/01/2020</td>
|
|
578
|
-
<td>150</td>
|
|
579
|
-
</tr>
|
|
580
|
-
<tr>
|
|
581
|
-
<td>...</td>
|
|
582
|
-
<td>...</td>
|
|
583
|
-
<td>...</td>
|
|
584
|
-
</tr>
|
|
585
|
-
<tr>
|
|
586
|
-
<td>Virus 2</td>
|
|
587
|
-
<td>01/01/2020</td>
|
|
588
|
-
<td>15</td>
|
|
589
|
-
</tr>
|
|
590
|
-
<tr>
|
|
591
|
-
<td>Virus 2</td>
|
|
592
|
-
<td>02/01/2020</td>
|
|
593
|
-
<td>20</td>
|
|
594
|
-
</tr>
|
|
595
|
-
<tr>
|
|
596
|
-
<td>...</td>
|
|
597
|
-
<td>...</td>
|
|
598
|
-
<td>...</td>
|
|
599
|
-
</tr>
|
|
600
|
-
</tbody>
|
|
601
|
-
</table>
|
|
602
|
-
</div>
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
{config.dataDescription.singleRow === false && (
|
|
606
|
-
<>
|
|
607
|
-
<div className="question">
|
|
608
|
-
<div className="heading-4 data-question">Which property in the dataset represents which series the row is describing?</div>
|
|
609
|
-
<select onChange={(e) => {updateDescriptionProp('seriesKey', e.target.value)}}>
|
|
610
|
-
<option value="">Choose an option</option>
|
|
611
|
-
{Object.keys(config.data[0]).map((value, index) => <option value={value} key={index}>{value}</option>)}
|
|
612
|
-
</select>
|
|
613
|
-
</div>
|
|
614
|
-
<div className="question">
|
|
615
|
-
<div className="heading-4 data-question">Which property in the dataset represents the values for the category/date axis or map geography?</div>
|
|
616
|
-
<select onChange={(e) => {updateDescriptionProp('xKey', e.target.value)}}>
|
|
617
|
-
<option value="">Choose an option</option>
|
|
618
|
-
{Object.keys(config.data[0]).map((value, index) => <option value={value} key={index}>{value}</option>)}
|
|
619
|
-
</select>
|
|
620
|
-
</div>
|
|
621
|
-
<div className="question">
|
|
622
|
-
<div className="heading-4 data-question">Which property in the dataset represents the numeric value?</div>
|
|
623
|
-
<select onChange={(e) => {updateDescriptionProp('valueKey', e.target.value)}}>
|
|
624
|
-
<option value="">Choose an option</option>
|
|
625
|
-
{Object.keys(config.data[0]).map((value, index) => <option value={value} key={index}>{value}</option>)}
|
|
626
|
-
</select>
|
|
627
|
-
</div>
|
|
628
|
-
</>
|
|
629
|
-
)}
|
|
630
|
-
</>
|
|
631
|
-
)}
|
|
632
|
-
</>
|
|
633
|
-
)}
|
|
634
|
-
{config.formattedData && (
|
|
635
|
-
<button className="btn btn-primary" style={{ float: 'right', marginBottom: '2em' }}
|
|
636
|
-
onClick={() => setGlobalActive(1)}>Select your visualization type »</button>
|
|
637
|
-
)}
|
|
631
|
+
<h3>Get Help</h3>
|
|
632
|
+
<p>Documentation and examples on formatting data and configuring visualizations.</p>
|
|
638
633
|
</div>
|
|
639
|
-
|
|
634
|
+
</a>
|
|
640
635
|
</div>
|
|
641
636
|
<div className="right-col">
|
|
642
|
-
<PreviewDataTable data={
|
|
637
|
+
<PreviewDataTable data={previewData}/>
|
|
643
638
|
</div>
|
|
644
639
|
</>
|
|
645
640
|
)
|