@cdc/editor 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdceditor.js +276 -78
- package/example/valid-county-data.csv +3048 -0
- package/example/valid-county-data.json +3049 -0
- package/package.json +9 -4
- package/src/CdcEditor.js +5 -4
- package/src/components/ChooseTab.js +13 -4
- package/src/components/ConfigureTab.js +17 -3
- package/src/components/DataImport.js +355 -310
- 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 +10 -0
- package/LICENSE +0 -201
|
@@ -1,23 +1,24 @@
|
|
|
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 GlobalState from '../context'
|
|
8
|
-
import '../scss/data-import.scss'
|
|
9
|
-
import TabPane from './TabPane'
|
|
10
|
-
import Tabs from './Tabs'
|
|
11
|
-
import PreviewDataTable from './PreviewDataTable'
|
|
7
|
+
import GlobalState from '../context'
|
|
8
|
+
import '../scss/data-import.scss'
|
|
9
|
+
import TabPane from './TabPane'
|
|
10
|
+
import Tabs from './Tabs'
|
|
11
|
+
import PreviewDataTable from './PreviewDataTable'
|
|
12
12
|
|
|
13
|
-
import LinkIcon from '../assets/icons/link.svg'
|
|
14
|
-
import FileUploadIcon from '../assets/icons/file-upload-solid.svg'
|
|
15
|
-
import CloseIcon from '@cdc/core/assets/icon-close.svg'
|
|
13
|
+
import LinkIcon from '../assets/icons/link.svg'
|
|
14
|
+
import FileUploadIcon from '../assets/icons/file-upload-solid.svg'
|
|
15
|
+
import CloseIcon from '@cdc/core/assets/icon-close.svg'
|
|
16
16
|
|
|
17
|
-
import validMapData from '../../example/valid-data-map.csv'
|
|
18
|
-
import validChartData from '../../example/valid-data-chart.csv'
|
|
17
|
+
import validMapData from '../../example/valid-data-map.csv'
|
|
18
|
+
import validChartData from '../../example/valid-data-chart.csv'
|
|
19
|
+
import validCountyMapData from '../../example/valid-county-data.csv'
|
|
19
20
|
|
|
20
|
-
import { DataTransform } from '@cdc/core/components/DataTransform'
|
|
21
|
+
import { DataTransform } from '@cdc/core/components/DataTransform'
|
|
21
22
|
|
|
22
23
|
export default function DataImport() {
|
|
23
24
|
const {
|
|
@@ -29,238 +30,253 @@ export default function DataImport() {
|
|
|
29
30
|
maxFileSize,
|
|
30
31
|
setGlobalActive,
|
|
31
32
|
tempConfig,
|
|
32
|
-
setTempConfig
|
|
33
|
-
|
|
33
|
+
setTempConfig,
|
|
34
|
+
sharepath
|
|
35
|
+
} = useContext(GlobalState)
|
|
34
36
|
|
|
35
|
-
const transform = new DataTransform()
|
|
37
|
+
const transform = new DataTransform()
|
|
36
38
|
|
|
37
|
-
const [externalURL, setExternalURL] = useState('')
|
|
39
|
+
const [ externalURL, setExternalURL ] = useState(config.dataFileSourceType === 'url' ? config.dataFileName : (config.dataUrl || ''))
|
|
38
40
|
|
|
39
|
-
const [ debouncedExternalURL ] = useDebounce(externalURL, 200)
|
|
41
|
+
const [ debouncedExternalURL ] = useDebounce(externalURL, 200)
|
|
40
42
|
|
|
41
|
-
const [keepURL, setKeepURL] = useState(config.dataUrl
|
|
43
|
+
const [ keepURL, setKeepURL ] = useState(!!config.dataUrl)
|
|
42
44
|
|
|
43
45
|
const supportedDataTypes = {
|
|
44
46
|
'.csv': 'text/csv',
|
|
45
47
|
'.json': 'application/json'
|
|
46
|
-
}
|
|
48
|
+
}
|
|
47
49
|
|
|
48
50
|
useEffect(() => {
|
|
49
|
-
if(
|
|
50
|
-
setConfig({...config, dataUrl: debouncedExternalURL})
|
|
51
|
+
if (false !== keepURL) {
|
|
52
|
+
setConfig({ ...config, dataUrl: debouncedExternalURL || externalURL })
|
|
53
|
+
} else {
|
|
54
|
+
let newConfig = {...config};
|
|
55
|
+
delete newConfig.dataUrl;
|
|
56
|
+
setConfig(newConfig);
|
|
51
57
|
}
|
|
52
|
-
}, [debouncedExternalURL, keepURL])
|
|
58
|
+
}, [ debouncedExternalURL, keepURL ])
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
61
|
* Check to see all series for the viz exists in the new dataset
|
|
56
62
|
*/
|
|
57
63
|
const dataExists = (newData, oldSeries, oldAxisX) => {
|
|
58
|
-
|
|
64
|
+
|
|
59
65
|
// 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
|
|
66
|
+
oldSeries.map(function (currentValue, index, newData) {
|
|
67
|
+
if (!newData.find(element => element.dataKey === currentValue.dataKey))
|
|
68
|
+
return false
|
|
63
69
|
})
|
|
64
70
|
|
|
65
71
|
// Is the X Axis still in the dataset?
|
|
66
|
-
if(
|
|
67
|
-
return false
|
|
72
|
+
if (newData.columns.indexOf(oldAxisX) < 0)
|
|
73
|
+
return false
|
|
68
74
|
|
|
69
|
-
return true
|
|
75
|
+
return true
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
const loadExternal = async () => {
|
|
73
|
-
let dataURL = ''
|
|
79
|
+
let dataURL = ''
|
|
74
80
|
// Is URL valid?
|
|
75
81
|
try {
|
|
76
|
-
dataURL = new URL(externalURL)
|
|
82
|
+
dataURL = new URL(externalURL)
|
|
77
83
|
} catch {
|
|
78
84
|
throw errorMessages.urlInvalid
|
|
79
85
|
}
|
|
80
|
-
let responseBlob = null
|
|
86
|
+
let responseBlob = null
|
|
81
87
|
|
|
82
88
|
const fileExtension = Object.keys(supportedDataTypes).find(extension => dataURL.pathname.endsWith(extension))
|
|
83
89
|
|
|
84
90
|
try {
|
|
85
|
-
const response = await get(
|
|
91
|
+
const response = await get(dataURL,
|
|
86
92
|
{
|
|
87
93
|
responseType: 'blob'
|
|
88
94
|
})
|
|
89
95
|
.then((response) => {
|
|
90
|
-
responseBlob = response.data
|
|
96
|
+
responseBlob = response.data
|
|
91
97
|
|
|
92
98
|
// 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,
|
|
99
|
+
if (fileExtension === '.csv' && responseBlob.type === 'text/plain') {
|
|
100
|
+
responseBlob = responseBlob.slice(0, responseBlob.size, 'text/csv')
|
|
101
|
+
} else if (fileExtension === '.json' && responseBlob.type === 'text/plain') {
|
|
102
|
+
responseBlob = responseBlob.slice(0, responseBlob.size, 'application/json')
|
|
97
103
|
}
|
|
98
|
-
})
|
|
104
|
+
})
|
|
99
105
|
} catch (err) {
|
|
100
106
|
console.error(err)
|
|
101
107
|
|
|
102
|
-
const error = err.toString()
|
|
108
|
+
const error = err.toString()
|
|
103
109
|
|
|
104
|
-
if(
|
|
110
|
+
if (Object.values(errorMessages).includes(err)) {
|
|
105
111
|
throw error
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
throw errorMessages.failedFetch
|
|
109
115
|
}
|
|
110
|
-
|
|
111
|
-
return responseBlob
|
|
116
|
+
|
|
117
|
+
return responseBlob
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
const onDrop = ([uploadedFile]) => loadData(uploadedFile)
|
|
120
|
+
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile)
|
|
115
121
|
|
|
116
122
|
/**
|
|
117
123
|
* Handle loading data
|
|
118
124
|
*/
|
|
119
125
|
const loadData = async (fileBlob = null, fileName) => {
|
|
120
|
-
let fileData = fileBlob
|
|
121
|
-
let fileSource = fileData?.path ?? fileName ?? null
|
|
122
|
-
let fileSourceType = 'file'
|
|
126
|
+
let fileData = fileBlob
|
|
127
|
+
let fileSource = fileData?.path ?? fileName ?? null
|
|
128
|
+
let fileSourceType = 'file'
|
|
123
129
|
|
|
124
130
|
// Get the raw data as text from the file
|
|
125
|
-
if(null === fileData) {
|
|
126
|
-
fileSourceType = 'url'
|
|
131
|
+
if (null === fileData) {
|
|
132
|
+
fileSourceType = 'url'
|
|
127
133
|
try {
|
|
128
|
-
fileData = await loadExternal()
|
|
129
|
-
fileSource = externalURL
|
|
134
|
+
fileData = await loadExternal()
|
|
135
|
+
fileSource = externalURL
|
|
130
136
|
} catch (error) {
|
|
131
|
-
setErrors([error])
|
|
132
|
-
return
|
|
137
|
+
setErrors([ error ])
|
|
138
|
+
return
|
|
133
139
|
}
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
// Check if file is too big
|
|
137
|
-
if(fileData.size > (maxFileSize * 1048576)
|
|
138
|
-
setErrors([errorMessages.fileTooLarge])
|
|
139
|
-
return
|
|
143
|
+
if (fileData.size > (maxFileSize * 1048576)) {
|
|
144
|
+
setErrors([ errorMessages.fileTooLarge ])
|
|
145
|
+
return
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
let path = fileBlob?.name || externalURL || fileName
|
|
143
|
-
let fileExtension = path.match(/(?:\.([^.]+))?$/g)
|
|
149
|
+
let fileExtension = path.match(/(?:\.([^.]+))?$/g)
|
|
144
150
|
|
|
145
|
-
if(fileExtension.length === 0) {
|
|
151
|
+
if (fileExtension.length === 0) {
|
|
146
152
|
fileExtension = '.csv'
|
|
147
153
|
} else {
|
|
148
154
|
fileExtension = fileExtension[0]
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
let mimeType = supportedDataTypes[fileExtension]
|
|
157
|
+
let mimeType = supportedDataTypes[fileExtension]
|
|
152
158
|
|
|
153
159
|
// Convert from blob into raw text
|
|
154
160
|
// Have to use FileReader instead of just .text because IE11 and the polyfills for this are bugged
|
|
155
|
-
let filereader = new FileReader()
|
|
161
|
+
let filereader = new FileReader()
|
|
156
162
|
|
|
157
163
|
// Set encoding for CSV files - needed to render special characters properly
|
|
158
|
-
let encoding = (
|
|
164
|
+
let encoding = (mimeType === 'text/csv') ? 'ISO-8859-1' : ''
|
|
159
165
|
|
|
160
|
-
filereader.onload = function() {
|
|
166
|
+
filereader.onload = function () {
|
|
161
167
|
let text = this.result
|
|
162
168
|
|
|
163
169
|
switch (mimeType) {
|
|
164
|
-
case 'text/csv':
|
|
165
|
-
text = csvParse(text)
|
|
166
|
-
break
|
|
170
|
+
case 'text/csv':
|
|
171
|
+
text = csvParse(text)
|
|
172
|
+
break
|
|
167
173
|
case 'text/plain':
|
|
168
174
|
case 'application/json':
|
|
169
175
|
try {
|
|
170
|
-
text = JSON.parse(text)
|
|
171
|
-
} catch (errors) {
|
|
172
|
-
setErrors([errorMessages.formatting])
|
|
173
|
-
return
|
|
176
|
+
text = JSON.parse(text)
|
|
177
|
+
} catch (errors) {
|
|
178
|
+
setErrors([ errorMessages.formatting ])
|
|
179
|
+
return
|
|
174
180
|
}
|
|
175
|
-
break
|
|
181
|
+
break
|
|
176
182
|
default:
|
|
177
|
-
setErrors([errorMessages.fileType])
|
|
178
|
-
return
|
|
183
|
+
setErrors([ errorMessages.fileType ])
|
|
184
|
+
return
|
|
179
185
|
}
|
|
180
|
-
|
|
186
|
+
|
|
181
187
|
// Validate parsed data and set if no issues.
|
|
182
188
|
try {
|
|
183
|
-
text = transform.autoStandardize(text)
|
|
189
|
+
text = transform.autoStandardize(text)
|
|
184
190
|
|
|
185
191
|
if (config.data && config.series) {
|
|
186
192
|
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
187
193
|
setConfig({
|
|
188
|
-
...config,
|
|
189
|
-
...tempConfig,
|
|
190
|
-
data:text, // new data
|
|
191
|
-
dataFileName:fileSource, // new file source
|
|
192
|
-
dataFileSourceType:fileSourceType
|
|
194
|
+
...config,
|
|
195
|
+
...tempConfig,
|
|
196
|
+
data: text, // new data
|
|
197
|
+
dataFileName: fileSource, // new file source
|
|
198
|
+
dataFileSourceType: fileSourceType,// new file source type
|
|
193
199
|
})
|
|
194
200
|
} else {
|
|
195
|
-
resetEditor({
|
|
201
|
+
resetEditor({
|
|
202
|
+
data: text,
|
|
203
|
+
dataFileName: fileSource,
|
|
204
|
+
dataFileSourceType: fileSourceType
|
|
205
|
+
}, '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
206
|
}
|
|
197
207
|
} else {
|
|
198
|
-
|
|
208
|
+
setConfig({ ...config, data: text, dataFileName: fileSource, dataFileSourceType: fileSourceType })
|
|
199
209
|
}
|
|
200
210
|
} catch (err) {
|
|
201
|
-
|
|
211
|
+
setErrors(err)
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
}
|
|
205
|
-
|
|
215
|
+
filereader.readAsText(fileData, encoding)
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
useEffect(() => {
|
|
209
|
-
let newConfig = {...config}
|
|
210
|
-
if(tempConfig !== null) {
|
|
211
|
-
newConfig = {...tempConfig}
|
|
212
|
-
|
|
219
|
+
let newConfig = { ...config }
|
|
220
|
+
if (tempConfig !== null) {
|
|
221
|
+
newConfig = { ...tempConfig }
|
|
213
222
|
}
|
|
214
223
|
|
|
215
|
-
if(undefined === config.formattedData && config.dataDescription) {
|
|
224
|
+
if (undefined === config.formattedData && config.dataDescription) {
|
|
216
225
|
const formattedData = transform.developerStandardize(config.data, config.dataDescription)
|
|
217
|
-
|
|
218
|
-
if(formattedData) newConfig.formattedData = formattedData
|
|
226
|
+
|
|
227
|
+
if (formattedData) newConfig.formattedData = formattedData
|
|
219
228
|
}
|
|
220
229
|
|
|
221
|
-
if(tempConfig !== null) setTempConfig(null)
|
|
230
|
+
if (tempConfig !== null) setTempConfig(null)
|
|
222
231
|
|
|
223
232
|
setConfig(newConfig)
|
|
224
|
-
}, [])
|
|
233
|
+
}, [])
|
|
225
234
|
|
|
226
235
|
const updateDescriptionProp = (key, value) => {
|
|
227
|
-
let dataDescription = {...config.dataDescription, [key]: value}
|
|
236
|
+
let dataDescription = { ...config.dataDescription, [key]: value }
|
|
228
237
|
let formattedData = transform.developerStandardize(config.data, dataDescription)
|
|
229
238
|
|
|
230
|
-
setConfig({...config, formattedData, dataDescription})
|
|
231
|
-
}
|
|
239
|
+
setConfig({ ...config, formattedData, dataDescription })
|
|
240
|
+
}
|
|
232
241
|
|
|
233
|
-
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop})
|
|
242
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
|
|
234
243
|
|
|
235
244
|
const loadFileFromUrl = (url) => {
|
|
236
|
-
const extUrl = (url) ? url : config.dataFileName
|
|
245
|
+
// const extUrl = (url) ? url : config.dataFileName // set url to what is saved in config unless the user has entered something
|
|
246
|
+
|
|
237
247
|
return (
|
|
238
248
|
<>
|
|
239
249
|
<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
|
-
|
|
250
|
+
<input id="external-data" type="text" className="form-control flex-grow-1 border-right-0"
|
|
251
|
+
placeholder="e.g., https://data.cdc.gov/resources/file.json" aria-label="Load data from external URL"
|
|
252
|
+
aria-describedby="load-data" value={externalURL} onChange={(e) => setExternalURL(e.target.value)}/>
|
|
253
|
+
<button className="input-group-text btn btn-primary px-4" type="submit" id="load-data"
|
|
254
|
+
onClick={() => loadData(null, externalURL)}>Load
|
|
255
|
+
</button>
|
|
242
256
|
</form>
|
|
243
257
|
<label htmlFor="keep-url" className="mt-1 d-flex keep-url">
|
|
244
|
-
<input type="checkbox" id="keep-url"
|
|
258
|
+
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() => setKeepURL(!keepURL)}/> Always
|
|
259
|
+
load from URL (normally will only pull once)
|
|
245
260
|
</label>
|
|
246
261
|
</>
|
|
247
262
|
)
|
|
248
263
|
}
|
|
249
264
|
|
|
250
|
-
const resetEditor = (
|
|
251
|
-
config.newViz = true
|
|
252
|
-
const confirmDataReset = window.confirm(message)
|
|
253
|
-
|
|
265
|
+
const resetEditor = (config = {}, message = 'Are you sure you want to do this?') => {
|
|
266
|
+
config.newViz = true
|
|
267
|
+
const confirmDataReset = window.confirm(message)
|
|
268
|
+
|
|
254
269
|
if (confirmDataReset === true) {
|
|
255
|
-
setTempConfig(null)
|
|
256
|
-
setConfig(config)
|
|
257
|
-
}
|
|
270
|
+
setTempConfig(null)
|
|
271
|
+
setConfig(config)
|
|
272
|
+
}
|
|
258
273
|
}
|
|
259
274
|
|
|
260
275
|
const resetButton = () => {
|
|
261
276
|
return ( //todo convert to modal
|
|
262
|
-
|
|
263
|
-
|
|
277
|
+
<button className="btn danger"
|
|
278
|
+
onClick={() => resetEditor({}, 'Reseting will remove your data and settings. Do you want to continue?')}>Clear
|
|
279
|
+
<CloseIcon/>
|
|
264
280
|
</button>
|
|
265
281
|
)
|
|
266
282
|
}
|
|
@@ -268,36 +284,55 @@ export default function DataImport() {
|
|
|
268
284
|
return (
|
|
269
285
|
<>
|
|
270
286
|
<div className="left-col">
|
|
271
|
-
{
|
|
287
|
+
{(!config.data || !config.dataFileSourceType) && ( // dataFileSourceType needs to be checked here since earlier versions did not track this state
|
|
272
288
|
<div className="load-data-area">
|
|
273
289
|
<Tabs>
|
|
274
|
-
<TabPane title="Upload File" icon={<FileUploadIcon className="inline-icon"
|
|
275
|
-
|
|
290
|
+
<TabPane title="Upload File" icon={<FileUploadIcon className="inline-icon"/>}>
|
|
291
|
+
{sharepath &&
|
|
292
|
+
<p className="alert--info">
|
|
293
|
+
The share path set for this website is: {sharepath}
|
|
294
|
+
</p>
|
|
295
|
+
}
|
|
296
|
+
<div
|
|
297
|
+
className={isDragActive ? 'drag-active cdcdataviz-file-selector' : 'cdcdataviz-file-selector'} {...getRootProps()}>
|
|
276
298
|
<input {...getInputProps()} />
|
|
277
299
|
{
|
|
278
300
|
isDragActive ?
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
<p>Drop file here</p> :
|
|
302
|
+
<p>Drag file to this area, or <span>select a file</span>.</p>
|
|
281
303
|
}
|
|
282
304
|
</div>
|
|
283
305
|
</TabPane>
|
|
284
|
-
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"
|
|
306
|
+
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"/>}>
|
|
285
307
|
{loadFileFromUrl(externalURL)}
|
|
286
308
|
</TabPane>
|
|
287
309
|
</Tabs>
|
|
288
310
|
{errors && (errors.map ? errors.map((message, index) => (
|
|
289
311
|
<div className="error-box slim mt-2" key={`error-${message}`}>
|
|
290
|
-
<span>{message}</span> <CloseIcon className=
|
|
312
|
+
<span>{message}</span> <CloseIcon className="inline-icon dismiss-error"
|
|
313
|
+
onClick={() => setErrors(errors.filter((val, i) => i !== index))}/>
|
|
291
314
|
</div>
|
|
292
315
|
)) : errors.message)}
|
|
293
|
-
<p className="footnote">Supported file types: {Object.keys(supportedDataTypes).join(', ')}. Maximum file
|
|
316
|
+
<p className="footnote">Supported file types: {Object.keys(supportedDataTypes).join(', ')}. Maximum file
|
|
317
|
+
size {maxFileSize}MB.</p>
|
|
294
318
|
{/* TODO: Add more sample data in, but this will do for now. */}
|
|
295
319
|
<span className="heading-3">Load Sample Data:</span>
|
|
296
320
|
<ul className="sample-data-list">
|
|
297
|
-
<li
|
|
298
|
-
|
|
321
|
+
<li
|
|
322
|
+
onClick={() => loadData(new Blob([ validMapData ], { type: 'text/csv' }), 'valid-data-map.csv')}>United
|
|
323
|
+
States Sample Data #1
|
|
324
|
+
</li>
|
|
325
|
+
<li
|
|
326
|
+
onClick={() => loadData(new Blob([ validChartData ], { type: 'text/csv' }), 'valid-data-chart.csv')}>Chart
|
|
327
|
+
Sample Data
|
|
328
|
+
</li>
|
|
329
|
+
<li
|
|
330
|
+
onClick={() => loadData(new Blob([ validCountyMapData ], { type: 'text/csv' }), 'valid-county-data.csv')}>United
|
|
331
|
+
States Counties Sample Data
|
|
332
|
+
</li>
|
|
299
333
|
</ul>
|
|
300
|
-
<a href="https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html" target="_blank"
|
|
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">
|
|
301
336
|
<div>
|
|
302
337
|
<h3>Get Help</h3>
|
|
303
338
|
<p>Documentation and examples on formatting data and configuring visualizations.</p>
|
|
@@ -310,82 +345,91 @@ export default function DataImport() {
|
|
|
310
345
|
<div>
|
|
311
346
|
<div className="heading-3">Data Source</div>
|
|
312
347
|
<div className="file-loaded-area">
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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 ?
|
|
319
355
|
<p>Drop file here</p> :
|
|
320
|
-
<p><FileUploadIcon
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
356
|
+
<p><FileUploadIcon/> <span>{config.dataFileName ?? 'Replace data file'}</span></p>
|
|
357
|
+
}
|
|
358
|
+
</div>
|
|
359
|
+
<div>
|
|
324
360
|
{resetButton()}
|
|
325
|
-
</div>
|
|
326
361
|
</div>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<div>
|
|
335
|
-
{resetButton()}
|
|
336
|
-
</div>
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{config.dataFileSourceType === 'url' && (
|
|
366
|
+
<div className="url-source-options">
|
|
367
|
+
<div>
|
|
368
|
+
{loadFileFromUrl(externalURL)}
|
|
337
369
|
</div>
|
|
338
|
-
|
|
370
|
+
<div>
|
|
371
|
+
{resetButton()}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
339
375
|
</div>
|
|
340
376
|
<div className="question">
|
|
341
377
|
<div className="heading-3">Describe Data</div>
|
|
342
378
|
<div className="heading-4 data-question">Data Orientation</div>
|
|
343
379
|
<div className="table-button-container">
|
|
344
|
-
<div
|
|
380
|
+
<div
|
|
381
|
+
className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === false ? ' active' : '')}
|
|
382
|
+
onClick={() => {
|
|
383
|
+
updateDescriptionProp('horizontal', false)
|
|
384
|
+
}}>
|
|
345
385
|
<strong>Vertical</strong>
|
|
346
|
-
<p>
|
|
386
|
+
<p>Values for map geography or chart date/category axis are contained in a single <em>column</em>.</p>
|
|
347
387
|
<table>
|
|
348
388
|
<tbody>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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>
|
|
364
404
|
</tbody>
|
|
365
405
|
</table>
|
|
366
406
|
<table>
|
|
367
407
|
<tbody>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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>
|
|
383
423
|
</tbody>
|
|
384
424
|
</table>
|
|
385
425
|
</div>
|
|
386
|
-
<div
|
|
426
|
+
<div
|
|
427
|
+
className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === true ? ' active' : '')}
|
|
428
|
+
onClick={() => {
|
|
429
|
+
updateDescriptionProp('horizontal', true)
|
|
430
|
+
}}>
|
|
387
431
|
<strong>Horizontal</strong>
|
|
388
|
-
<p>
|
|
432
|
+
<p>Values for map geography or chart date/category axis are contained in a single <em>row</em></p>
|
|
389
433
|
<table>
|
|
390
434
|
<tbody>
|
|
391
435
|
<tr>
|
|
@@ -404,153 +448,154 @@ export default function DataImport() {
|
|
|
404
448
|
</table>
|
|
405
449
|
<table>
|
|
406
450
|
<tbody>
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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>
|
|
419
463
|
</tbody>
|
|
420
464
|
</table>
|
|
421
465
|
</div>
|
|
422
466
|
</div>
|
|
423
467
|
</div>
|
|
424
468
|
{config.dataDescription && (
|
|
425
|
-
|
|
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
|
+
<>
|
|
426
488
|
<div className="question">
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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>
|
|
431
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>
|
|
432
559
|
</div>
|
|
433
|
-
{config.dataDescription.
|
|
434
|
-
|
|
435
|
-
<div className="
|
|
436
|
-
|
|
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)}}>
|
|
437
565
|
<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
|
-
</>
|
|
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
|
+
</>
|
|
542
584
|
)}
|
|
543
|
-
|
|
585
|
+
</>
|
|
586
|
+
)}
|
|
587
|
+
</>
|
|
544
588
|
)}
|
|
545
589
|
{config.formattedData && (
|
|
546
|
-
<button className="btn btn-primary" style={{float: 'right', marginBottom: '2em'}}
|
|
590
|
+
<button className="btn btn-primary" style={{ float: 'right', marginBottom: '2em' }}
|
|
591
|
+
onClick={() => setGlobalActive(1)}>Select your visualization type »</button>
|
|
547
592
|
)}
|
|
548
|
-
|
|
593
|
+
</div>
|
|
549
594
|
)}
|
|
550
595
|
</div>
|
|
551
596
|
<div className="right-col">
|
|
552
|
-
<PreviewDataTable data={config.data}
|
|
597
|
+
<PreviewDataTable data={config.data}/>
|
|
553
598
|
</div>
|
|
554
599
|
</>
|
|
555
|
-
)
|
|
600
|
+
)
|
|
556
601
|
}
|