@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.
@@ -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
- } = useContext(GlobalState);
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 || false)
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(true === keepURL) {
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( ! newData.find( element => element.dataKey === currentValue.dataKey ) )
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( newData.columns.indexOf(oldAxisX) < 0 )
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( dataURL,
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 ( fileExtension === ".csv" && responseBlob.type === "text/plain" ) {
94
- responseBlob = responseBlob.slice(0, responseBlob.size, "text/csv")
95
- } else if ( fileExtension === ".json" && responseBlob.type === "text/plain" ) {
96
- responseBlob = responseBlob.slice(0, responseBlob.size, "application/json")
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( Object.values(errorMessages).includes(err) ) {
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 = ( mimeType === 'text/csv' ) ? 'ISO-8859-1' : '';
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 ,// new file source type
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({data:text, dataFileName:fileSource, dataFileSourceType:fileSourceType }, '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?');
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
- setConfig({...config, data:text, dataFileName:fileSource, dataFileSourceType:fileSourceType });
208
+ setConfig({ ...config, data: text, dataFileName: fileSource, dataFileSourceType: fileSourceType })
199
209
  }
200
210
  } catch (err) {
201
- setErrors(err);
211
+ setErrors(err)
202
212
  }
203
213
 
204
214
  }
205
- filereader.readAsText(fileData, encoding)
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; // set url to what is saved in config unless the user has entered something
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" placeholder="e.g., https://data.cdc.gov/resources/file.json" aria-label="Load data from external URL" aria-describedby="load-data" value={extUrl} onChange={(e) => setExternalURL(e.target.value)} />
241
- <button className="input-group-text btn btn-primary px-4" type="submit" id="load-data" onClick={() => loadData( null, externalURL )}>Load</button>
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" defaultChecked={keepURL} onClick={() => setKeepURL(!keepURL)} /> Always load from URL (normally will only pull once)
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 = ( config = {}, message = 'Are you sure you want to do this?' ) => {
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
- <button className="btn danger" onClick={() => resetEditor( {}, 'Reseting will remove your data and settings. Do you want to continue?') }>Clear
263
- <CloseIcon />
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
- { (!config.data || !config.dataFileSourceType) && ( // dataFileSourceType needs to be checked here since earlier versions did not track this state
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
- <div className={isDragActive ? 'drag-active cdcdataviz-file-selector' : 'cdcdataviz-file-selector'} {...getRootProps()}>
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
- <p>Drop file here</p> :
280
- <p>Drag file to this area, or <span>select a file</span>.</p>
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='inline-icon dismiss-error' onClick={() => setErrors( errors.filter((val, i) => i !== index) )} />
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 size {maxFileSize}MB.</p>
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 onClick={() => loadData(new Blob([validMapData], {type : 'text/csv'}), 'valid-data-map.csv')}>United States Sample Data #1</li>
298
- <li onClick={() => loadData(new Blob([validChartData], {type : 'text/csv'}), 'valid-data-chart.csv')}>Chart Sample Data</li>
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" rel="noopener noreferrer" className="guidance-link">
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
- {config.dataFileSourceType === 'file' && (
314
- <div className="data-source-options">
315
- <div className={isDragActive ? 'drag-active cdcdataviz-file-selector loaded-file' : 'cdcdataviz-file-selector loaded-file'} {...getRootProps()}>
316
- <input {...getInputProps()} />
317
- {
318
- isDragActive ?
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 /> <span>{config.dataFileName ?? 'Replace data file'}</span></p>
321
- }
322
- </div>
323
- <div>
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
- {config.dataFileSourceType === 'url' && (
330
- <div className="url-source-options">
331
- <div>
332
- {loadFileFromUrl(externalURL)}
333
- </div>
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 className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === false ? ' active' : '')} onClick={() => {updateDescriptionProp('horizontal', false)}}>
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>Geography/X-axis values are contained on a single column.</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
- <tr>
350
- <th>Date</th>
351
- <th>Value</th>
352
- <th>...</th>
353
- </tr>
354
- <tr>
355
- <td>01/01/2020</td>
356
- <td>150</td>
357
- <td>...</td>
358
- </tr>
359
- <tr>
360
- <td>02/01/2020</td>
361
- <td>150</td>
362
- <td>...</td>
363
- </tr>
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
- <tr>
369
- <th>State</th>
370
- <th>Value</th>
371
- <th>...</th>
372
- </tr>
373
- <tr>
374
- <td>Georgia</td>
375
- <td>150</td>
376
- <td>...</td>
377
- </tr>
378
- <tr>
379
- <td>Florida</td>
380
- <td>150</td>
381
- <td>...</td>
382
- </tr>
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 className={'table-button' + (config.dataDescription && config.dataDescription.horizontal === true ? ' active' : '')} onClick={() => {updateDescriptionProp('horizontal', true)}}>
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>Geography/X-axis values are contained on a single row.</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
- <tr>
408
- <th>State</th>
409
- <td>Georgia</td>
410
- <td>Florida</td>
411
- <td>...</td>
412
- </tr>
413
- <tr>
414
- <th>Value</th>
415
- <td>100</td>
416
- <td>150</td>
417
- <td>...</td>
418
- </tr>
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
- <div className="heading-4 data-question">Are there multiple series represented in your data?</div>
428
- <div>
429
- <button className={config.dataDescription.series === true ? 'btn btn-primary active' : 'btn btn-primary'} style={{marginRight: '.5em'}} onClick={() => {updateDescriptionProp('series', true)}}>Yes</button>
430
- <button className={config.dataDescription.series === false ? 'btn btn-primary active' : 'btn btn-primary'} onClick={() => {updateDescriptionProp('series', false)}}>No</button>
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.horizontal === true && config.dataDescription.series === true && (
434
- <div className="question">
435
- <div className="heading-4 data-question">Which property in the dataset represents which series the row is describing?</div>
436
- <select onChange={(e) => { updateDescriptionProp('seriesKey', e.target.value) }} value={config.dataDescription.seriesKey}>
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(key => <option value={key}>{key}</option>)}
439
- </select>
440
- </div>
441
- )}
442
- {config.dataDescription.horizontal === false && config.dataDescription.series === true && (
443
- <>
444
- <div className="question">
445
- <div className="heading-4 data-question">Are the series values in your data represented in a single row, or across multiple rows?</div>
446
- <div className="table-button-container">
447
- <div className={'table-button' + (config.dataDescription.singleRow === true ? ' active' : '')} onClick={() => {updateDescriptionProp('singleRow', true)}}>
448
- <p>Each row contains the data for an individual series in itself.</p>
449
- <table>
450
- <tbody>
451
- <tr>
452
- <th>Virus</th>
453
- <th>01/01/2020</th>
454
- <th>02/01/2020</th>
455
- <th>...</th>
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'}} onClick={() => setGlobalActive(1)}>Select your visualization type &raquo;</button>
590
+ <button className="btn btn-primary" style={{ float: 'right', marginBottom: '2em' }}
591
+ onClick={() => setGlobalActive(1)}>Select your visualization type &raquo;</button>
547
592
  )}
548
- </div>
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
  }