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