@cdc/editor 4.26.2 → 4.26.4
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 +84436 -77912
- package/package.json +9 -9
- package/src/CdcEditor.tsx +2 -3
- package/src/_stories/Editor.stories.tsx +173 -6
- package/src/components/ChooseTab.test.tsx +36 -0
- package/src/components/ChooseTab.tsx +39 -26
- package/src/components/DataImport/components/DataImport.tsx +124 -35
- 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 +55 -49
- package/src/components/modal/Confirmation.jsx +5 -4
- package/src/scss/main.scss +15 -2
- package/dist/cdceditor-Cf9_fbQf.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,7 @@
|
|
|
1
1
|
import React, { useState, useContext, useEffect } from 'react'
|
|
2
2
|
import { useDropzone } from 'react-dropzone'
|
|
3
3
|
import axios from 'axios'
|
|
4
|
+
import { csvFormat } from 'd3'
|
|
4
5
|
|
|
5
6
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
6
7
|
|
|
@@ -19,12 +20,13 @@ import CloseIcon from '@cdc/core/assets/icon-close.svg'
|
|
|
19
20
|
import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
20
21
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
21
22
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
23
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
22
24
|
import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
|
|
23
25
|
import { type Visualization } from '@cdc/core/types/Visualization'
|
|
24
26
|
import { type DataSet } from '@cdc/core/types/DataSet'
|
|
25
27
|
|
|
26
28
|
import './data-import.scss'
|
|
27
|
-
import '@cdc/core/
|
|
29
|
+
import '@cdc/core/components/managers/data-designer.scss'
|
|
28
30
|
|
|
29
31
|
import { errorMessages, maxFileSize } from '../../../helpers/errorMessages'
|
|
30
32
|
import { displaySize } from '../helpers/displaySize'
|
|
@@ -32,6 +34,7 @@ import { supportedDataTypes } from '../helpers/supportedDataTypes'
|
|
|
32
34
|
import { getFileExtension } from '../helpers/getFileExtension'
|
|
33
35
|
import { parseTextByMimeType } from '../helpers/parseTextByMimeType'
|
|
34
36
|
import { getMimeType } from '../helpers/getMimeType'
|
|
37
|
+
import { applyAutoDetectedDateParseFormat } from '../helpers/applyAutoDetectedDateParseFormat'
|
|
35
38
|
import {
|
|
36
39
|
extractCoveData,
|
|
37
40
|
getSampleVegaJson,
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
parseVegaConfig,
|
|
40
43
|
updateVegaData
|
|
41
44
|
} from '@cdc/core/helpers/vegaConfig'
|
|
45
|
+
import { extractDataAndMetadata } from '@cdc/core/helpers/extractDataAndMetadata'
|
|
42
46
|
|
|
43
47
|
const DataImport = () => {
|
|
44
48
|
const { config, errors, tempConfig, sharepath } = useContext(ConfigContext)
|
|
@@ -179,12 +183,13 @@ const DataImport = () => {
|
|
|
179
183
|
const filereader = new FileReader()
|
|
180
184
|
|
|
181
185
|
filereader.onload = function () {
|
|
182
|
-
const handleSetConfig = (newData: Object[], useTempConfig = false) => {
|
|
186
|
+
const handleSetConfig = (newData: Object[], useTempConfig = false, dataMetadata = {}) => {
|
|
183
187
|
const setDataURL = keepURL && fileSourceType === 'url'
|
|
184
188
|
if (config.type === 'dashboard') {
|
|
185
189
|
const dataFileFormat = mimeType.split('/')[1].toUpperCase()
|
|
186
190
|
const dataset = {
|
|
187
191
|
data: newData,
|
|
192
|
+
dataMetadata,
|
|
188
193
|
dataFileSize: fileSize,
|
|
189
194
|
dataFileName: fileSource, // new file source
|
|
190
195
|
dataFileSourceType: fileSourceType, // new file source type
|
|
@@ -206,13 +211,21 @@ const DataImport = () => {
|
|
|
206
211
|
payload: { datasetKey: newDatasetName || fileSource, dataset, oldDatasetKey }
|
|
207
212
|
})
|
|
208
213
|
} else {
|
|
214
|
+
const configWithAutoDetectedDateFormat = applyAutoDetectedDateParseFormat(
|
|
215
|
+
{
|
|
216
|
+
...config,
|
|
217
|
+
...tempConfig
|
|
218
|
+
},
|
|
219
|
+
newData as Record<string, unknown>[]
|
|
220
|
+
)
|
|
221
|
+
|
|
209
222
|
let newConfig = {
|
|
210
|
-
...
|
|
211
|
-
...tempConfig,
|
|
223
|
+
...configWithAutoDetectedDateFormat,
|
|
212
224
|
data: newData,
|
|
225
|
+
dataMetadata,
|
|
213
226
|
dataFileName: fileSource, // new file source
|
|
214
227
|
dataFileSourceType: fileSourceType, // new file source type
|
|
215
|
-
formattedData: transform.developerStandardize(newData,
|
|
228
|
+
formattedData: transform.developerStandardize(newData, configWithAutoDetectedDateFormat.dataDescription)
|
|
216
229
|
}
|
|
217
230
|
if (setDataURL) {
|
|
218
231
|
newConfig.dataUrl = fileSource
|
|
@@ -227,14 +240,16 @@ const DataImport = () => {
|
|
|
227
240
|
if (config.vegaConfig) {
|
|
228
241
|
return updateDataFromVegaData(result, fileSource, fileSourceType)
|
|
229
242
|
}
|
|
230
|
-
const
|
|
243
|
+
const { data: extractedData, dataMetadata } = extractDataAndMetadata(result)
|
|
244
|
+
const text = transform.autoStandardize(extractedData)
|
|
231
245
|
if (config.data && config.series) {
|
|
232
246
|
if (dataExists(text, config.series, config?.xAxis.dataKey)) {
|
|
233
|
-
handleSetConfig(text, true)
|
|
247
|
+
handleSetConfig(text, true, dataMetadata)
|
|
234
248
|
} else {
|
|
235
249
|
resetEditor(
|
|
236
250
|
{
|
|
237
251
|
data: text,
|
|
252
|
+
dataMetadata,
|
|
238
253
|
dataFileName: fileSource,
|
|
239
254
|
dataFileSourceType: fileSourceType
|
|
240
255
|
} as Visualization,
|
|
@@ -242,7 +257,7 @@ const DataImport = () => {
|
|
|
242
257
|
)
|
|
243
258
|
}
|
|
244
259
|
} else {
|
|
245
|
-
handleSetConfig(text)
|
|
260
|
+
handleSetConfig(text, false, dataMetadata)
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
if (editingDataset) {
|
|
@@ -354,7 +369,7 @@ const DataImport = () => {
|
|
|
354
369
|
Always load from URL (normally will only pull once)
|
|
355
370
|
</label>
|
|
356
371
|
<div className='d-flex justify-content-end mt-2 mb-3'>
|
|
357
|
-
<
|
|
372
|
+
<Button
|
|
358
373
|
className='btn btn-primary px-4'
|
|
359
374
|
type='submit'
|
|
360
375
|
id='load-data'
|
|
@@ -362,7 +377,7 @@ const DataImport = () => {
|
|
|
362
377
|
onClick={() => loadData(null, externalURL, editingDataset)}
|
|
363
378
|
>
|
|
364
379
|
Save & Load
|
|
365
|
-
</
|
|
380
|
+
</Button>
|
|
366
381
|
</div>
|
|
367
382
|
</>
|
|
368
383
|
)
|
|
@@ -383,7 +398,7 @@ const DataImport = () => {
|
|
|
383
398
|
return (
|
|
384
399
|
//todo convert to modal
|
|
385
400
|
<>
|
|
386
|
-
<
|
|
401
|
+
<Button
|
|
387
402
|
className='btn danger'
|
|
388
403
|
onClick={() =>
|
|
389
404
|
resetEditor(
|
|
@@ -394,7 +409,7 @@ const DataImport = () => {
|
|
|
394
409
|
>
|
|
395
410
|
Clear
|
|
396
411
|
<CloseIcon />
|
|
397
|
-
</
|
|
412
|
+
</Button>
|
|
398
413
|
{/* DEV-851 link to replace file should pop file dialog */}
|
|
399
414
|
{config.dataFileSourceType === 'file' && (
|
|
400
415
|
<div className='link link-replace' {...getRootProps2()}>
|
|
@@ -430,6 +445,49 @@ const DataImport = () => {
|
|
|
430
445
|
dispatch({ type: 'DELETE_DASHBOARD_DATASET', payload: { datasetKey } })
|
|
431
446
|
}
|
|
432
447
|
|
|
448
|
+
const downloadCSV = (data: Array<Record<string, unknown>>, filename: string) => {
|
|
449
|
+
// Normalize filename: strip query/hash, remove path, replace extension with .csv
|
|
450
|
+
let baseName = filename || ''
|
|
451
|
+
|
|
452
|
+
const queryIndex = baseName.indexOf('?')
|
|
453
|
+
const hashIndex = baseName.indexOf('#')
|
|
454
|
+
const cutoffIndex = queryIndex === -1 ? hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex)
|
|
455
|
+
if (cutoffIndex !== -1) {
|
|
456
|
+
baseName = baseName.substring(0, cutoffIndex)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const lastSlash = Math.max(baseName.lastIndexOf('/'), baseName.lastIndexOf('\\'))
|
|
460
|
+
if (lastSlash !== -1) {
|
|
461
|
+
baseName = baseName.substring(lastSlash + 1)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const lastDot = baseName.lastIndexOf('.')
|
|
465
|
+
if (lastDot > 0) {
|
|
466
|
+
baseName = baseName.substring(0, lastDot)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!baseName) {
|
|
470
|
+
baseName = 'data'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const name = `${baseName}.csv`
|
|
474
|
+
const blob = new Blob(['\uFEFF' + csvFormat(data)], { type: 'text/csv;charset=utf-8;' })
|
|
475
|
+
// @ts-ignore
|
|
476
|
+
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
477
|
+
// @ts-ignore
|
|
478
|
+
navigator.msSaveBlob(blob, name)
|
|
479
|
+
} else {
|
|
480
|
+
const url = URL.createObjectURL(blob)
|
|
481
|
+
const link = document.createElement('a')
|
|
482
|
+
link.href = url
|
|
483
|
+
link.download = name
|
|
484
|
+
document.body.appendChild(link)
|
|
485
|
+
link.click()
|
|
486
|
+
document.body.removeChild(link)
|
|
487
|
+
URL.revokeObjectURL(url)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
433
491
|
const updateDataFromVegaConfig = pastedConfig => {
|
|
434
492
|
const vegaConfig = parseVegaConfig(JSON.parse(pastedConfig))
|
|
435
493
|
const newData = extractCoveData(vegaConfig)
|
|
@@ -604,7 +662,7 @@ const DataImport = () => {
|
|
|
604
662
|
</fieldset>
|
|
605
663
|
)
|
|
606
664
|
)}
|
|
607
|
-
<
|
|
665
|
+
<Button
|
|
608
666
|
className='btn full-width btn-primary'
|
|
609
667
|
onClick={() => {
|
|
610
668
|
setConfig({
|
|
@@ -616,7 +674,7 @@ const DataImport = () => {
|
|
|
616
674
|
}}
|
|
617
675
|
>
|
|
618
676
|
Add New URL Filter
|
|
619
|
-
</
|
|
677
|
+
</Button>
|
|
620
678
|
</>
|
|
621
679
|
)
|
|
622
680
|
|
|
@@ -634,7 +692,7 @@ const DataImport = () => {
|
|
|
634
692
|
<th>Name</th>
|
|
635
693
|
<th>Size</th>
|
|
636
694
|
<th>Type</th>
|
|
637
|
-
<th colSpan={
|
|
695
|
+
<th colSpan={4}>Actions</th>
|
|
638
696
|
</tr>
|
|
639
697
|
</thead>
|
|
640
698
|
<tbody>
|
|
@@ -646,17 +704,33 @@ const DataImport = () => {
|
|
|
646
704
|
<td className='p-1'>{displaySize(config.datasets[datasetKey].dataFileSize)}</td>
|
|
647
705
|
<td className='p-1'>{config.datasets[datasetKey].dataFileFormat}</td>
|
|
648
706
|
<td>
|
|
649
|
-
<
|
|
650
|
-
|
|
707
|
+
<Button
|
|
708
|
+
variant='link'
|
|
709
|
+
className='p-1'
|
|
651
710
|
onClick={() => setGlobalDatasetProp(datasetKey, 'preview', true)}
|
|
652
711
|
>
|
|
653
712
|
Preview
|
|
713
|
+
</Button>
|
|
714
|
+
</td>
|
|
715
|
+
<td>
|
|
716
|
+
<button
|
|
717
|
+
className='btn btn-link p-1'
|
|
718
|
+
disabled={!config.datasets[datasetKey].data?.length}
|
|
719
|
+
onClick={() =>
|
|
720
|
+
downloadCSV(
|
|
721
|
+
config.datasets[datasetKey].data,
|
|
722
|
+
config.datasets[datasetKey].dataFileName || datasetKey
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
>
|
|
726
|
+
Download
|
|
654
727
|
</button>
|
|
655
728
|
</td>
|
|
656
729
|
<td>
|
|
657
730
|
{config.datasets[datasetKey].dataFileSourceType === 'url' && (
|
|
658
|
-
<
|
|
659
|
-
|
|
731
|
+
<Button
|
|
732
|
+
variant='link'
|
|
733
|
+
className='p-1'
|
|
660
734
|
onClick={() => {
|
|
661
735
|
if (editingDataset === datasetKey) {
|
|
662
736
|
setEditingDataset(undefined)
|
|
@@ -672,13 +746,13 @@ const DataImport = () => {
|
|
|
672
746
|
}}
|
|
673
747
|
>
|
|
674
748
|
Edit
|
|
675
|
-
</
|
|
749
|
+
</Button>
|
|
676
750
|
)}
|
|
677
751
|
</td>
|
|
678
752
|
<td>
|
|
679
|
-
<
|
|
753
|
+
<Button variant='danger' onClick={() => removeDataset(datasetKey)}>
|
|
680
754
|
X
|
|
681
|
-
</
|
|
755
|
+
</Button>
|
|
682
756
|
</td>
|
|
683
757
|
</tr>
|
|
684
758
|
)
|
|
@@ -714,6 +788,14 @@ const DataImport = () => {
|
|
|
714
788
|
)}
|
|
715
789
|
</div>
|
|
716
790
|
<div>{resetButton()}</div>
|
|
791
|
+
{config.data?.length > 0 && (
|
|
792
|
+
<button
|
|
793
|
+
className='btn btn-link p-1'
|
|
794
|
+
onClick={() => downloadCSV(config.data, config.dataFileName || 'data')}
|
|
795
|
+
>
|
|
796
|
+
Download CSV
|
|
797
|
+
</button>
|
|
798
|
+
)}
|
|
717
799
|
</div>
|
|
718
800
|
)}
|
|
719
801
|
|
|
@@ -722,6 +804,14 @@ const DataImport = () => {
|
|
|
722
804
|
<div className='url-source-options'>
|
|
723
805
|
<div>{loadDataFromUrl()}</div>
|
|
724
806
|
<div>{resetButton()}</div>
|
|
807
|
+
{config.data?.length > 0 && (
|
|
808
|
+
<button
|
|
809
|
+
className='btn btn-link p-1'
|
|
810
|
+
onClick={() => downloadCSV(config.data, config.dataFileName || 'data')}
|
|
811
|
+
>
|
|
812
|
+
Download CSV
|
|
813
|
+
</button>
|
|
814
|
+
)}
|
|
725
815
|
</div>
|
|
726
816
|
{config.dataUrl && (config.type === 'chart' || config.type === 'map') && urlFilters}
|
|
727
817
|
</>
|
|
@@ -745,15 +835,16 @@ const DataImport = () => {
|
|
|
745
835
|
placeholder='{ }'
|
|
746
836
|
/>
|
|
747
837
|
<div className='mb-3 d-flex justify-content-end'>
|
|
748
|
-
<
|
|
749
|
-
|
|
838
|
+
<Button
|
|
839
|
+
variant='primary'
|
|
840
|
+
className='px-4'
|
|
750
841
|
type='submit'
|
|
751
842
|
id='load-data'
|
|
752
843
|
disabled={!pastedConfig}
|
|
753
844
|
onClick={() => updateDataFromVegaConfig(pastedConfig)}
|
|
754
845
|
>
|
|
755
846
|
Save & Load
|
|
756
|
-
</
|
|
847
|
+
</Button>
|
|
757
848
|
</div>
|
|
758
849
|
</div>
|
|
759
850
|
</TabPane>
|
|
@@ -807,15 +898,16 @@ const DataImport = () => {
|
|
|
807
898
|
/>
|
|
808
899
|
</label>
|
|
809
900
|
<div className='d-flex justify-content-end mt-2 mb-3'>
|
|
810
|
-
<
|
|
811
|
-
|
|
901
|
+
<Button
|
|
902
|
+
variant='primary'
|
|
903
|
+
className='px-4'
|
|
812
904
|
type='submit'
|
|
813
905
|
id='load-data'
|
|
814
906
|
disabled={!newDatasetName || !externalURL}
|
|
815
907
|
onClick={() => loadData(null, externalURL, editingDataset)}
|
|
816
908
|
>
|
|
817
909
|
Save & Load
|
|
818
|
-
</
|
|
910
|
+
</Button>
|
|
819
911
|
</div>
|
|
820
912
|
</TabPane>
|
|
821
913
|
</Tabs>
|
|
@@ -890,20 +982,17 @@ const DataImport = () => {
|
|
|
890
982
|
|
|
891
983
|
{config.type === 'dashboard' && !addingDataset && (
|
|
892
984
|
<div className='mt-2'>
|
|
893
|
-
<
|
|
985
|
+
<Button variant='primary' onClick={() => setAddingDataset(true)}>
|
|
894
986
|
+ Add More Files
|
|
895
|
-
</
|
|
987
|
+
</Button>
|
|
896
988
|
</div>
|
|
897
989
|
)}
|
|
898
990
|
|
|
899
991
|
{readyToConfigure && (
|
|
900
992
|
<div className='mt-2'>
|
|
901
|
-
<
|
|
902
|
-
className='btn btn-primary'
|
|
903
|
-
onClick={() => dispatch({ type: 'EDITOR_SET_GLOBALACTIVE', payload: 2 })}
|
|
904
|
-
>
|
|
993
|
+
<Button variant='primary' onClick={() => dispatch({ type: 'EDITOR_SET_GLOBALACTIVE', payload: 2 })}>
|
|
905
994
|
Configure your visualization
|
|
906
|
-
</
|
|
995
|
+
</Button>
|
|
907
996
|
</div>
|
|
908
997
|
)}
|
|
909
998
|
|
|
@@ -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
|
+
})
|