@cdc/editor 1.4.2 → 1.4.4
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 +276 -78
- package/example/private/map.csv +60 -0
- package/example/private/test.html +1 -0
- package/example/valid-county-data.csv +3048 -0
- package/example/valid-county-data.json +3049 -0
- package/example/valid-data-map.csv +2 -2
- package/package.json +14 -9
- package/src/CdcEditor.js +26 -18
- package/src/components/ChooseTab.js +228 -79
- package/src/components/ConfigureTab.js +17 -3
- package/src/components/DataImport.js +403 -313
- package/src/components/PreviewDataTable.js +7 -4
- package/src/index.html +2 -2
- package/src/index.js +3 -1
- package/src/scss/_variables.scss +1 -1
- package/src/scss/choose-vis-tab.scss +1 -0
- package/src/scss/main.scss +17 -2
- package/LICENSE +0 -201
|
@@ -1,23 +1,31 @@
|
|
|
1
|
-
import React, { useState, useContext, useEffect } from 'react'
|
|
2
|
-
import {useDropzone} from 'react-dropzone'
|
|
3
|
-
import {csvParse} from 'd3'
|
|
4
|
-
import { useDebounce } from 'use-debounce'
|
|
5
|
-
import { get } from 'axios'
|
|
1
|
+
import React, { useState, useContext, useEffect } from 'react'
|
|
2
|
+
import { useDropzone } from 'react-dropzone'
|
|
3
|
+
import { csvParse } from 'd3'
|
|
4
|
+
import { useDebounce } from 'use-debounce'
|
|
5
|
+
import { get } from 'axios'
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import '
|
|
9
|
-
import
|
|
10
|
-
import Tabs from './Tabs';
|
|
11
|
-
import PreviewDataTable from './PreviewDataTable';
|
|
7
|
+
import { DataTransform } from '@cdc/core/components/DataTransform'
|
|
8
|
+
import Modal from '@cdc/core/components/ui/Modal'
|
|
9
|
+
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
12
10
|
|
|
13
|
-
import
|
|
14
|
-
import FileUploadIcon from '../assets/icons/file-upload-solid.svg';
|
|
15
|
-
import CloseIcon from '@cdc/core/assets/icon-close.svg';
|
|
11
|
+
import GlobalState from '../context'
|
|
16
12
|
|
|
17
|
-
import
|
|
18
|
-
import
|
|
13
|
+
import TabPane from './TabPane'
|
|
14
|
+
import Tabs from './Tabs'
|
|
15
|
+
import PreviewDataTable from './PreviewDataTable'
|
|
16
|
+
import LinkIcon from '../assets/icons/link.svg'
|
|
19
17
|
|
|
20
|
-
import
|
|
18
|
+
import FileUploadIcon from '../assets/icons/file-upload-solid.svg'
|
|
19
|
+
import CloseIcon from '@cdc/core/assets/icon-close.svg'
|
|
20
|
+
import validMapData from '../../example/valid-data-map.csv'
|
|
21
|
+
|
|
22
|
+
import validChartData from '../../example/valid-data-chart.csv'
|
|
23
|
+
import validCountyMapData from '../../example/valid-county-data.csv'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
import '../scss/data-import.scss'
|
|
27
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
28
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
21
29
|
|
|
22
30
|
export default function DataImport() {
|
|
23
31
|
const {
|
|
@@ -29,275 +37,347 @@ export default function DataImport() {
|
|
|
29
37
|
maxFileSize,
|
|
30
38
|
setGlobalActive,
|
|
31
39
|
tempConfig,
|
|
32
|
-
setTempConfig
|
|
33
|
-
|
|
40
|
+
setTempConfig,
|
|
41
|
+
sharepath
|
|
42
|
+
} = useContext(GlobalState)
|
|
34
43
|
|
|
35
|
-
const
|
|
44
|
+
const { overlay } = useGlobalContext()
|
|
36
45
|
|
|
37
|
-
const
|
|
46
|
+
const transform = new DataTransform()
|
|
38
47
|
|
|
39
|
-
const [
|
|
48
|
+
const [ externalURL, setExternalURL ] = useState(config.dataFileSourceType === 'url' ? config.dataFileName : (config.dataUrl || ''))
|
|
40
49
|
|
|
41
|
-
const [
|
|
50
|
+
const [ debouncedExternalURL ] = useDebounce(externalURL, 200)
|
|
51
|
+
|
|
52
|
+
const [ keepURL, setKeepURL ] = useState(!!config.dataUrl)
|
|
42
53
|
|
|
43
54
|
const supportedDataTypes = {
|
|
44
55
|
'.csv': 'text/csv',
|
|
45
56
|
'.json': 'application/json'
|
|
46
|
-
}
|
|
57
|
+
}
|
|
47
58
|
|
|
48
59
|
useEffect(() => {
|
|
49
|
-
if(
|
|
50
|
-
setConfig({...config, dataUrl: debouncedExternalURL})
|
|
60
|
+
if (false !== keepURL) {
|
|
61
|
+
setConfig({ ...config, dataUrl: debouncedExternalURL || externalURL })
|
|
62
|
+
} else {
|
|
63
|
+
let newConfig = {...config};
|
|
64
|
+
delete newConfig.dataUrl;
|
|
65
|
+
setConfig(newConfig);
|
|
51
66
|
}
|
|
52
|
-
}, [debouncedExternalURL, keepURL])
|
|
67
|
+
}, [ debouncedExternalURL, keepURL ])
|
|
53
68
|
|
|
54
69
|
/**
|
|
55
70
|
* Check to see all series for the viz exists in the new dataset
|
|
56
71
|
*/
|
|
57
72
|
const dataExists = (newData, oldSeries, oldAxisX) => {
|
|
58
|
-
|
|
73
|
+
|
|
59
74
|
// Loop through old series to make sure each exists in the new data
|
|
60
|
-
oldSeries.map(function(currentValue, index, newData){
|
|
61
|
-
if(
|
|
62
|
-
return false
|
|
75
|
+
oldSeries.map(function (currentValue, index, newData) {
|
|
76
|
+
if (!newData.find(element => element.dataKey === currentValue.dataKey))
|
|
77
|
+
return false
|
|
63
78
|
})
|
|
64
79
|
|
|
65
80
|
// Is the X Axis still in the dataset?
|
|
66
|
-
if(
|
|
67
|
-
return false
|
|
81
|
+
if (newData.columns.indexOf(oldAxisX) < 0)
|
|
82
|
+
return false
|
|
68
83
|
|
|
69
|
-
return true
|
|
84
|
+
return true
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
const loadExternal = async () => {
|
|
73
|
-
let dataURL = ''
|
|
88
|
+
let dataURL = ''
|
|
74
89
|
// Is URL valid?
|
|
75
90
|
try {
|
|
76
|
-
dataURL = new URL(externalURL)
|
|
91
|
+
dataURL = new URL(externalURL)
|
|
77
92
|
} catch {
|
|
78
93
|
throw errorMessages.urlInvalid
|
|
79
94
|
}
|
|
80
|
-
let responseBlob = null
|
|
95
|
+
let responseBlob = null
|
|
81
96
|
|
|
82
97
|
const fileExtension = Object.keys(supportedDataTypes).find(extension => dataURL.pathname.endsWith(extension))
|
|
83
98
|
|
|
84
99
|
try {
|
|
85
|
-
const response = await get(
|
|
100
|
+
const response = await get(dataURL,
|
|
86
101
|
{
|
|
87
102
|
responseType: 'blob'
|
|
88
103
|
})
|
|
89
104
|
.then((response) => {
|
|
90
|
-
responseBlob = response.data
|
|
105
|
+
responseBlob = response.data
|
|
91
106
|
|
|
92
107
|
// Sometimes the files are coming in as plain text types... Maybe when saved from Macs
|
|
93
|
-
if (
|
|
94
|
-
responseBlob = responseBlob.slice(0, responseBlob.size,
|
|
95
|
-
} else if (
|
|
96
|
-
responseBlob = responseBlob.slice(0, responseBlob.size,
|
|
108
|
+
if (fileExtension === '.csv' && responseBlob.type === 'text/plain') {
|
|
109
|
+
responseBlob = responseBlob.slice(0, responseBlob.size, 'text/csv')
|
|
110
|
+
} else if (fileExtension === '.json' && responseBlob.type === 'text/plain') {
|
|
111
|
+
responseBlob = responseBlob.slice(0, responseBlob.size, 'application/json')
|
|
97
112
|
}
|
|
98
|
-
})
|
|
113
|
+
})
|
|
99
114
|
} catch (err) {
|
|
100
115
|
console.error(err)
|
|
101
116
|
|
|
102
|
-
const error = err.toString()
|
|
117
|
+
const error = err.toString()
|
|
103
118
|
|
|
104
|
-
if(
|
|
119
|
+
if (Object.values(errorMessages).includes(err)) {
|
|
105
120
|
throw error
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
throw errorMessages.failedFetch
|
|
109
124
|
}
|
|
110
|
-
|
|
111
|
-
return responseBlob
|
|
125
|
+
|
|
126
|
+
return responseBlob
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
const onDrop = ([uploadedFile]) => loadData(uploadedFile)
|
|
129
|
+
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile)
|
|
115
130
|
|
|
116
131
|
/**
|
|
117
132
|
* Handle loading data
|
|
118
133
|
*/
|
|
119
134
|
const loadData = async (fileBlob = null, fileName) => {
|
|
120
|
-
let fileData = fileBlob
|
|
121
|
-
let fileSource = fileData?.path ?? fileName ?? null
|
|
122
|
-
let fileSourceType = 'file'
|
|
135
|
+
let fileData = fileBlob
|
|
136
|
+
let fileSource = fileData?.path ?? fileName ?? null
|
|
137
|
+
let fileSourceType = 'file'
|
|
123
138
|
|
|
124
139
|
// Get the raw data as text from the file
|
|
125
|
-
if(null === fileData) {
|
|
126
|
-
|
|
140
|
+
if (null === fileData) {
|
|
141
|
+
// const round = 1000 * 60 * 15;
|
|
142
|
+
// const date = new Date();
|
|
143
|
+
// const rounded = new Date(date.getTime() - (date.getTime() % round));
|
|
144
|
+
// const trimmedDate = rounded.toString().replace(/\s+/g, "");
|
|
145
|
+
|
|
146
|
+
const newUrl = new URL(fileName);
|
|
147
|
+
// newUrl.searchParams.append("v", trimmedDate);
|
|
148
|
+
|
|
149
|
+
fileSourceType = "url";
|
|
127
150
|
try {
|
|
128
|
-
fileData = await loadExternal()
|
|
129
|
-
fileSource = externalURL
|
|
151
|
+
fileData = await loadExternal()
|
|
152
|
+
fileSource = externalURL
|
|
130
153
|
} catch (error) {
|
|
131
|
-
setErrors([error])
|
|
132
|
-
return
|
|
154
|
+
setErrors([ error ])
|
|
155
|
+
return
|
|
133
156
|
}
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
// Check if file is too big
|
|
137
|
-
if(fileData.size > (maxFileSize * 1048576)
|
|
138
|
-
setErrors([errorMessages.fileTooLarge])
|
|
139
|
-
return
|
|
160
|
+
if (fileData.size > (maxFileSize * 1048576)) {
|
|
161
|
+
setErrors([ errorMessages.fileTooLarge ])
|
|
162
|
+
return
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
let path = fileBlob?.name || externalURL || fileName
|
|
143
|
-
let fileExtension = path.match(/(?:\.([^.]+))?$/g)
|
|
166
|
+
let fileExtension = path.match(/(?:\.([^.]+))?$/g)
|
|
144
167
|
|
|
145
|
-
if(fileExtension.length === 0) {
|
|
168
|
+
if (fileExtension.length === 0) {
|
|
146
169
|
fileExtension = '.csv'
|
|
147
170
|
} else {
|
|
148
171
|
fileExtension = fileExtension[0]
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
let mimeType = supportedDataTypes[fileExtension]
|
|
174
|
+
let mimeType = supportedDataTypes[fileExtension]
|
|
152
175
|
|
|
153
176
|
// Convert from blob into raw text
|
|
154
177
|
// Have to use FileReader instead of just .text because IE11 and the polyfills for this are bugged
|
|
155
|
-
let filereader = new FileReader()
|
|
178
|
+
let filereader = new FileReader()
|
|
156
179
|
|
|
157
180
|
// Set encoding for CSV files - needed to render special characters properly
|
|
158
|
-
let encoding = (
|
|
181
|
+
let encoding = (mimeType === 'text/csv') ? 'ISO-8859-1' : ''
|
|
159
182
|
|
|
160
|
-
filereader.onload = function() {
|
|
183
|
+
filereader.onload = function () {
|
|
161
184
|
let text = this.result
|
|
162
185
|
|
|
163
186
|
switch (mimeType) {
|
|
164
|
-
case 'text/csv':
|
|
165
|
-
text = csvParse(text)
|
|
166
|
-
break
|
|
187
|
+
case 'text/csv':
|
|
188
|
+
text = csvParse(text)
|
|
189
|
+
break
|
|
167
190
|
case 'text/plain':
|
|
168
191
|
case 'application/json':
|
|
169
192
|
try {
|
|
170
|
-
text = JSON.parse(text)
|
|
171
|
-
} catch (errors) {
|
|
172
|
-
setErrors([errorMessages.formatting])
|
|
173
|
-
return
|
|
193
|
+
text = JSON.parse(text)
|
|
194
|
+
} catch (errors) {
|
|
195
|
+
setErrors([ errorMessages.formatting ])
|
|
196
|
+
return
|
|
174
197
|
}
|
|
175
|
-
break
|
|
198
|
+
break
|
|
176
199
|
default:
|
|
177
|
-
setErrors([errorMessages.fileType])
|
|
178
|
-
return
|
|
200
|
+
setErrors([ errorMessages.fileType ])
|
|
201
|
+
return
|
|
179
202
|
}
|
|
180
|
-
|
|
203
|
+
|
|
181
204
|
// Validate parsed data and set if no issues.
|
|
182
205
|
try {
|
|
183
|
-
text = transform.autoStandardize(text)
|
|
206
|
+
text = transform.autoStandardize(text)
|
|
184
207
|
|
|
185
208
|
if (config.data && config.series) {
|
|
186
209
|
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
187
210
|
setConfig({
|
|
188
|
-
...config,
|
|
189
|
-
...tempConfig,
|
|
190
|
-
data:text, // new data
|
|
191
|
-
dataFileName:fileSource, // new file source
|
|
192
|
-
dataFileSourceType:fileSourceType
|
|
211
|
+
...config,
|
|
212
|
+
...tempConfig,
|
|
213
|
+
data: text, // new data
|
|
214
|
+
dataFileName: fileSource, // new file source
|
|
215
|
+
dataFileSourceType: fileSourceType,// new file source type
|
|
193
216
|
})
|
|
194
217
|
} else {
|
|
195
|
-
resetEditor({
|
|
218
|
+
resetEditor({
|
|
219
|
+
data: text,
|
|
220
|
+
dataFileName: fileSource,
|
|
221
|
+
dataFileSourceType: fileSourceType
|
|
222
|
+
}, '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?')
|
|
196
223
|
}
|
|
197
224
|
} else {
|
|
198
|
-
|
|
225
|
+
setConfig({ ...config, data: text, dataFileName: fileSource, dataFileSourceType: fileSourceType })
|
|
199
226
|
}
|
|
200
227
|
} catch (err) {
|
|
201
|
-
|
|
228
|
+
setErrors(err)
|
|
202
229
|
}
|
|
203
230
|
|
|
204
231
|
}
|
|
205
|
-
|
|
232
|
+
filereader.readAsText(fileData, encoding)
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
useEffect(() => {
|
|
209
|
-
let newConfig = {...config}
|
|
210
|
-
if(tempConfig !== null) {
|
|
211
|
-
newConfig = {...tempConfig}
|
|
212
|
-
|
|
236
|
+
let newConfig = { ...config }
|
|
237
|
+
if (tempConfig !== null) {
|
|
238
|
+
newConfig = { ...tempConfig }
|
|
213
239
|
}
|
|
214
240
|
|
|
215
|
-
if(undefined === config.formattedData && config.dataDescription) {
|
|
241
|
+
if (undefined === config.formattedData && config.dataDescription) {
|
|
216
242
|
const formattedData = transform.developerStandardize(config.data, config.dataDescription)
|
|
217
|
-
|
|
218
|
-
if(formattedData) newConfig.formattedData = formattedData
|
|
243
|
+
|
|
244
|
+
if (formattedData) newConfig.formattedData = formattedData
|
|
219
245
|
}
|
|
220
246
|
|
|
221
|
-
if(tempConfig !== null) setTempConfig(null)
|
|
247
|
+
if (tempConfig !== null) setTempConfig(null)
|
|
222
248
|
|
|
223
249
|
setConfig(newConfig)
|
|
224
|
-
}, [])
|
|
250
|
+
}, [])
|
|
225
251
|
|
|
226
252
|
const updateDescriptionProp = (key, value) => {
|
|
227
|
-
let dataDescription = {...config.dataDescription, [key]: value}
|
|
253
|
+
let dataDescription = { ...config.dataDescription, [key]: value }
|
|
228
254
|
let formattedData = transform.developerStandardize(config.data, dataDescription)
|
|
229
255
|
|
|
230
|
-
setConfig({...config, formattedData, dataDescription})
|
|
231
|
-
}
|
|
256
|
+
setConfig({ ...config, formattedData, dataDescription })
|
|
257
|
+
}
|
|
232
258
|
|
|
233
|
-
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})
|
|
259
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
|
|
234
260
|
|
|
235
261
|
const loadFileFromUrl = (url) => {
|
|
236
|
-
const extUrl = (url) ? url : config.dataFileName
|
|
262
|
+
// const extUrl = (url) ? url : config.dataFileName // set url to what is saved in config unless the user has entered something
|
|
263
|
+
|
|
237
264
|
return (
|
|
238
265
|
<>
|
|
239
266
|
<form className="input-group d-flex" onSubmit={(e) => e.preventDefault()}>
|
|
240
|
-
<input id="external-data" type="text" className="form-control flex-grow-1 border-right-0"
|
|
241
|
-
|
|
267
|
+
<input id="external-data" type="text" className="form-control flex-grow-1 border-right-0"
|
|
268
|
+
placeholder="e.g., https://data.cdc.gov/resources/file.json" aria-label="Load data from external URL"
|
|
269
|
+
aria-describedby="load-data" value={externalURL} onChange={(e) => setExternalURL(e.target.value)}/>
|
|
270
|
+
<button className="input-group-text btn btn-primary px-4" type="submit" id="load-data"
|
|
271
|
+
onClick={() => loadData(null, externalURL)}>Load
|
|
272
|
+
</button>
|
|
242
273
|
</form>
|
|
243
274
|
<label htmlFor="keep-url" className="mt-1 d-flex keep-url">
|
|
244
|
-
<input type="checkbox" id="keep-url"
|
|
275
|
+
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() => setKeepURL(!keepURL)}/> Always
|
|
276
|
+
load from URL (normally will only pull once)
|
|
245
277
|
</label>
|
|
246
278
|
</>
|
|
247
279
|
)
|
|
248
280
|
}
|
|
249
281
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
const resetEditor = (config = {}, message = 'Are you sure you want to do this?') => {
|
|
315
|
+
config.newViz = true
|
|
316
|
+
setTempConfig(null)
|
|
317
|
+
setConfig(config)
|
|
258
318
|
}
|
|
259
319
|
|
|
260
320
|
const resetButton = () => {
|
|
261
|
-
return (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
321
|
+
return (
|
|
322
|
+
<Button className="warn" style={{ height: '2.5rem', display: 'inline-flex', justifyContent: 'center', alignItems: 'center' }}
|
|
323
|
+
onClick={() => overlay.actions.openOverlay(warningModal(), true)}>
|
|
324
|
+
Clear <Icon display="close" style={{marginLeft: '0.5rem'}}/>
|
|
325
|
+
</Button>
|
|
265
326
|
)
|
|
266
327
|
}
|
|
267
328
|
|
|
268
329
|
return (
|
|
269
330
|
<>
|
|
270
331
|
<div className="left-col">
|
|
271
|
-
{
|
|
332
|
+
{(!config.data || !config.dataFileSourceType) && ( // dataFileSourceType needs to be checked here since earlier versions did not track this state
|
|
272
333
|
<div className="load-data-area">
|
|
273
334
|
<Tabs>
|
|
274
|
-
<TabPane title="Upload File" icon={<FileUploadIcon className="inline-icon"
|
|
275
|
-
|
|
335
|
+
<TabPane title="Upload File" icon={<FileUploadIcon className="inline-icon"/>}>
|
|
336
|
+
{sharepath &&
|
|
337
|
+
<p className="alert--info">
|
|
338
|
+
The share path set for this website is: {sharepath}
|
|
339
|
+
</p>
|
|
340
|
+
}
|
|
341
|
+
<div
|
|
342
|
+
className={isDragActive ? 'drag-active cdcdataviz-file-selector' : 'cdcdataviz-file-selector'} {...getRootProps()}>
|
|
276
343
|
<input {...getInputProps()} />
|
|
277
344
|
{
|
|
278
345
|
isDragActive ?
|
|
279
|
-
|
|
280
|
-
|
|
346
|
+
<p>Drop file here</p> :
|
|
347
|
+
<p>Drag file to this area, or <span>select a file</span>.</p>
|
|
281
348
|
}
|
|
282
349
|
</div>
|
|
283
350
|
</TabPane>
|
|
284
|
-
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"
|
|
351
|
+
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"/>}>
|
|
285
352
|
{loadFileFromUrl(externalURL)}
|
|
286
353
|
</TabPane>
|
|
287
354
|
</Tabs>
|
|
288
355
|
{errors && (errors.map ? errors.map((message, index) => (
|
|
289
356
|
<div className="error-box slim mt-2" key={`error-${message}`}>
|
|
290
|
-
<span>{message}</span> <CloseIcon className=
|
|
357
|
+
<span>{message}</span> <CloseIcon className="inline-icon dismiss-error"
|
|
358
|
+
onClick={() => setErrors(errors.filter((val, i) => i !== index))}/>
|
|
291
359
|
</div>
|
|
292
360
|
)) : errors.message)}
|
|
293
|
-
<p className="footnote">Supported file types: {Object.keys(supportedDataTypes).join(', ')}. Maximum file
|
|
361
|
+
<p className="footnote">Supported file types: {Object.keys(supportedDataTypes).join(', ')}. Maximum file
|
|
362
|
+
size {maxFileSize}MB.</p>
|
|
294
363
|
{/* TODO: Add more sample data in, but this will do for now. */}
|
|
295
364
|
<span className="heading-3">Load Sample Data:</span>
|
|
296
365
|
<ul className="sample-data-list">
|
|
297
|
-
<li
|
|
298
|
-
|
|
366
|
+
<li
|
|
367
|
+
onClick={() => loadData(new Blob([ validMapData ], { type: 'text/csv' }), 'valid-data-map.csv')}>United
|
|
368
|
+
States Sample Data #1
|
|
369
|
+
</li>
|
|
370
|
+
<li
|
|
371
|
+
onClick={() => loadData(new Blob([ validChartData ], { type: 'text/csv' }), 'valid-data-chart.csv')}>Chart
|
|
372
|
+
Sample Data
|
|
373
|
+
</li>
|
|
374
|
+
<li
|
|
375
|
+
onClick={() => loadData(new Blob([ validCountyMapData ], { type: 'text/csv' }), 'valid-county-data.csv')}>United
|
|
376
|
+
States Counties Sample Data
|
|
377
|
+
</li>
|
|
299
378
|
</ul>
|
|
300
|
-
<a href="https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html" target="_blank"
|
|
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">
|
|
301
381
|
<div>
|
|
302
382
|
<h3>Get Help</h3>
|
|
303
383
|
<p>Documentation and examples on formatting data and configuring visualizations.</p>
|
|
@@ -310,82 +390,91 @@ export default function DataImport() {
|
|
|
310
390
|
<div>
|
|
311
391
|
<div className="heading-3">Data Source</div>
|
|
312
392
|
<div className="file-loaded-area">
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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 ?
|
|
319
400
|
<p>Drop file here</p> :
|
|
320
|
-
<p><FileUploadIcon
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
401
|
+
<p><FileUploadIcon/> <span>{config.dataFileName ?? 'Replace data file'}</span></p>
|
|
402
|
+
}
|
|
403
|
+
</div>
|
|
404
|
+
<div>
|
|
324
405
|
{resetButton()}
|
|
325
|
-
</div>
|
|
326
406
|
</div>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<div>
|
|
335
|
-
{resetButton()}
|
|
336
|
-
</div>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{config.dataFileSourceType === 'url' && (
|
|
411
|
+
<div className="url-source-options">
|
|
412
|
+
<div>
|
|
413
|
+
{loadFileFromUrl(externalURL)}
|
|
337
414
|
</div>
|
|
338
|
-
|
|
415
|
+
<div>
|
|
416
|
+
{resetButton()}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
339
420
|
</div>
|
|
340
421
|
<div className="question">
|
|
341
422
|
<div className="heading-3">Describe Data</div>
|
|
342
423
|
<div className="heading-4 data-question">Data Orientation</div>
|
|
343
424
|
<div className="table-button-container">
|
|
344
|
-
<div
|
|
425
|
+
<div
|
|
426
|
+
className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === false ? ' active' : '')}
|
|
427
|
+
onClick={() => {
|
|
428
|
+
updateDescriptionProp('horizontal', false)
|
|
429
|
+
}}>
|
|
345
430
|
<strong>Vertical</strong>
|
|
346
|
-
<p>
|
|
431
|
+
<p>Values for map geography or chart date/category axis are contained in a single <em>column</em>.</p>
|
|
347
432
|
<table>
|
|
348
433
|
<tbody>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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>
|
|
364
449
|
</tbody>
|
|
365
450
|
</table>
|
|
366
451
|
<table>
|
|
367
452
|
<tbody>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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>
|
|
383
468
|
</tbody>
|
|
384
469
|
</table>
|
|
385
470
|
</div>
|
|
386
|
-
<div
|
|
471
|
+
<div
|
|
472
|
+
className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === true ? ' active' : '')}
|
|
473
|
+
onClick={() => {
|
|
474
|
+
updateDescriptionProp('horizontal', true)
|
|
475
|
+
}}>
|
|
387
476
|
<strong>Horizontal</strong>
|
|
388
|
-
<p>
|
|
477
|
+
<p>Values for map geography or chart date/category axis are contained in a single <em>row</em></p>
|
|
389
478
|
<table>
|
|
390
479
|
<tbody>
|
|
391
480
|
<tr>
|
|
@@ -404,153 +493,154 @@ export default function DataImport() {
|
|
|
404
493
|
</table>
|
|
405
494
|
<table>
|
|
406
495
|
<tbody>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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>
|
|
419
508
|
</tbody>
|
|
420
509
|
</table>
|
|
421
510
|
</div>
|
|
422
511
|
</div>
|
|
423
512
|
</div>
|
|
424
513
|
{config.dataDescription && (
|
|
425
|
-
|
|
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
|
+
<>
|
|
426
533
|
<div className="question">
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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>
|
|
431
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>
|
|
432
604
|
</div>
|
|
433
|
-
{config.dataDescription.
|
|
434
|
-
|
|
435
|
-
<div className="
|
|
436
|
-
|
|
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)}}>
|
|
437
610
|
<option value="">Choose an option</option>
|
|
438
|
-
{Object.keys(config.data[0]).map(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
<
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
</tr>
|
|
457
|
-
<tr>
|
|
458
|
-
<td>Virus 1</td>
|
|
459
|
-
<td>100</td>
|
|
460
|
-
<td>150</td>
|
|
461
|
-
<td>...</td>
|
|
462
|
-
</tr>
|
|
463
|
-
<tr>
|
|
464
|
-
<td>Virus 2</td>
|
|
465
|
-
<td>15</td>
|
|
466
|
-
<td>20</td>
|
|
467
|
-
<td>...</td>
|
|
468
|
-
</tr>
|
|
469
|
-
</tbody>
|
|
470
|
-
</table>
|
|
471
|
-
</div>
|
|
472
|
-
<div className={'table-button' + (config.dataDescription.singleRow === false ? ' active' : '')} onClick={() => {updateDescriptionProp('singleRow', false)}}>
|
|
473
|
-
<p>Each series data is broken out into multiple rows.</p>
|
|
474
|
-
<table>
|
|
475
|
-
<tbody>
|
|
476
|
-
<tr>
|
|
477
|
-
<th>Virus</th>
|
|
478
|
-
<th>Date</th>
|
|
479
|
-
<th>Value</th>
|
|
480
|
-
</tr>
|
|
481
|
-
<tr>
|
|
482
|
-
<td>Virus 1</td>
|
|
483
|
-
<td>01/01/2020</td>
|
|
484
|
-
<td>100</td>
|
|
485
|
-
</tr>
|
|
486
|
-
<tr>
|
|
487
|
-
<td>Virus 1</td>
|
|
488
|
-
<td>02/01/2020</td>
|
|
489
|
-
<td>150</td>
|
|
490
|
-
</tr>
|
|
491
|
-
<tr>
|
|
492
|
-
<td>...</td>
|
|
493
|
-
<td>...</td>
|
|
494
|
-
<td>...</td>
|
|
495
|
-
</tr>
|
|
496
|
-
<tr>
|
|
497
|
-
<td>Virus 2</td>
|
|
498
|
-
<td>01/01/2020</td>
|
|
499
|
-
<td>15</td>
|
|
500
|
-
</tr>
|
|
501
|
-
<tr>
|
|
502
|
-
<td>Virus 2</td>
|
|
503
|
-
<td>02/01/2020</td>
|
|
504
|
-
<td>20</td>
|
|
505
|
-
</tr>
|
|
506
|
-
<tr>
|
|
507
|
-
<td>...</td>
|
|
508
|
-
<td>...</td>
|
|
509
|
-
<td>...</td>
|
|
510
|
-
</tr>
|
|
511
|
-
</tbody>
|
|
512
|
-
</table>
|
|
513
|
-
</div>
|
|
514
|
-
</div>
|
|
515
|
-
</div>
|
|
516
|
-
{config.dataDescription.singleRow === false && (
|
|
517
|
-
<>
|
|
518
|
-
<div className="question">
|
|
519
|
-
<div className="heading-4 data-question">Which property in the dataset represents which series the row is describing?</div>
|
|
520
|
-
<select onChange={(e) => {updateDescriptionProp('seriesKey', e.target.value)}}>
|
|
521
|
-
<option value="">Choose an option</option>
|
|
522
|
-
{Object.keys(config.data[0]).map(key => <option value={key}>{key}</option>)}
|
|
523
|
-
</select>
|
|
524
|
-
</div>
|
|
525
|
-
<div className="question">
|
|
526
|
-
<div className="heading-4 data-question">Which property in the dataset represents the X-axis, or geography value?</div>
|
|
527
|
-
<select onChange={(e) => {updateDescriptionProp('xKey', e.target.value)}}>
|
|
528
|
-
<option value="">Choose an option</option>
|
|
529
|
-
{Object.keys(config.data[0]).map(key => <option value={key}>{key}</option>)}
|
|
530
|
-
</select>
|
|
531
|
-
</div>
|
|
532
|
-
<div className="question">
|
|
533
|
-
<div className="heading-4 data-question">Which property in the dataset represents the numeric value?</div>
|
|
534
|
-
<select onChange={(e) => {updateDescriptionProp('valueKey', e.target.value)}}>
|
|
535
|
-
<option value="">Choose an option</option>
|
|
536
|
-
{Object.keys(config.data[0]).map(key => <option value={key}>{key}</option>)}
|
|
537
|
-
</select>
|
|
538
|
-
</div>
|
|
539
|
-
</>
|
|
540
|
-
)}
|
|
541
|
-
</>
|
|
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
|
+
</>
|
|
542
629
|
)}
|
|
543
|
-
|
|
630
|
+
</>
|
|
631
|
+
)}
|
|
632
|
+
</>
|
|
544
633
|
)}
|
|
545
634
|
{config.formattedData && (
|
|
546
|
-
<button className="btn btn-primary" style={{float: 'right', marginBottom: '2em'}}
|
|
635
|
+
<button className="btn btn-primary" style={{ float: 'right', marginBottom: '2em' }}
|
|
636
|
+
onClick={() => setGlobalActive(1)}>Select your visualization type »</button>
|
|
547
637
|
)}
|
|
548
|
-
|
|
638
|
+
</div>
|
|
549
639
|
)}
|
|
550
640
|
</div>
|
|
551
641
|
<div className="right-col">
|
|
552
|
-
<PreviewDataTable data={config.data}
|
|
642
|
+
<PreviewDataTable data={config.data}/>
|
|
553
643
|
</div>
|
|
554
644
|
</>
|
|
555
|
-
)
|
|
645
|
+
)
|
|
556
646
|
}
|