@cdc/editor 1.4.3 → 4.22.10
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/private/map.csv +60 -0
- package/example/private/test.html +1 -0
- package/example/supported-cities.csv +107 -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 +236 -85
- package/src/components/DataImport.js +337 -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,32 @@
|
|
|
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
18
|
|
|
19
|
+
|
|
17
20
|
import validMapData from '../../example/valid-data-map.csv'
|
|
18
21
|
import validChartData from '../../example/valid-data-chart.csv'
|
|
19
22
|
import validCountyMapData from '../../example/valid-county-data.csv'
|
|
23
|
+
import sampleGeoPoints from '../../example/supported-cities.csv'
|
|
20
24
|
|
|
21
|
-
import
|
|
25
|
+
import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
26
|
+
|
|
27
|
+
import '../scss/data-import.scss'
|
|
28
|
+
|
|
29
|
+
import '@cdc/core/styles/v2/components/data-designer.scss'
|
|
22
30
|
|
|
23
31
|
export default function DataImport() {
|
|
24
32
|
const {
|
|
@@ -34,28 +42,41 @@ export default function DataImport() {
|
|
|
34
42
|
sharepath
|
|
35
43
|
} = useContext(GlobalState)
|
|
36
44
|
|
|
45
|
+
const { overlay } = useGlobalContext()
|
|
46
|
+
|
|
37
47
|
const transform = new DataTransform()
|
|
38
48
|
|
|
39
49
|
const [ externalURL, setExternalURL ] = useState(config.dataFileSourceType === 'url' ? config.dataFileName : (config.dataUrl || ''))
|
|
40
50
|
|
|
41
|
-
const [ debouncedExternalURL ] = useDebounce(externalURL, 200)
|
|
42
|
-
|
|
43
51
|
const [ keepURL, setKeepURL ] = useState(!!config.dataUrl)
|
|
44
52
|
|
|
53
|
+
const [ addingDataset, setAddingDataset ] = useState(config.type === 'dashboard' || !config.data);
|
|
54
|
+
|
|
55
|
+
const [ editingDataset, setEditingDataset ] = useState();
|
|
56
|
+
|
|
45
57
|
const supportedDataTypes = {
|
|
46
58
|
'.csv': 'text/csv',
|
|
47
59
|
'.json': 'application/json'
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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';
|
|
53
76
|
} else {
|
|
54
|
-
|
|
55
|
-
delete newConfig.dataUrl;
|
|
56
|
-
setConfig(newConfig);
|
|
77
|
+
return size + ' B'
|
|
57
78
|
}
|
|
58
|
-
}
|
|
79
|
+
}
|
|
59
80
|
|
|
60
81
|
/**
|
|
61
82
|
* Check to see all series for the viz exists in the new dataset
|
|
@@ -117,19 +138,27 @@ export default function DataImport() {
|
|
|
117
138
|
return responseBlob
|
|
118
139
|
}
|
|
119
140
|
|
|
120
|
-
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile)
|
|
141
|
+
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile, editingDataset, editingDataset)
|
|
121
142
|
|
|
122
143
|
/**
|
|
123
144
|
* Handle loading data
|
|
124
145
|
*/
|
|
125
|
-
const loadData = async (fileBlob = null, fileName) => {
|
|
146
|
+
const loadData = async (fileBlob = null, fileName, editingDatasetKey) => {
|
|
126
147
|
let fileData = fileBlob
|
|
127
148
|
let fileSource = fileData?.path ?? fileName ?? null
|
|
128
149
|
let fileSourceType = 'file'
|
|
129
150
|
|
|
130
151
|
// Get the raw data as text from the file
|
|
131
152
|
if (null === fileData) {
|
|
132
|
-
|
|
153
|
+
// const round = 1000 * 60 * 15;
|
|
154
|
+
// const date = new Date();
|
|
155
|
+
// const rounded = new Date(date.getTime() - (date.getTime() % round));
|
|
156
|
+
// const trimmedDate = rounded.toString().replace(/\s+/g, "");
|
|
157
|
+
|
|
158
|
+
const newUrl = new URL(fileName);
|
|
159
|
+
// newUrl.searchParams.append("v", trimmedDate);
|
|
160
|
+
|
|
161
|
+
fileSourceType = "url";
|
|
133
162
|
try {
|
|
134
163
|
fileData = await loadExternal()
|
|
135
164
|
fileSource = externalURL
|
|
@@ -139,8 +168,10 @@ export default function DataImport() {
|
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
170
|
|
|
171
|
+
let fileSize = fileData.size;
|
|
172
|
+
|
|
142
173
|
// Check if file is too big
|
|
143
|
-
if (
|
|
174
|
+
if (fileSize > (maxFileSize * 1048576)) {
|
|
144
175
|
setErrors([ errorMessages.fileTooLarge ])
|
|
145
176
|
return
|
|
146
177
|
}
|
|
@@ -190,13 +221,43 @@ export default function DataImport() {
|
|
|
190
221
|
|
|
191
222
|
if (config.data && config.series) {
|
|
192
223
|
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
193
|
-
|
|
194
|
-
...config
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}
|
|
200
261
|
} else {
|
|
201
262
|
resetEditor({
|
|
202
263
|
data: text,
|
|
@@ -205,8 +266,47 @@ export default function DataImport() {
|
|
|
205
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?')
|
|
206
267
|
}
|
|
207
268
|
} else {
|
|
208
|
-
|
|
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
|
+
}
|
|
209
302
|
}
|
|
303
|
+
|
|
304
|
+
if(editingDataset){
|
|
305
|
+
setEditingDataset(undefined);
|
|
306
|
+
}
|
|
307
|
+
setAddingDataset(false);
|
|
308
|
+
setExternalURL('');
|
|
309
|
+
setKeepURL(false);
|
|
210
310
|
} catch (err) {
|
|
211
311
|
setErrors(err)
|
|
212
312
|
}
|
|
@@ -232,18 +332,50 @@ export default function DataImport() {
|
|
|
232
332
|
setConfig(newConfig)
|
|
233
333
|
}, [])
|
|
234
334
|
|
|
235
|
-
const updateDescriptionProp = (key, value) => {
|
|
236
|
-
|
|
237
|
-
|
|
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)
|
|
339
|
+
|
|
340
|
+
let newDatasets = {...config.datasets}
|
|
341
|
+
newDatasets[datasetKey] = {...newDatasets[datasetKey], dataDescription, formattedData};
|
|
238
342
|
|
|
239
|
-
|
|
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
|
+
}
|
|
240
350
|
}
|
|
241
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
|
+
|
|
242
373
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
|
|
374
|
+
const { getRootProps: getRootProps2, getInputProps: getInputProps2, isDragActive: isDragActive2 } = useDropzone({ onDrop })
|
|
243
375
|
|
|
244
|
-
const loadFileFromUrl = (url) => {
|
|
376
|
+
const loadFileFromUrl = (url, editingDatasetKey) => {
|
|
245
377
|
// const extUrl = (url) ? url : config.dataFileName // set url to what is saved in config unless the user has entered something
|
|
246
|
-
|
|
378
|
+
|
|
247
379
|
return (
|
|
248
380
|
<>
|
|
249
381
|
<form className="input-group d-flex" onSubmit={(e) => e.preventDefault()}>
|
|
@@ -251,11 +383,11 @@ export default function DataImport() {
|
|
|
251
383
|
placeholder="e.g., https://data.cdc.gov/resources/file.json" aria-label="Load data from external URL"
|
|
252
384
|
aria-describedby="load-data" value={externalURL} onChange={(e) => setExternalURL(e.target.value)}/>
|
|
253
385
|
<button className="input-group-text btn btn-primary px-4" type="submit" id="load-data"
|
|
254
|
-
onClick={() => loadData(null, externalURL)}>Load
|
|
386
|
+
onClick={() => loadData(null, externalURL, editingDatasetKey)}>Load
|
|
255
387
|
</button>
|
|
256
388
|
</form>
|
|
257
389
|
<label htmlFor="keep-url" className="mt-1 d-flex keep-url">
|
|
258
|
-
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() =>
|
|
390
|
+
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() => changeKeepURL(!keepURL, editingDatasetKey)}/> Always
|
|
259
391
|
load from URL (normally will only pull once)
|
|
260
392
|
</label>
|
|
261
393
|
</>
|
|
@@ -264,29 +396,180 @@ export default function DataImport() {
|
|
|
264
396
|
|
|
265
397
|
const resetEditor = (config = {}, message = 'Are you sure you want to do this?') => {
|
|
266
398
|
config.newViz = true
|
|
399
|
+
|
|
267
400
|
const confirmDataReset = window.confirm(message)
|
|
268
401
|
|
|
269
402
|
if (confirmDataReset === true) {
|
|
270
403
|
setTempConfig(null)
|
|
271
404
|
setConfig(config)
|
|
405
|
+
setAddingDataset(true)
|
|
272
406
|
}
|
|
273
407
|
}
|
|
274
408
|
|
|
275
409
|
const resetButton = () => {
|
|
276
410
|
return ( //todo convert to modal
|
|
277
411
|
<button className="btn danger"
|
|
278
|
-
onClick={() => resetEditor({}, 'Reseting will remove your data and settings. Do you want to continue?')}>Clear
|
|
412
|
+
onClick={() => resetEditor({type: config.type, visualizationType: config.visualizationType}, 'Reseting will remove your data and settings. Do you want to continue?')}>Clear
|
|
279
413
|
<CloseIcon/>
|
|
280
414
|
</button>
|
|
281
415
|
)
|
|
282
416
|
}
|
|
283
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
|
+
|
|
284
490
|
return (
|
|
285
491
|
<>
|
|
286
492
|
<div className="left-col">
|
|
287
|
-
{
|
|
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
|
|
288
570
|
<div className="load-data-area">
|
|
289
|
-
<
|
|
571
|
+
<div className="heading-3">{editingDataset ? `Editing ${editingDataset}` : 'Add Dataset'}</div>
|
|
572
|
+
<Tabs startingTab={editingDataset && config.datasets[editingDataset].dataFileSourceType === 'url' ? 1 : 0}>
|
|
290
573
|
<TabPane title="Upload File" icon={<FileUploadIcon className="inline-icon"/>}>
|
|
291
574
|
{sharepath &&
|
|
292
575
|
<p className="alert--info">
|
|
@@ -304,7 +587,7 @@ export default function DataImport() {
|
|
|
304
587
|
</div>
|
|
305
588
|
</TabPane>
|
|
306
589
|
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"/>}>
|
|
307
|
-
{loadFileFromUrl(externalURL)}
|
|
590
|
+
{loadFileFromUrl(editingDataset && config.datasets[editingDataset].dataFileSourceType === 'url' ? config.datasets[editingDataset].dataFileName : externalURL, editingDataset)}
|
|
308
591
|
</TabPane>
|
|
309
592
|
</Tabs>
|
|
310
593
|
{errors && (errors.map ? errors.map((message, index) => (
|
|
@@ -319,282 +602,39 @@ export default function DataImport() {
|
|
|
319
602
|
<span className="heading-3">Load Sample Data:</span>
|
|
320
603
|
<ul className="sample-data-list">
|
|
321
604
|
<li
|
|
322
|
-
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
|
|
323
606
|
States Sample Data #1
|
|
324
607
|
</li>
|
|
325
608
|
<li
|
|
326
|
-
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
|
|
327
610
|
Sample Data
|
|
328
611
|
</li>
|
|
329
612
|
<li
|
|
330
|
-
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
|
|
331
614
|
States Counties Sample Data
|
|
332
615
|
</li>
|
|
616
|
+
<li
|
|
617
|
+
onClick={() => loadData(new Blob([ sampleGeoPoints ], { type: 'text/csv' }), 'supported-cities.csv', editingDataset)}>
|
|
618
|
+
Sample Geo Points
|
|
619
|
+
</li>
|
|
333
620
|
</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
621
|
</div>
|
|
342
622
|
)}
|
|
343
623
|
|
|
344
|
-
{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">
|
|
345
630
|
<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
|
-
)}
|
|
631
|
+
<h3>Get Help</h3>
|
|
632
|
+
<p>Documentation and examples on formatting data and configuring visualizations.</p>
|
|
593
633
|
</div>
|
|
594
|
-
|
|
634
|
+
</a>
|
|
595
635
|
</div>
|
|
596
636
|
<div className="right-col">
|
|
597
|
-
<PreviewDataTable data={
|
|
637
|
+
<PreviewDataTable data={previewData}/>
|
|
598
638
|
</div>
|
|
599
639
|
</>
|
|
600
640
|
)
|