@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.
Files changed (35) hide show
  1. package/dist/cdceditor-CY9IcPSi.es.js +6 -0
  2. package/dist/cdceditor-DlpiY3fQ.es.js +4 -0
  3. package/dist/cdceditor.js +78238 -74548
  4. package/example/private/dashboard-filter-issue/dashboard-filter-issue.json +957 -0
  5. package/package.json +9 -9
  6. package/src/_stories/Editor.stories.tsx +124 -0
  7. package/src/assets.d.ts +4 -0
  8. package/src/components/ChooseTab.test.tsx +64 -1
  9. package/src/components/ChooseTab.tsx +82 -4
  10. package/src/components/DataImport/components/DataImport.tsx +135 -50
  11. package/src/components/DataImport/components/SampleData.test.tsx +16 -0
  12. package/src/components/DataImport/components/SampleData.tsx +6 -0
  13. package/src/components/DataImport/components/samples/valid-heatmap-varicella-cases.csv +13 -0
  14. package/src/components/DataImport/helpers/applyAutoDetectedDateParseFormat.ts +35 -0
  15. package/src/components/DataImport/tests/applyAutoDetectedDateParseFormat.test.ts +128 -0
  16. package/src/components/PreviewDataTable.test.tsx +184 -0
  17. package/src/components/PreviewDataTable.tsx +18 -7
  18. package/src/components/modal/Confirmation.jsx +5 -4
  19. package/src/scss/choose-vis-tab.scss +7 -1
  20. package/src/scss/main.scss +14 -6
  21. package/dist/cdceditor-vr9HZwRt.es.js +0 -6
  22. package/example/data-horizontal-filters.json +0 -8
  23. package/example/data-horizontal-multiseries-filters.json +0 -18
  24. package/example/data-horizontal-multiseries.json +0 -6
  25. package/example/data-horizontal.json +0 -4
  26. package/example/data-vertical-filters.json +0 -10
  27. package/example/data-vertical-multiseries-filters.json +0 -18
  28. package/example/data-vertical-multiseries-multirow-filters.json +0 -50
  29. package/example/data-vertical-multiseries-multirow.json +0 -14
  30. package/example/data-vertical-multiseries.json +0 -6
  31. package/example/data-vertical.json +0 -6
  32. package/example/region-map.json +0 -33
  33. package/example/test.json +0 -110280
  34. package/example/valid-county-data.json +0 -3049
  35. 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 axios from 'axios'
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 axios
103
- .get(dataURL.toString(), {
104
- responseType: 'blob'
105
- })
106
- .then(response => {
107
- responseBlob = response.data
108
-
109
- // Sometimes the files are coming in as plain text types... Maybe when saved from Macs
110
- const csvTypes = ['text/csv', 'text/plain']
111
- if ((fileExtension === '.csv' && csvTypes.includes(responseBlob.type)) || isSolrCsv(externalURL)) {
112
- responseBlob = responseBlob.slice(0, responseBlob.size, 'text/csv')
113
- } else if (
114
- responseBlob.type === 'application/json' ||
115
- (fileExtension === '.json' && responseBlob.type === 'text/plain') ||
116
- isSolrJson(externalURL)
117
- ) {
118
- responseBlob = responseBlob.slice(0, responseBlob.size, 'application/json')
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
- ...config,
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, config.dataDescription)
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
- <button
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
- </button>
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
- <button
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
- </button>
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
- <button
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
- </button>
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={3}>Actions</th>
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
- <button
655
- className='btn btn-link p-1'
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
- <button
664
- className='btn btn-link p-1'
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
- </button>
750
+ </Button>
681
751
  )}
682
752
  </td>
683
753
  <td>
684
- <button className='btn btn-danger' onClick={() => removeDataset(datasetKey)}>
754
+ <Button variant='danger' onClick={() => removeDataset(datasetKey)}>
685
755
  X
686
- </button>
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
- <button
754
- className='btn btn-primary px-4'
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
- </button>
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
- <button
816
- className='btn btn-primary px-4'
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
- </button>
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
- <button className='btn btn-primary' onClick={() => setAddingDataset(true)}>
986
+ <Button variant='primary' onClick={() => setAddingDataset(true)}>
899
987
  + Add More Files
900
- </button>
988
+ </Button>
901
989
  </div>
902
990
  )}
903
991
 
904
992
  {readyToConfigure && (
905
993
  <div className='mt-2'>
906
- <button
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
- </button>
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
+ })