@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.
Files changed (42) hide show
  1. package/dist/cdceditor.js +734 -0
  2. package/example/data-horizontal-filters.json +9 -0
  3. package/example/data-horizontal-multiseries-filters.json +20 -0
  4. package/example/data-horizontal-multiseries.json +7 -0
  5. package/example/data-horizontal.json +5 -0
  6. package/example/data-vertical-filters.json +11 -0
  7. package/example/data-vertical-multiseries-filters.json +20 -0
  8. package/example/data-vertical-multiseries-multirow-filters.json +53 -0
  9. package/example/data-vertical-multiseries-multirow.json +14 -0
  10. package/example/data-vertical-multiseries.json +7 -0
  11. package/example/data-vertical.json +7 -0
  12. package/example/region-map.json +33 -0
  13. package/example/valid-county-data.csv +3048 -0
  14. package/example/valid-county-data.json +3049 -0
  15. package/example/valid-data-chart.csv +6 -0
  16. package/example/valid-data-map.csv +59 -0
  17. package/package.json +14 -12
  18. package/src/CdcEditor.js +117 -0
  19. package/src/assets/icons/dashboard.svg +8 -0
  20. package/src/assets/icons/file-upload-solid.svg +1 -0
  21. package/src/assets/icons/globe-asia-solid.svg +1 -0
  22. package/src/assets/icons/link.svg +1 -0
  23. package/src/assets/icons/upload-solid.svg +1 -0
  24. package/src/components/ChooseTab.js +103 -0
  25. package/src/components/ConfigureTab.js +60 -0
  26. package/src/components/DataImport.js +601 -0
  27. package/src/components/PreviewDataTable.js +266 -0
  28. package/src/components/TabPane.js +5 -0
  29. package/src/components/Tabs.js +62 -0
  30. package/src/components/modal/Confirmation.js +14 -0
  31. package/src/components/modal/Modal.js +51 -0
  32. package/src/components/modal/UseModal.js +10 -0
  33. package/src/context.js +7 -0
  34. package/src/index.html +22 -0
  35. package/src/index.js +17 -0
  36. package/src/scss/_data-table.scss +15 -0
  37. package/src/scss/_variables.scss +27 -0
  38. package/src/scss/choose-vis-tab.scss +70 -0
  39. package/src/scss/configure-tab.scss +19 -0
  40. package/src/scss/data-import.scss +212 -0
  41. package/src/scss/main.scss +166 -0
  42. 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 &raquo;</button>
592
+ )}
593
+ </div>
594
+ )}
595
+ </div>
596
+ <div className="right-col">
597
+ <PreviewDataTable data={config.data}/>
598
+ </div>
599
+ </>
600
+ )
601
+ }