@cdc/editor 1.4.0 → 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 +734 -0
- package/example/data-horizontal-filters.json +9 -0
- package/example/data-horizontal-multiseries-filters.json +20 -0
- package/example/data-horizontal-multiseries.json +7 -0
- package/example/data-horizontal.json +5 -0
- package/example/data-vertical-filters.json +11 -0
- package/example/data-vertical-multiseries-filters.json +20 -0
- package/example/data-vertical-multiseries-multirow-filters.json +53 -0
- package/example/data-vertical-multiseries-multirow.json +14 -0
- package/example/data-vertical-multiseries.json +7 -0
- package/example/data-vertical.json +7 -0
- package/example/region-map.json +33 -0
- package/example/valid-county-data.csv +3048 -0
- package/example/valid-county-data.json +3049 -0
- package/example/valid-data-chart.csv +6 -0
- package/example/valid-data-map.csv +59 -0
- package/package.json +14 -12
- package/src/CdcEditor.js +117 -0
- package/src/assets/icons/dashboard.svg +8 -0
- package/src/assets/icons/file-upload-solid.svg +1 -0
- package/src/assets/icons/globe-asia-solid.svg +1 -0
- package/src/assets/icons/link.svg +1 -0
- package/src/assets/icons/upload-solid.svg +1 -0
- package/src/components/ChooseTab.js +103 -0
- package/src/components/ConfigureTab.js +60 -0
- package/src/components/DataImport.js +601 -0
- package/src/components/PreviewDataTable.js +266 -0
- package/src/components/TabPane.js +5 -0
- package/src/components/Tabs.js +62 -0
- package/src/components/modal/Confirmation.js +14 -0
- package/src/components/modal/Modal.js +51 -0
- package/src/components/modal/UseModal.js +10 -0
- package/src/context.js +7 -0
- package/src/index.html +22 -0
- package/src/index.js +17 -0
- package/src/scss/_data-table.scss +15 -0
- package/src/scss/_variables.scss +27 -0
- package/src/scss/choose-vis-tab.scss +70 -0
- package/src/scss/configure-tab.scss +19 -0
- package/src/scss/data-import.scss +212 -0
- package/src/scss/main.scss +166 -0
- package/LICENSE +0 -201
|
@@ -0,0 +1,601 @@
|
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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'
|
|
20
|
+
|
|
21
|
+
import { DataTransform } from '@cdc/core/components/DataTransform'
|
|
22
|
+
|
|
23
|
+
export default function DataImport() {
|
|
24
|
+
const {
|
|
25
|
+
config,
|
|
26
|
+
setConfig,
|
|
27
|
+
errors,
|
|
28
|
+
setErrors,
|
|
29
|
+
errorMessages,
|
|
30
|
+
maxFileSize,
|
|
31
|
+
setGlobalActive,
|
|
32
|
+
tempConfig,
|
|
33
|
+
setTempConfig,
|
|
34
|
+
sharepath
|
|
35
|
+
} = useContext(GlobalState)
|
|
36
|
+
|
|
37
|
+
const transform = new DataTransform()
|
|
38
|
+
|
|
39
|
+
const [ externalURL, setExternalURL ] = useState(config.dataFileSourceType === 'url' ? config.dataFileName : (config.dataUrl || ''))
|
|
40
|
+
|
|
41
|
+
const [ debouncedExternalURL ] = useDebounce(externalURL, 200)
|
|
42
|
+
|
|
43
|
+
const [ keepURL, setKeepURL ] = useState(!!config.dataUrl)
|
|
44
|
+
|
|
45
|
+
const supportedDataTypes = {
|
|
46
|
+
'.csv': 'text/csv',
|
|
47
|
+
'.json': 'application/json'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (false !== keepURL) {
|
|
52
|
+
setConfig({ ...config, dataUrl: debouncedExternalURL || externalURL })
|
|
53
|
+
} else {
|
|
54
|
+
let newConfig = {...config};
|
|
55
|
+
delete newConfig.dataUrl;
|
|
56
|
+
setConfig(newConfig);
|
|
57
|
+
}
|
|
58
|
+
}, [ debouncedExternalURL, keepURL ])
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check to see all series for the viz exists in the new dataset
|
|
62
|
+
*/
|
|
63
|
+
const dataExists = (newData, oldSeries, oldAxisX) => {
|
|
64
|
+
|
|
65
|
+
// Loop through old series to make sure each exists in the new data
|
|
66
|
+
oldSeries.map(function (currentValue, index, newData) {
|
|
67
|
+
if (!newData.find(element => element.dataKey === currentValue.dataKey))
|
|
68
|
+
return false
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Is the X Axis still in the dataset?
|
|
72
|
+
if (newData.columns.indexOf(oldAxisX) < 0)
|
|
73
|
+
return false
|
|
74
|
+
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const loadExternal = async () => {
|
|
79
|
+
let dataURL = ''
|
|
80
|
+
// Is URL valid?
|
|
81
|
+
try {
|
|
82
|
+
dataURL = new URL(externalURL)
|
|
83
|
+
} catch {
|
|
84
|
+
throw errorMessages.urlInvalid
|
|
85
|
+
}
|
|
86
|
+
let responseBlob = null
|
|
87
|
+
|
|
88
|
+
const fileExtension = Object.keys(supportedDataTypes).find(extension => dataURL.pathname.endsWith(extension))
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await get(dataURL,
|
|
92
|
+
{
|
|
93
|
+
responseType: 'blob'
|
|
94
|
+
})
|
|
95
|
+
.then((response) => {
|
|
96
|
+
responseBlob = response.data
|
|
97
|
+
|
|
98
|
+
// Sometimes the files are coming in as plain text types... Maybe when saved from Macs
|
|
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')
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(err)
|
|
107
|
+
|
|
108
|
+
const error = err.toString()
|
|
109
|
+
|
|
110
|
+
if (Object.values(errorMessages).includes(err)) {
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw errorMessages.failedFetch
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return responseBlob
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const onDrop = ([ uploadedFile ]) => loadData(uploadedFile)
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handle loading data
|
|
124
|
+
*/
|
|
125
|
+
const loadData = async (fileBlob = null, fileName) => {
|
|
126
|
+
let fileData = fileBlob
|
|
127
|
+
let fileSource = fileData?.path ?? fileName ?? null
|
|
128
|
+
let fileSourceType = 'file'
|
|
129
|
+
|
|
130
|
+
// Get the raw data as text from the file
|
|
131
|
+
if (null === fileData) {
|
|
132
|
+
fileSourceType = 'url'
|
|
133
|
+
try {
|
|
134
|
+
fileData = await loadExternal()
|
|
135
|
+
fileSource = externalURL
|
|
136
|
+
} catch (error) {
|
|
137
|
+
setErrors([ error ])
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check if file is too big
|
|
143
|
+
if (fileData.size > (maxFileSize * 1048576)) {
|
|
144
|
+
setErrors([ errorMessages.fileTooLarge ])
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let path = fileBlob?.name || externalURL || fileName
|
|
149
|
+
let fileExtension = path.match(/(?:\.([^.]+))?$/g)
|
|
150
|
+
|
|
151
|
+
if (fileExtension.length === 0) {
|
|
152
|
+
fileExtension = '.csv'
|
|
153
|
+
} else {
|
|
154
|
+
fileExtension = fileExtension[0]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let mimeType = supportedDataTypes[fileExtension]
|
|
158
|
+
|
|
159
|
+
// Convert from blob into raw text
|
|
160
|
+
// Have to use FileReader instead of just .text because IE11 and the polyfills for this are bugged
|
|
161
|
+
let filereader = new FileReader()
|
|
162
|
+
|
|
163
|
+
// Set encoding for CSV files - needed to render special characters properly
|
|
164
|
+
let encoding = (mimeType === 'text/csv') ? 'ISO-8859-1' : ''
|
|
165
|
+
|
|
166
|
+
filereader.onload = function () {
|
|
167
|
+
let text = this.result
|
|
168
|
+
|
|
169
|
+
switch (mimeType) {
|
|
170
|
+
case 'text/csv':
|
|
171
|
+
text = csvParse(text)
|
|
172
|
+
break
|
|
173
|
+
case 'text/plain':
|
|
174
|
+
case 'application/json':
|
|
175
|
+
try {
|
|
176
|
+
text = JSON.parse(text)
|
|
177
|
+
} catch (errors) {
|
|
178
|
+
setErrors([ errorMessages.formatting ])
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
break
|
|
182
|
+
default:
|
|
183
|
+
setErrors([ errorMessages.fileType ])
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate parsed data and set if no issues.
|
|
188
|
+
try {
|
|
189
|
+
text = transform.autoStandardize(text)
|
|
190
|
+
|
|
191
|
+
if (config.data && config.series) {
|
|
192
|
+
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
193
|
+
setConfig({
|
|
194
|
+
...config,
|
|
195
|
+
...tempConfig,
|
|
196
|
+
data: text, // new data
|
|
197
|
+
dataFileName: fileSource, // new file source
|
|
198
|
+
dataFileSourceType: fileSourceType,// new file source type
|
|
199
|
+
})
|
|
200
|
+
} else {
|
|
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?')
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
setConfig({ ...config, data: text, dataFileName: fileSource, dataFileSourceType: fileSourceType })
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
setErrors(err)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
}
|
|
215
|
+
filereader.readAsText(fileData, encoding)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
let newConfig = { ...config }
|
|
220
|
+
if (tempConfig !== null) {
|
|
221
|
+
newConfig = { ...tempConfig }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (undefined === config.formattedData && config.dataDescription) {
|
|
225
|
+
const formattedData = transform.developerStandardize(config.data, config.dataDescription)
|
|
226
|
+
|
|
227
|
+
if (formattedData) newConfig.formattedData = formattedData
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (tempConfig !== null) setTempConfig(null)
|
|
231
|
+
|
|
232
|
+
setConfig(newConfig)
|
|
233
|
+
}, [])
|
|
234
|
+
|
|
235
|
+
const updateDescriptionProp = (key, value) => {
|
|
236
|
+
let dataDescription = { ...config.dataDescription, [key]: value }
|
|
237
|
+
let formattedData = transform.developerStandardize(config.data, dataDescription)
|
|
238
|
+
|
|
239
|
+
setConfig({ ...config, formattedData, dataDescription })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop })
|
|
243
|
+
|
|
244
|
+
const loadFileFromUrl = (url) => {
|
|
245
|
+
// const extUrl = (url) ? url : config.dataFileName // set url to what is saved in config unless the user has entered something
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<>
|
|
249
|
+
<form className="input-group d-flex" onSubmit={(e) => e.preventDefault()}>
|
|
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>
|
|
256
|
+
</form>
|
|
257
|
+
<label htmlFor="keep-url" className="mt-1 d-flex keep-url">
|
|
258
|
+
<input type="checkbox" id="keep-url" checked={keepURL} onChange={() => setKeepURL(!keepURL)}/> Always
|
|
259
|
+
load from URL (normally will only pull once)
|
|
260
|
+
</label>
|
|
261
|
+
</>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const resetEditor = (config = {}, message = 'Are you sure you want to do this?') => {
|
|
266
|
+
config.newViz = true
|
|
267
|
+
const confirmDataReset = window.confirm(message)
|
|
268
|
+
|
|
269
|
+
if (confirmDataReset === true) {
|
|
270
|
+
setTempConfig(null)
|
|
271
|
+
setConfig(config)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const resetButton = () => {
|
|
276
|
+
return ( //todo convert to modal
|
|
277
|
+
<button className="btn danger"
|
|
278
|
+
onClick={() => resetEditor({}, 'Reseting will remove your data and settings. Do you want to continue?')}>Clear
|
|
279
|
+
<CloseIcon/>
|
|
280
|
+
</button>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
<div className="left-col">
|
|
287
|
+
{(!config.data || !config.dataFileSourceType) && ( // dataFileSourceType needs to be checked here since earlier versions did not track this state
|
|
288
|
+
<div className="load-data-area">
|
|
289
|
+
<Tabs>
|
|
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()}>
|
|
298
|
+
<input {...getInputProps()} />
|
|
299
|
+
{
|
|
300
|
+
isDragActive ?
|
|
301
|
+
<p>Drop file here</p> :
|
|
302
|
+
<p>Drag file to this area, or <span>select a file</span>.</p>
|
|
303
|
+
}
|
|
304
|
+
</div>
|
|
305
|
+
</TabPane>
|
|
306
|
+
<TabPane title="Load from URL" icon={<LinkIcon className="inline-icon"/>}>
|
|
307
|
+
{loadFileFromUrl(externalURL)}
|
|
308
|
+
</TabPane>
|
|
309
|
+
</Tabs>
|
|
310
|
+
{errors && (errors.map ? errors.map((message, index) => (
|
|
311
|
+
<div className="error-box slim mt-2" key={`error-${message}`}>
|
|
312
|
+
<span>{message}</span> <CloseIcon className="inline-icon dismiss-error"
|
|
313
|
+
onClick={() => setErrors(errors.filter((val, i) => i !== index))}/>
|
|
314
|
+
</div>
|
|
315
|
+
)) : errors.message)}
|
|
316
|
+
<p className="footnote">Supported file types: {Object.keys(supportedDataTypes).join(', ')}. Maximum file
|
|
317
|
+
size {maxFileSize}MB.</p>
|
|
318
|
+
{/* TODO: Add more sample data in, but this will do for now. */}
|
|
319
|
+
<span className="heading-3">Load Sample Data:</span>
|
|
320
|
+
<ul className="sample-data-list">
|
|
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>
|
|
333
|
+
</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
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
{config.dataFileSourceType && (
|
|
345
|
+
<div>
|
|
346
|
+
<div className="heading-3">Data Source</div>
|
|
347
|
+
<div className="file-loaded-area">
|
|
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
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
)}
|
|
595
|
+
</div>
|
|
596
|
+
<div className="right-col">
|
|
597
|
+
<PreviewDataTable data={config.data}/>
|
|
598
|
+
</div>
|
|
599
|
+
</>
|
|
600
|
+
)
|
|
601
|
+
}
|