@cdc/editor 4.26.3 → 4.26.5
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-CY9IcPSi.es.js +6 -0
- package/dist/cdceditor-DlpiY3fQ.es.js +4 -0
- package/dist/cdceditor.js +78238 -74548
- package/example/private/dashboard-filter-issue/dashboard-filter-issue.json +957 -0
- package/package.json +9 -9
- package/src/_stories/Editor.stories.tsx +124 -0
- package/src/assets.d.ts +4 -0
- package/src/components/ChooseTab.test.tsx +64 -1
- package/src/components/ChooseTab.tsx +82 -4
- package/src/components/DataImport/components/DataImport.tsx +135 -50
- package/src/components/DataImport/components/SampleData.test.tsx +16 -0
- package/src/components/DataImport/components/SampleData.tsx +6 -0
- package/src/components/DataImport/components/samples/valid-heatmap-varicella-cases.csv +13 -0
- package/src/components/DataImport/helpers/applyAutoDetectedDateParseFormat.ts +35 -0
- package/src/components/DataImport/tests/applyAutoDetectedDateParseFormat.test.ts +128 -0
- package/src/components/PreviewDataTable.test.tsx +184 -0
- package/src/components/PreviewDataTable.tsx +18 -7
- package/src/components/modal/Confirmation.jsx +5 -4
- package/src/scss/choose-vis-tab.scss +7 -1
- package/src/scss/main.scss +14 -6
- package/dist/cdceditor-vr9HZwRt.es.js +0 -6
- package/example/data-horizontal-filters.json +0 -8
- package/example/data-horizontal-multiseries-filters.json +0 -18
- package/example/data-horizontal-multiseries.json +0 -6
- package/example/data-horizontal.json +0 -4
- package/example/data-vertical-filters.json +0 -10
- package/example/data-vertical-multiseries-filters.json +0 -18
- package/example/data-vertical-multiseries-multirow-filters.json +0 -50
- package/example/data-vertical-multiseries-multirow.json +0 -14
- package/example/data-vertical-multiseries.json +0 -6
- package/example/data-vertical.json +0 -6
- package/example/region-map.json +0 -33
- package/example/test.json +0 -110280
- package/example/valid-county-data.json +0 -3049
- package/example/valid-scatterplot.csv +0 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useContext, useEffect } from 'react'
|
|
2
2
|
import { useDropzone } from 'react-dropzone'
|
|
3
|
-
import
|
|
3
|
+
import { csvFormat } from 'd3'
|
|
4
4
|
|
|
5
5
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
6
6
|
|
|
@@ -19,6 +19,7 @@ import CloseIcon from '@cdc/core/assets/icon-close.svg'
|
|
|
19
19
|
import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
20
20
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
21
21
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
22
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
22
23
|
import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
|
|
23
24
|
import { type Visualization } from '@cdc/core/types/Visualization'
|
|
24
25
|
import { type DataSet } from '@cdc/core/types/DataSet'
|
|
@@ -32,6 +33,7 @@ import { supportedDataTypes } from '../helpers/supportedDataTypes'
|
|
|
32
33
|
import { getFileExtension } from '../helpers/getFileExtension'
|
|
33
34
|
import { parseTextByMimeType } from '../helpers/parseTextByMimeType'
|
|
34
35
|
import { getMimeType } from '../helpers/getMimeType'
|
|
36
|
+
import { applyAutoDetectedDateParseFormat } from '../helpers/applyAutoDetectedDateParseFormat'
|
|
35
37
|
import {
|
|
36
38
|
extractCoveData,
|
|
37
39
|
getSampleVegaJson,
|
|
@@ -99,25 +101,27 @@ const DataImport = () => {
|
|
|
99
101
|
|
|
100
102
|
try {
|
|
101
103
|
// eslint-disable-next-line no-unused-vars
|
|
102
|
-
await
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
104
|
+
const response = await fetch(dataURL.toString())
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`HTTP error ${response.status}`)
|
|
107
|
+
}
|
|
108
|
+
responseBlob = await response.blob()
|
|
109
|
+
|
|
110
|
+
// Strip charset/parameters — fetch preserves the full Content-Type (e.g. 'application/json; charset=utf-8')
|
|
111
|
+
// while XHR (axios) would strip them, so normalise before comparing.
|
|
112
|
+
const blobBaseType = responseBlob.type.split(';')[0].trim()
|
|
113
|
+
|
|
114
|
+
// Sometimes the files are coming in as plain text types... Maybe when saved from Macs
|
|
115
|
+
const csvTypes = ['text/csv', 'text/plain']
|
|
116
|
+
if ((fileExtension === '.csv' && csvTypes.includes(blobBaseType)) || isSolrCsv(externalURL)) {
|
|
117
|
+
responseBlob = responseBlob.slice(0, responseBlob.size, 'text/csv')
|
|
118
|
+
} else if (
|
|
119
|
+
blobBaseType === 'application/json' ||
|
|
120
|
+
(fileExtension === '.json' && blobBaseType === 'text/plain') ||
|
|
121
|
+
isSolrJson(externalURL)
|
|
122
|
+
) {
|
|
123
|
+
responseBlob = responseBlob.slice(0, responseBlob.size, 'application/json')
|
|
124
|
+
}
|
|
121
125
|
} catch (err) {
|
|
122
126
|
// eslint-disable-next-line no-console
|
|
123
127
|
console.error('Error in loadExternal', err)
|
|
@@ -208,14 +212,21 @@ const DataImport = () => {
|
|
|
208
212
|
payload: { datasetKey: newDatasetName || fileSource, dataset, oldDatasetKey }
|
|
209
213
|
})
|
|
210
214
|
} else {
|
|
215
|
+
const configWithAutoDetectedDateFormat = applyAutoDetectedDateParseFormat(
|
|
216
|
+
{
|
|
217
|
+
...config,
|
|
218
|
+
...tempConfig
|
|
219
|
+
},
|
|
220
|
+
newData as Record<string, unknown>[]
|
|
221
|
+
)
|
|
222
|
+
|
|
211
223
|
let newConfig = {
|
|
212
|
-
...
|
|
213
|
-
...tempConfig,
|
|
224
|
+
...configWithAutoDetectedDateFormat,
|
|
214
225
|
data: newData,
|
|
215
226
|
dataMetadata,
|
|
216
227
|
dataFileName: fileSource, // new file source
|
|
217
228
|
dataFileSourceType: fileSourceType, // new file source type
|
|
218
|
-
formattedData: transform.developerStandardize(newData,
|
|
229
|
+
formattedData: transform.developerStandardize(newData, configWithAutoDetectedDateFormat.dataDescription)
|
|
219
230
|
}
|
|
220
231
|
if (setDataURL) {
|
|
221
232
|
newConfig.dataUrl = fileSource
|
|
@@ -359,7 +370,7 @@ const DataImport = () => {
|
|
|
359
370
|
Always load from URL (normally will only pull once)
|
|
360
371
|
</label>
|
|
361
372
|
<div className='d-flex justify-content-end mt-2 mb-3'>
|
|
362
|
-
<
|
|
373
|
+
<Button
|
|
363
374
|
className='btn btn-primary px-4'
|
|
364
375
|
type='submit'
|
|
365
376
|
id='load-data'
|
|
@@ -367,7 +378,7 @@ const DataImport = () => {
|
|
|
367
378
|
onClick={() => loadData(null, externalURL, editingDataset)}
|
|
368
379
|
>
|
|
369
380
|
Save & Load
|
|
370
|
-
</
|
|
381
|
+
</Button>
|
|
371
382
|
</div>
|
|
372
383
|
</>
|
|
373
384
|
)
|
|
@@ -388,7 +399,7 @@ const DataImport = () => {
|
|
|
388
399
|
return (
|
|
389
400
|
//todo convert to modal
|
|
390
401
|
<>
|
|
391
|
-
<
|
|
402
|
+
<Button
|
|
392
403
|
className='btn danger'
|
|
393
404
|
onClick={() =>
|
|
394
405
|
resetEditor(
|
|
@@ -399,7 +410,7 @@ const DataImport = () => {
|
|
|
399
410
|
>
|
|
400
411
|
Clear
|
|
401
412
|
<CloseIcon />
|
|
402
|
-
</
|
|
413
|
+
</Button>
|
|
403
414
|
{/* DEV-851 link to replace file should pop file dialog */}
|
|
404
415
|
{config.dataFileSourceType === 'file' && (
|
|
405
416
|
<div className='link link-replace' {...getRootProps2()}>
|
|
@@ -435,6 +446,49 @@ const DataImport = () => {
|
|
|
435
446
|
dispatch({ type: 'DELETE_DASHBOARD_DATASET', payload: { datasetKey } })
|
|
436
447
|
}
|
|
437
448
|
|
|
449
|
+
const downloadCSV = (data: Array<Record<string, unknown>>, filename: string) => {
|
|
450
|
+
// Normalize filename: strip query/hash, remove path, replace extension with .csv
|
|
451
|
+
let baseName = filename || ''
|
|
452
|
+
|
|
453
|
+
const queryIndex = baseName.indexOf('?')
|
|
454
|
+
const hashIndex = baseName.indexOf('#')
|
|
455
|
+
const cutoffIndex = queryIndex === -1 ? hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex)
|
|
456
|
+
if (cutoffIndex !== -1) {
|
|
457
|
+
baseName = baseName.substring(0, cutoffIndex)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const lastSlash = Math.max(baseName.lastIndexOf('/'), baseName.lastIndexOf('\\'))
|
|
461
|
+
if (lastSlash !== -1) {
|
|
462
|
+
baseName = baseName.substring(lastSlash + 1)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const lastDot = baseName.lastIndexOf('.')
|
|
466
|
+
if (lastDot > 0) {
|
|
467
|
+
baseName = baseName.substring(0, lastDot)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (!baseName) {
|
|
471
|
+
baseName = 'data'
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const name = `${baseName}.csv`
|
|
475
|
+
const blob = new Blob(['\uFEFF' + csvFormat(data)], { type: 'text/csv;charset=utf-8;' })
|
|
476
|
+
// @ts-ignore
|
|
477
|
+
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
478
|
+
// @ts-ignore
|
|
479
|
+
navigator.msSaveBlob(blob, name)
|
|
480
|
+
} else {
|
|
481
|
+
const url = URL.createObjectURL(blob)
|
|
482
|
+
const link = document.createElement('a')
|
|
483
|
+
link.href = url
|
|
484
|
+
link.download = name
|
|
485
|
+
document.body.appendChild(link)
|
|
486
|
+
link.click()
|
|
487
|
+
document.body.removeChild(link)
|
|
488
|
+
URL.revokeObjectURL(url)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
438
492
|
const updateDataFromVegaConfig = pastedConfig => {
|
|
439
493
|
const vegaConfig = parseVegaConfig(JSON.parse(pastedConfig))
|
|
440
494
|
const newData = extractCoveData(vegaConfig)
|
|
@@ -609,7 +663,7 @@ const DataImport = () => {
|
|
|
609
663
|
</fieldset>
|
|
610
664
|
)
|
|
611
665
|
)}
|
|
612
|
-
<
|
|
666
|
+
<Button
|
|
613
667
|
className='btn full-width btn-primary'
|
|
614
668
|
onClick={() => {
|
|
615
669
|
setConfig({
|
|
@@ -621,7 +675,7 @@ const DataImport = () => {
|
|
|
621
675
|
}}
|
|
622
676
|
>
|
|
623
677
|
Add New URL Filter
|
|
624
|
-
</
|
|
678
|
+
</Button>
|
|
625
679
|
</>
|
|
626
680
|
)
|
|
627
681
|
|
|
@@ -639,7 +693,7 @@ const DataImport = () => {
|
|
|
639
693
|
<th>Name</th>
|
|
640
694
|
<th>Size</th>
|
|
641
695
|
<th>Type</th>
|
|
642
|
-
<th colSpan={
|
|
696
|
+
<th colSpan={4}>Actions</th>
|
|
643
697
|
</tr>
|
|
644
698
|
</thead>
|
|
645
699
|
<tbody>
|
|
@@ -651,17 +705,33 @@ const DataImport = () => {
|
|
|
651
705
|
<td className='p-1'>{displaySize(config.datasets[datasetKey].dataFileSize)}</td>
|
|
652
706
|
<td className='p-1'>{config.datasets[datasetKey].dataFileFormat}</td>
|
|
653
707
|
<td>
|
|
654
|
-
<
|
|
655
|
-
|
|
708
|
+
<Button
|
|
709
|
+
variant='link'
|
|
710
|
+
className='p-1'
|
|
656
711
|
onClick={() => setGlobalDatasetProp(datasetKey, 'preview', true)}
|
|
657
712
|
>
|
|
658
713
|
Preview
|
|
714
|
+
</Button>
|
|
715
|
+
</td>
|
|
716
|
+
<td>
|
|
717
|
+
<button
|
|
718
|
+
className='btn btn-link p-1'
|
|
719
|
+
disabled={!config.datasets[datasetKey].data?.length}
|
|
720
|
+
onClick={() =>
|
|
721
|
+
downloadCSV(
|
|
722
|
+
config.datasets[datasetKey].data,
|
|
723
|
+
config.datasets[datasetKey].dataFileName || datasetKey
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
>
|
|
727
|
+
Download
|
|
659
728
|
</button>
|
|
660
729
|
</td>
|
|
661
730
|
<td>
|
|
662
731
|
{config.datasets[datasetKey].dataFileSourceType === 'url' && (
|
|
663
|
-
<
|
|
664
|
-
|
|
732
|
+
<Button
|
|
733
|
+
variant='link'
|
|
734
|
+
className='p-1'
|
|
665
735
|
onClick={() => {
|
|
666
736
|
if (editingDataset === datasetKey) {
|
|
667
737
|
setEditingDataset(undefined)
|
|
@@ -677,13 +747,13 @@ const DataImport = () => {
|
|
|
677
747
|
}}
|
|
678
748
|
>
|
|
679
749
|
Edit
|
|
680
|
-
</
|
|
750
|
+
</Button>
|
|
681
751
|
)}
|
|
682
752
|
</td>
|
|
683
753
|
<td>
|
|
684
|
-
<
|
|
754
|
+
<Button variant='danger' onClick={() => removeDataset(datasetKey)}>
|
|
685
755
|
X
|
|
686
|
-
</
|
|
756
|
+
</Button>
|
|
687
757
|
</td>
|
|
688
758
|
</tr>
|
|
689
759
|
)
|
|
@@ -719,6 +789,14 @@ const DataImport = () => {
|
|
|
719
789
|
)}
|
|
720
790
|
</div>
|
|
721
791
|
<div>{resetButton()}</div>
|
|
792
|
+
{config.data?.length > 0 && (
|
|
793
|
+
<button
|
|
794
|
+
className='btn btn-link p-1'
|
|
795
|
+
onClick={() => downloadCSV(config.data, config.dataFileName || 'data')}
|
|
796
|
+
>
|
|
797
|
+
Download CSV
|
|
798
|
+
</button>
|
|
799
|
+
)}
|
|
722
800
|
</div>
|
|
723
801
|
)}
|
|
724
802
|
|
|
@@ -727,6 +805,14 @@ const DataImport = () => {
|
|
|
727
805
|
<div className='url-source-options'>
|
|
728
806
|
<div>{loadDataFromUrl()}</div>
|
|
729
807
|
<div>{resetButton()}</div>
|
|
808
|
+
{config.data?.length > 0 && (
|
|
809
|
+
<button
|
|
810
|
+
className='btn btn-link p-1'
|
|
811
|
+
onClick={() => downloadCSV(config.data, config.dataFileName || 'data')}
|
|
812
|
+
>
|
|
813
|
+
Download CSV
|
|
814
|
+
</button>
|
|
815
|
+
)}
|
|
730
816
|
</div>
|
|
731
817
|
{config.dataUrl && (config.type === 'chart' || config.type === 'map') && urlFilters}
|
|
732
818
|
</>
|
|
@@ -750,15 +836,16 @@ const DataImport = () => {
|
|
|
750
836
|
placeholder='{ }'
|
|
751
837
|
/>
|
|
752
838
|
<div className='mb-3 d-flex justify-content-end'>
|
|
753
|
-
<
|
|
754
|
-
|
|
839
|
+
<Button
|
|
840
|
+
variant='primary'
|
|
841
|
+
className='px-4'
|
|
755
842
|
type='submit'
|
|
756
843
|
id='load-data'
|
|
757
844
|
disabled={!pastedConfig}
|
|
758
845
|
onClick={() => updateDataFromVegaConfig(pastedConfig)}
|
|
759
846
|
>
|
|
760
847
|
Save & Load
|
|
761
|
-
</
|
|
848
|
+
</Button>
|
|
762
849
|
</div>
|
|
763
850
|
</div>
|
|
764
851
|
</TabPane>
|
|
@@ -812,15 +899,16 @@ const DataImport = () => {
|
|
|
812
899
|
/>
|
|
813
900
|
</label>
|
|
814
901
|
<div className='d-flex justify-content-end mt-2 mb-3'>
|
|
815
|
-
<
|
|
816
|
-
|
|
902
|
+
<Button
|
|
903
|
+
variant='primary'
|
|
904
|
+
className='px-4'
|
|
817
905
|
type='submit'
|
|
818
906
|
id='load-data'
|
|
819
907
|
disabled={!newDatasetName || !externalURL}
|
|
820
908
|
onClick={() => loadData(null, externalURL, editingDataset)}
|
|
821
909
|
>
|
|
822
910
|
Save & Load
|
|
823
|
-
</
|
|
911
|
+
</Button>
|
|
824
912
|
</div>
|
|
825
913
|
</TabPane>
|
|
826
914
|
</Tabs>
|
|
@@ -895,20 +983,17 @@ const DataImport = () => {
|
|
|
895
983
|
|
|
896
984
|
{config.type === 'dashboard' && !addingDataset && (
|
|
897
985
|
<div className='mt-2'>
|
|
898
|
-
<
|
|
986
|
+
<Button variant='primary' onClick={() => setAddingDataset(true)}>
|
|
899
987
|
+ Add More Files
|
|
900
|
-
</
|
|
988
|
+
</Button>
|
|
901
989
|
</div>
|
|
902
990
|
)}
|
|
903
991
|
|
|
904
992
|
{readyToConfigure && (
|
|
905
993
|
<div className='mt-2'>
|
|
906
|
-
<
|
|
907
|
-
className='btn btn-primary'
|
|
908
|
-
onClick={() => dispatch({ type: 'EDITOR_SET_GLOBALACTIVE', payload: 2 })}
|
|
909
|
-
>
|
|
994
|
+
<Button variant='primary' onClick={() => dispatch({ type: 'EDITOR_SET_GLOBALACTIVE', payload: 2 })}>
|
|
910
995
|
Configure your visualization
|
|
911
|
-
</
|
|
996
|
+
</Button>
|
|
912
997
|
</div>
|
|
913
998
|
)}
|
|
914
999
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import SampleData from './SampleData'
|
|
2
|
+
|
|
3
|
+
describe('SampleData', () => {
|
|
4
|
+
it('includes a synthetic varicella HeatMap sample dataset', () => {
|
|
5
|
+
const sample = SampleData.data.charts.find(sample => sample.fileName === 'valid-heatmap-varicella-cases.csv')
|
|
6
|
+
|
|
7
|
+
expect(sample).toBeDefined()
|
|
8
|
+
expect(sample).toEqual(
|
|
9
|
+
expect.objectContaining({
|
|
10
|
+
text: 'HeatMap Data (Synthetic Varicella Cases)',
|
|
11
|
+
data: expect.stringContaining('Month,HHS Region 1,HHS Region 2')
|
|
12
|
+
})
|
|
13
|
+
)
|
|
14
|
+
expect(sample?.data).toContain('Apr,55,61,78,69,72,64,58,57,66,50')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -10,6 +10,7 @@ import validBoxPlotData from './samples/valid-boxplot.csv?raw'
|
|
|
10
10
|
import validChartData from './samples/valid-data-chart.csv?raw'
|
|
11
11
|
import validCountyMapData from './samples/valid-county-data.csv?raw'
|
|
12
12
|
import validForecastData from './samples/valid-forecast-data.csv?raw'
|
|
13
|
+
import validHeatMapData from './samples/valid-heatmap-varicella-cases.csv?raw'
|
|
13
14
|
import validGeoPoint from './samples/valid-geo-point.csv?raw'
|
|
14
15
|
import validHorizonData from './samples/valid-horizon-chart.json?raw'
|
|
15
16
|
import validMapData from './samples/valid-data-map.csv?raw'
|
|
@@ -60,6 +61,11 @@ const sampleData = {
|
|
|
60
61
|
fileName: 'valid-horizon-data.json',
|
|
61
62
|
data: validHorizonData
|
|
62
63
|
},
|
|
64
|
+
{
|
|
65
|
+
text: 'HeatMap Data (Synthetic Varicella Cases)',
|
|
66
|
+
fileName: 'valid-heatmap-varicella-cases.csv',
|
|
67
|
+
data: validHeatMapData
|
|
68
|
+
},
|
|
63
69
|
{
|
|
64
70
|
text: 'Sankey Chart Data',
|
|
65
71
|
fileName: 'valid-sankey-data.json',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Month,HHS Region 1,HHS Region 2,HHS Region 3,HHS Region 4,HHS Region 5,HHS Region 6,HHS Region 7,HHS Region 8,HHS Region 9,HHS Region 10
|
|
2
|
+
Jan,18,22,35,28,31,24,19,21,26,16
|
|
3
|
+
Feb,24,29,41,34,38,31,27,28,33,22
|
|
4
|
+
Mar,42,48,63,56,59,51,46,44,53,39
|
|
5
|
+
Apr,55,61,78,69,72,64,58,57,66,50
|
|
6
|
+
May,49,54,70,62,65,58,52,51,60,46
|
|
7
|
+
Jun,31,36,49,43,45,40,35,34,41,30
|
|
8
|
+
Jul,17,21,30,25,28,23,20,19,24,16
|
|
9
|
+
Aug,12,15,22,18,20,16,14,13,17,11
|
|
10
|
+
Sep,14,18,26,21,23,19,16,15,20,13
|
|
11
|
+
Oct,20,24,33,28,30,25,22,21,27,18
|
|
12
|
+
Nov,27,32,44,37,40,34,29,30,35,24
|
|
13
|
+
Dec,34,39,52,45,48,41,36,37,43,31
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getAutoDetectedDateParseFormat, isDateScale } from '@cdc/core/helpers/cove/date'
|
|
2
|
+
import { type Visualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
|
|
4
|
+
const isChartWithDateAxis = (config: Visualization) =>
|
|
5
|
+
config?.type === 'chart' && !!config?.xAxis && isDateScale(config.xAxis)
|
|
6
|
+
|
|
7
|
+
export const applyAutoDetectedDateParseFormat = (
|
|
8
|
+
config: Visualization,
|
|
9
|
+
importedData: Record<string, unknown>[]
|
|
10
|
+
): Visualization => {
|
|
11
|
+
if (!isChartWithDateAxis(config) || !config?.xAxis?.dataKey || !importedData?.length) {
|
|
12
|
+
return config
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const existingDateParseFormat = config.xAxis.dateParseFormat
|
|
16
|
+
|
|
17
|
+
if (typeof existingDateParseFormat === 'string' && existingDateParseFormat.trim() !== '') {
|
|
18
|
+
return config
|
|
19
|
+
}
|
|
20
|
+
const xAxisKey = config.xAxis.dataKey
|
|
21
|
+
|
|
22
|
+
const autoDetectedDateParseFormat = getAutoDetectedDateParseFormat(importedData, xAxisKey)
|
|
23
|
+
|
|
24
|
+
if (!autoDetectedDateParseFormat) {
|
|
25
|
+
return config
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...config,
|
|
30
|
+
xAxis: {
|
|
31
|
+
...config.xAxis,
|
|
32
|
+
dateParseFormat: autoDetectedDateParseFormat
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { applyAutoDetectedDateParseFormat } from '../helpers/applyAutoDetectedDateParseFormat'
|
|
3
|
+
|
|
4
|
+
describe('applyAutoDetectedDateParseFormat', () => {
|
|
5
|
+
const baseChartConfig: any = {
|
|
6
|
+
type: 'chart',
|
|
7
|
+
xAxis: {
|
|
8
|
+
type: 'date',
|
|
9
|
+
dataKey: 'date',
|
|
10
|
+
dateParseFormat: ''
|
|
11
|
+
},
|
|
12
|
+
tooltips: {
|
|
13
|
+
dateDisplayFormat: '%B %-d, %Y'
|
|
14
|
+
},
|
|
15
|
+
table: {
|
|
16
|
+
dateDisplayFormat: '%b %-d %Y'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const reliableSlashDateRows = [
|
|
21
|
+
{ date: '2024/03/15', value: 10 },
|
|
22
|
+
{ date: '2025/11/09', value: 12 }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const ambiguousUsOrEuDateRows = [
|
|
26
|
+
{ date: '01/02/2024', value: 10 },
|
|
27
|
+
{ date: '02/03/2024', value: 12 }
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
const missingXAxisDateRows = [{ otherDate: '2024/03/15', value: 10 }]
|
|
31
|
+
const sparseFirstRowReliableDateRows = [
|
|
32
|
+
{ otherDate: 'ignore-me', value: 8 },
|
|
33
|
+
{ date: '2024/03/15', value: 10 },
|
|
34
|
+
{ date: '2025/11/09', value: 12 }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
it('auto-fills the chart xAxis date parse format when detection is reliable', () => {
|
|
38
|
+
const result: any = applyAutoDetectedDateParseFormat(baseChartConfig, reliableSlashDateRows)
|
|
39
|
+
|
|
40
|
+
expect(result.xAxis.dateParseFormat).toBe('%Y/%m/%d')
|
|
41
|
+
expect(result.tooltips.dateDisplayFormat).toBe('%B %-d, %Y')
|
|
42
|
+
expect(result.table.dateDisplayFormat).toBe('%b %-d %Y')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('auto-fills the chart xAxis date parse format for date-time axes when detection is reliable', () => {
|
|
46
|
+
const result: any = applyAutoDetectedDateParseFormat(
|
|
47
|
+
{
|
|
48
|
+
...baseChartConfig,
|
|
49
|
+
xAxis: {
|
|
50
|
+
...baseChartConfig.xAxis,
|
|
51
|
+
type: 'date-time'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
reliableSlashDateRows
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(result.xAxis.dateParseFormat).toBe('%Y/%m/%d')
|
|
58
|
+
expect(result.tooltips.dateDisplayFormat).toBe('%B %-d, %Y')
|
|
59
|
+
expect(result.table.dateDisplayFormat).toBe('%b %-d %Y')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('leaves the config unchanged when the sample is ambiguous', () => {
|
|
63
|
+
const result: any = applyAutoDetectedDateParseFormat(baseChartConfig, ambiguousUsOrEuDateRows)
|
|
64
|
+
|
|
65
|
+
expect(result).toEqual(baseChartConfig)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('leaves the config unchanged when xAxis.dateParseFormat is already configured', () => {
|
|
69
|
+
const preconfiguredChartConfig: any = {
|
|
70
|
+
...baseChartConfig,
|
|
71
|
+
xAxis: {
|
|
72
|
+
...baseChartConfig.xAxis,
|
|
73
|
+
dateParseFormat: '%d/%m/%Y'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result: any = applyAutoDetectedDateParseFormat(preconfiguredChartConfig, reliableSlashDateRows)
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual(preconfiguredChartConfig)
|
|
80
|
+
})
|
|
81
|
+
it('does nothing for non-date chart axes', () => {
|
|
82
|
+
const result: any = applyAutoDetectedDateParseFormat(
|
|
83
|
+
{
|
|
84
|
+
...baseChartConfig,
|
|
85
|
+
xAxis: {
|
|
86
|
+
...baseChartConfig.xAxis,
|
|
87
|
+
type: 'categorical'
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[{ date: '2024/03/15', value: 10 }]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual({
|
|
94
|
+
...baseChartConfig,
|
|
95
|
+
xAxis: {
|
|
96
|
+
...baseChartConfig.xAxis,
|
|
97
|
+
type: 'categorical'
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('does nothing for non-chart visualizations', () => {
|
|
103
|
+
const result: any = applyAutoDetectedDateParseFormat(
|
|
104
|
+
{
|
|
105
|
+
...baseChartConfig,
|
|
106
|
+
type: 'map'
|
|
107
|
+
},
|
|
108
|
+
[{ date: '2024/03/15', value: 10 }]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
expect(result).toEqual({
|
|
112
|
+
...baseChartConfig,
|
|
113
|
+
type: 'map'
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does nothing when the configured x-axis column is missing', () => {
|
|
118
|
+
const result: any = applyAutoDetectedDateParseFormat(baseChartConfig, missingXAxisDateRows)
|
|
119
|
+
|
|
120
|
+
expect(result).toEqual(baseChartConfig)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('still detects the date parse format when the first row is missing the x-axis key', () => {
|
|
124
|
+
const result: any = applyAutoDetectedDateParseFormat(baseChartConfig, sparseFirstRowReliableDateRows)
|
|
125
|
+
|
|
126
|
+
expect(result.xAxis.dateParseFormat).toBe('%Y/%m/%d')
|
|
127
|
+
})
|
|
128
|
+
})
|