@cdc/core 4.26.2 → 4.26.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 (186) hide show
  1. package/LICENSE +201 -0
  2. package/_stories/Gallery.Charts.stories.tsx +1 -1
  3. package/_stories/Gallery.DataBite.stories.tsx +1 -1
  4. package/_stories/Gallery.Maps.stories.tsx +1 -1
  5. package/_stories/PageART.stories.tsx +1 -1
  6. package/_stories/PageBRFSS.stories.tsx +1 -1
  7. package/_stories/PageCancerRegistries.stories.tsx +1 -1
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +3 -3
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +1 -1
  10. package/_stories/PageMaternalMortality.stories.tsx +1 -1
  11. package/_stories/PageOralHealth.stories.tsx +1 -1
  12. package/_stories/PageRespiratory.stories.tsx +4 -4
  13. package/_stories/PageSmokingTobacco.stories.tsx +1 -1
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +1 -1
  15. package/_stories/PageWastewater.stories.tsx +4 -4
  16. package/_stories/VegaImport.stories.tsx +3 -3
  17. package/assets/callout-flag.svg +7 -0
  18. package/components/AdvancedEditor/EmbedEditor.tsx +1 -1
  19. package/components/Alert/components/Alert.styles.css +2 -2
  20. package/components/ComboBox/combobox.styles.css +48 -48
  21. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  22. package/components/DataTable/DataTable.tsx +46 -18
  23. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  24. package/components/DataTable/components/ChartHeader.tsx +21 -12
  25. package/components/DataTable/components/MapHeader.tsx +34 -28
  26. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  27. package/components/DataTable/data-table.css +50 -52
  28. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  29. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  30. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  32. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  33. package/components/DownloadButton.tsx +14 -6
  34. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  35. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  36. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  37. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  38. package/components/EditorPanel/EditorPanel.tsx +3 -8
  39. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  40. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  41. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +7 -6
  42. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -10
  43. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  44. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  45. package/{styles/v2/components → components/EditorPanel}/editor.scss +67 -13
  46. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  47. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  48. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  49. package/components/Filters/Filters.tsx +3 -5
  50. package/components/Filters/components/Tabs.tsx +19 -7
  51. package/{styles → components/Filters}/filters.scss +3 -3
  52. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  54. package/components/Layout/components/Responsive.tsx +14 -6
  55. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +12 -18
  57. package/components/Layout/components/Visualization/index.tsx +39 -38
  58. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  59. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  60. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  61. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  62. package/components/Layout/components/VisualizationContent.tsx +75 -0
  63. package/components/Layout/index.tsx +5 -5
  64. package/components/Layout/styles/editor-utils.scss +3 -3
  65. package/components/Layout/styles/editor.scss +4 -4
  66. package/components/Legend/Legend.Gradient.tsx +7 -1
  67. package/components/Loader/loader.styles.css +2 -2
  68. package/components/Loading.jsx +1 -1
  69. package/components/MediaControls.tsx +10 -2
  70. package/components/MultiSelect/multiselect.styles.css +19 -19
  71. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  72. package/components/PaletteSelector/PaletteSelector.css +15 -15
  73. package/components/RichTooltip/richTooltip.css +6 -6
  74. package/components/Table/table.styles.css +2 -2
  75. package/components/Waiting.tsx +1 -1
  76. package/components/_stories/Filters.stories.tsx +1 -1
  77. package/components/_stories/styles.scss +0 -1
  78. package/components/elements/Button.jsx +1 -1
  79. package/components/elements/Card.jsx +1 -1
  80. package/{styles/v2/components → components/elements}/button.scss +9 -8
  81. package/components/inputs/InputCheckbox.jsx +1 -1
  82. package/components/inputs/InputSelect.tsx +1 -1
  83. package/components/inputs/InputText.jsx +1 -1
  84. package/components/inputs/InputToggle.tsx +1 -1
  85. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  86. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  87. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  88. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  89. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  90. package/{styles → components}/loading.scss +1 -1
  91. package/components/managers/DataDesigner.tsx +1 -1
  92. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  93. package/components/ui/Accordion.jsx +1 -1
  94. package/components/ui/Icon.tsx +1 -1
  95. package/components/ui/LoadSpin.jsx +1 -1
  96. package/components/ui/Modal.jsx +1 -1
  97. package/components/ui/Overlay.jsx +1 -1
  98. package/components/ui/Title/index.test.tsx +34 -0
  99. package/components/ui/Title/index.tsx +24 -7
  100. package/components/ui/Title/title.styles.css +119 -25
  101. package/components/ui/Tooltip.tsx +1 -1
  102. package/components/ui/_stories/Title.stories.tsx +1 -1
  103. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  104. package/components/ui/accordion.styles.css +11 -11
  105. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  106. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  107. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  108. package/{styles → components}/waiting.scss +9 -3
  109. package/devTemplate/dev.js +50 -0
  110. package/dist/cove-main.css +528 -231
  111. package/dist/cove-main.css.map +1 -1
  112. package/generateViteConfig.js +2 -2
  113. package/helpers/backfillDefaults.ts +35 -0
  114. package/helpers/constants.ts +12 -0
  115. package/helpers/cove/date.ts +32 -3
  116. package/helpers/cove/number.ts +29 -15
  117. package/helpers/coveUpdateWorker.ts +12 -8
  118. package/helpers/displayDataAsText.ts +1 -1
  119. package/helpers/embed/embedHelper.js +13 -2
  120. package/helpers/embed/index.ts +0 -4
  121. package/helpers/extractDataAndMetadata.ts +20 -0
  122. package/helpers/fetchRemoteData.ts +14 -8
  123. package/helpers/labelHash.ts +9 -0
  124. package/helpers/markupProcessor.ts +56 -38
  125. package/helpers/prepareScreenshot.ts +6 -3
  126. package/helpers/testing.ts +1 -1
  127. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  128. package/helpers/tests/backfillDefaults.test.ts +253 -0
  129. package/helpers/tests/date.test.ts +46 -0
  130. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  131. package/helpers/tests/markupProcessor.test.ts +315 -124
  132. package/helpers/tests/number.test.ts +42 -0
  133. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  134. package/helpers/tests/testStandaloneBuild.ts +36 -26
  135. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  136. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  137. package/helpers/useDataVizClasses.ts +13 -7
  138. package/helpers/ver/4.24.4.ts +24 -0
  139. package/helpers/ver/4.26.3.ts +44 -0
  140. package/helpers/ver/4.26.4.ts +31 -0
  141. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  142. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  143. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  144. package/package.json +2 -2
  145. package/styles/_global.scss +7 -7
  146. package/styles/_reset.scss +2 -2
  147. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  148. package/styles/{v2/base → base}/_general.scss +2 -4
  149. package/styles/{v2/base → base}/index.scss +1 -1
  150. package/styles/base.scss +107 -165
  151. package/styles/cove-main.scss +3 -6
  152. package/styles/layout/_component.scss +110 -0
  153. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  154. package/styles/layout/_wrapper-padding.scss +27 -0
  155. package/styles/{v2/main.scss → main.scss} +3 -1
  156. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  157. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  158. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  159. package/styles/{v2/utils → utils}/index.scss +2 -1
  160. package/types/Axis.ts +2 -0
  161. package/types/ComponentStyles.ts +1 -0
  162. package/types/ConfigureData.ts +1 -0
  163. package/types/MarkupInclude.ts +1 -0
  164. package/types/MarkupVariable.ts +2 -1
  165. package/types/Palette.ts +1 -0
  166. package/types/Table.ts +9 -0
  167. package/types/Visualization.ts +1 -0
  168. package/styles/_common-components.css +0 -73
  169. package/styles/_variables.scss +0 -63
  170. package/styles/v2/layout/_component.scss +0 -21
  171. package/styles/v2/utils/_variables.scss +0 -9
  172. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  173. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  174. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  175. /package/styles/{v2/base → base}/_heading.scss +0 -0
  176. /package/styles/{v2/base → base}/_reset.scss +0 -0
  177. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  178. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  179. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  180. /package/styles/{v2/layout → layout}/index.scss +0 -0
  181. /package/styles/{v2/themes → themes}/index.scss +0 -0
  182. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  183. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  184. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  185. /package/styles/{v2/utils → utils}/_grid.scss +0 -0
  186. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -80,7 +80,7 @@ function findNearestHeadingIndex(parentChildren: Element[], vizWrapperIndex: num
80
80
  const child = parentChildren[i] as HTMLElement
81
81
 
82
82
  // STOP: Another visualization found - don't include its content
83
- if (child.classList.contains('cdc-open-viz-module') || child.querySelector('.cdc-open-viz-module')) {
83
+ if (child.classList.contains('cove-visualization') || child.querySelector('.cove-visualization')) {
84
84
  return -1
85
85
  }
86
86
 
@@ -135,7 +135,7 @@ function buildContextClone(
135
135
  }
136
136
 
137
137
  function isInEditorMode(element: HTMLElement): boolean {
138
- return element.closest('.cdc-open-viz-module.isEditor') !== null
138
+ return element.closest('.cove-visualization.is-editor') !== null
139
139
  }
140
140
 
141
141
  /**
@@ -239,7 +239,10 @@ function expandSvgWidths(clonedViz: HTMLElement): void {
239
239
  }
240
240
 
241
241
  // Remove animation classes to show final state immediately
242
- svg.classList.remove('animated', 'animate')
242
+ const isAnimatedPie = svg.classList.contains('animated-pie')
243
+ if (!isAnimatedPie) {
244
+ svg.classList.remove('animated', 'animate')
245
+ }
243
246
  })
244
247
  }
245
248
 
@@ -343,7 +343,7 @@ export const assertVisualizationRendered = async (vizElement: HTMLElement) => {
343
343
  () => {
344
344
  const svgCount = vizElement.querySelectorAll('svg').length
345
345
  const canvasCount = vizElement.querySelectorAll('canvas').length
346
- const hasCoveModule = !!vizElement.querySelector('.cdc-open-viz-module')
346
+ const hasCoveModule = !!vizElement.querySelector('.cove-visualization')
347
347
  const isDataBite = !!vizElement.querySelector('.bite-content')
348
348
  const isDataTable = !!vizElement.querySelector('.type-data-table')
349
349
  return { svgCount, canvasCount, hasCoveModule, isDataBite, isDataTable }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { abbreviateNumber } from '../cove/number'
3
+
4
+ describe('abbreviateNumber', () => {
5
+ describe('default (English) abbreviations', () => {
6
+ it('abbreviates thousands with K', () => {
7
+ expect(abbreviateNumber(1500)).toBe('1.5K')
8
+ })
9
+
10
+ it('abbreviates millions with M', () => {
11
+ expect(abbreviateNumber(2500000)).toBe('2.5M')
12
+ })
13
+
14
+ it('abbreviates billions with B', () => {
15
+ expect(abbreviateNumber(3000000000)).toBe('3B')
16
+ })
17
+
18
+ it('does not abbreviate numbers below 1000', () => {
19
+ expect(abbreviateNumber(999)).toBe('999')
20
+ })
21
+
22
+ it('handles negative numbers', () => {
23
+ expect(abbreviateNumber(-5000)).toBe('-5K')
24
+ })
25
+ })
26
+
27
+ describe('en-US locale abbreviations', () => {
28
+ it('uses K/M/B for en-US', () => {
29
+ expect(abbreviateNumber(1500, 'en-US')).toBe('1.5K')
30
+ expect(abbreviateNumber(2500000, 'en-US')).toBe('2.5M')
31
+ expect(abbreviateNumber(3000000000, 'en-US')).toBe('3B')
32
+ })
33
+ })
34
+
35
+ describe('es-MX locale abbreviations', () => {
36
+ it('uses " mil" for thousands in Spanish', () => {
37
+ expect(abbreviateNumber(1500, 'es-MX')).toBe('1.5 mil')
38
+ })
39
+
40
+ it('uses " M" for millions in Spanish', () => {
41
+ expect(abbreviateNumber(2500000, 'es-MX')).toBe('2.5 M')
42
+ })
43
+
44
+ it('uses " mil M" for billions in Spanish', () => {
45
+ expect(abbreviateNumber(3000000000, 'es-MX')).toBe('3 mil M')
46
+ })
47
+
48
+ it('does not abbreviate numbers below 1000 in Spanish', () => {
49
+ expect(abbreviateNumber(999, 'es-MX')).toBe('999')
50
+ })
51
+ })
52
+
53
+ describe('unknown locale', () => {
54
+ it('falls back to English K/M/B for unknown locale', () => {
55
+ expect(abbreviateNumber(1500, 'xx-XX')).toBe('1.5K')
56
+ expect(abbreviateNumber(2500000, 'xx-XX')).toBe('2.5M')
57
+ })
58
+ })
59
+ })
@@ -0,0 +1,253 @@
1
+ import { backfillDefaults } from '../backfillDefaults'
2
+ import { expect, describe, it } from 'vitest'
3
+ import { LEGACY_CHART_DEFAULTS } from '@cdc/chart/src/data/legacy-defaults'
4
+ import { LEGACY_MAP_DEFAULTS } from '@cdc/map/src/data/legacy-defaults'
5
+ import chartDefaults from '@cdc/chart/src/data/initial-state'
6
+ import mapDefaults from '@cdc/map/src/data/initial-state'
7
+
8
+ // ============================================================================
9
+ // backfillDefaults — pure function tests
10
+ // ============================================================================
11
+
12
+ describe('backfillDefaults', () => {
13
+ it('fills missing properties from defaults into config', () => {
14
+ const config = { yAxis: { hideLabel: false } }
15
+ const defaults = { yAxis: { hideLabel: true, gridLines: true, numTicks: 4 } }
16
+ backfillDefaults(config, defaults)
17
+ expect(config.yAxis.gridLines).toBe(true)
18
+ expect(config.yAxis.numTicks).toBe(4)
19
+ })
20
+
21
+ it('preserves existing config values including falsy ones', () => {
22
+ const config = {
23
+ yAxis: { hideAxis: false, numTicks: '', gridLines: false },
24
+ dataFormat: { commas: false },
25
+ table: { expanded: 0 }
26
+ }
27
+ const defaults = {
28
+ yAxis: { hideAxis: true, numTicks: 4, gridLines: true },
29
+ dataFormat: { commas: true },
30
+ table: { expanded: false }
31
+ }
32
+ backfillDefaults(config, defaults)
33
+ expect(config.yAxis.hideAxis).toBe(false)
34
+ expect(config.yAxis.numTicks).toBe('')
35
+ expect(config.yAxis.gridLines).toBe(false)
36
+ expect(config.dataFormat.commas).toBe(false)
37
+ expect(config.table.expanded).toBe(0)
38
+ })
39
+
40
+ it('preserves null config values (does not overwrite with default)', () => {
41
+ const config = { yAxis: { numTicks: null } }
42
+ const defaults = { yAxis: { numTicks: 4 } }
43
+ backfillDefaults(config, defaults)
44
+ expect(config.yAxis.numTicks).toBeNull()
45
+ })
46
+
47
+ it('skips arrays and primitive top-level keys', () => {
48
+ const config = { filters: [], title: 'My Chart', yAxis: { hideAxis: false } } as any
49
+ const defaults = { filters: [{ name: 'default' }], title: 'Default', yAxis: { hideAxis: true, numTicks: 4 } }
50
+ backfillDefaults(config, defaults)
51
+ expect(config.filters).toEqual([])
52
+ expect(config.title).toBe('My Chart')
53
+ expect(config.yAxis.numTicks).toBe(4)
54
+ })
55
+
56
+ it('skips top-level keys that exist in defaults but not in config', () => {
57
+ const config = { yAxis: { hideAxis: false } } as any
58
+ const defaults = { yAxis: { hideAxis: true }, legend: { position: 'top' } }
59
+ backfillDefaults(config, defaults)
60
+ expect(config.legend).toBeUndefined()
61
+ })
62
+
63
+ it('legacy changed-value entries override new defaults for missing properties', () => {
64
+ const config = { yAxis: {} } as any
65
+ const defaults = { yAxis: { numTicks: 6 } }
66
+ const legacy = { yAxis: { numTicks: '' } }
67
+ backfillDefaults(config, defaults, legacy)
68
+ expect(config.yAxis.numTicks).toBe('')
69
+ })
70
+
71
+ it('legacy undefined entries prevent backfill entirely', () => {
72
+ const config = { xAxis: {} } as any
73
+ const defaults = { xAxis: { dateDisplayFormat: '%b. %-d %Y' } }
74
+ const legacy = { xAxis: { dateDisplayFormat: undefined } }
75
+ backfillDefaults(config, defaults, legacy)
76
+ expect(config.xAxis.dateDisplayFormat).toBeUndefined()
77
+ expect('dateDisplayFormat' in config.xAxis).toBe(false)
78
+ })
79
+
80
+ it('legacy is ignored when config already has the property', () => {
81
+ const config = { yAxis: { numTicks: 10 } }
82
+ const defaults = { yAxis: { numTicks: 6 } }
83
+ const legacy = { yAxis: { numTicks: '' } }
84
+ backfillDefaults(config, defaults, legacy)
85
+ expect(config.yAxis.numTicks).toBe(10)
86
+ })
87
+
88
+ it('no legacy arg falls back to defaults normally', () => {
89
+ const config = { yAxis: {} } as any
90
+ const defaults = { yAxis: { numTicks: 6 } }
91
+ backfillDefaults(config, defaults)
92
+ expect(config.yAxis.numTicks).toBe(6)
93
+ })
94
+
95
+ it('empty legacy object falls back to defaults normally', () => {
96
+ const config = { yAxis: {} } as any
97
+ const defaults = { yAxis: { numTicks: 6 } }
98
+ backfillDefaults(config, defaults, {})
99
+ expect(config.yAxis.numTicks).toBe(6)
100
+ })
101
+
102
+ it('works with multiple sections in a single call', () => {
103
+ const config = { yAxis: {}, xAxis: {}, legend: {} } as any
104
+ const defaults = {
105
+ yAxis: { numTicks: 4 },
106
+ xAxis: { numTicks: 6 },
107
+ legend: { position: 'top' }
108
+ }
109
+ backfillDefaults(config, defaults)
110
+ expect(config.yAxis.numTicks).toBe(4)
111
+ expect(config.xAxis.numTicks).toBe(6)
112
+ expect(config.legend.position).toBe('top')
113
+ })
114
+ })
115
+
116
+ // ============================================================================
117
+ // Chart legacy defaults protection
118
+ // ============================================================================
119
+
120
+ describe('chart legacy defaults protection', () => {
121
+ it('old config with yAxis.numTicks: "" keeps "" (not replaced with 4)', () => {
122
+ const config = { yAxis: { numTicks: '' } }
123
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
124
+ expect(config.yAxis.numTicks).toBe('')
125
+ })
126
+
127
+ it('old config with yAxis.hideAxis: false keeps false (not replaced with true)', () => {
128
+ const config = { yAxis: { hideAxis: false } }
129
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
130
+ expect(config.yAxis.hideAxis).toBe(false)
131
+ })
132
+
133
+ it('old config with yAxis.gridLines: false keeps false (not replaced with true)', () => {
134
+ const config = { yAxis: { gridLines: false } }
135
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
136
+ expect(config.yAxis.gridLines).toBe(false)
137
+ })
138
+
139
+ it('old config with yAxis.hideTicks: false keeps false (not replaced with true)', () => {
140
+ const config = { yAxis: { hideTicks: false } }
141
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
142
+ expect(config.yAxis.hideTicks).toBe(false)
143
+ })
144
+
145
+ it('old config with xAxis.numTicks: "" keeps "" (not replaced with 6)', () => {
146
+ const config = { xAxis: { numTicks: '' } }
147
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
148
+ expect(config.xAxis.numTicks).toBe('')
149
+ })
150
+
151
+ it('old config missing xAxis.viewportNumTicks does NOT get { xs: 4, xxs: 4 }', () => {
152
+ const config = { xAxis: { numTicks: '' } } as any
153
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
154
+ expect(config.xAxis.viewportNumTicks).toBeUndefined()
155
+ })
156
+
157
+ it('old config missing xAxis.dateDisplayFormat does NOT get the new default', () => {
158
+ const config = { xAxis: { numTicks: '' } } as any
159
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
160
+ expect(config.xAxis.dateDisplayFormat).toBeUndefined()
161
+ })
162
+
163
+ it('old config with table.expanded: true keeps true (not replaced with false)', () => {
164
+ const config = { table: { expanded: true } }
165
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
166
+ expect(config.table.expanded).toBe(true)
167
+ })
168
+
169
+ it('old config with legend.position: "right" keeps "right" (not replaced with "top")', () => {
170
+ const config = { legend: { position: 'right' } }
171
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
172
+ expect(config.legend.position).toBe('right')
173
+ })
174
+
175
+ it('old config with dataFormat.commas: false keeps false (not replaced with true)', () => {
176
+ const config = { dataFormat: { commas: false } }
177
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
178
+ expect(config.dataFormat.commas).toBe(false)
179
+ })
180
+
181
+ it('old config with tooltips.dateDisplayFormat: "" keeps "" (not replaced with new format)', () => {
182
+ const config = { tooltips: { dateDisplayFormat: '' } }
183
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
184
+ expect(config.tooltips.dateDisplayFormat).toBe('')
185
+ })
186
+
187
+ it('new config (no properties set) gets all the new defaults correctly', () => {
188
+ const config = {
189
+ yAxis: {},
190
+ xAxis: {},
191
+ table: {},
192
+ legend: {},
193
+ dataFormat: {},
194
+ tooltips: {},
195
+ general: {}
196
+ } as any
197
+ backfillDefaults(config, chartDefaults, LEGACY_CHART_DEFAULTS)
198
+
199
+ expect(config.yAxis.hideAxis).toBe(false)
200
+ expect(config.yAxis.hideTicks).toBe(false)
201
+ expect(config.yAxis.gridLines).toBe(false)
202
+ expect(config.yAxis.numTicks).toBe('')
203
+ expect(config.table.expanded).toBe(true)
204
+ expect(config.table.dateDisplayFormat).toBe('')
205
+ expect(config.legend.position).toBe('right')
206
+ expect(config.dataFormat.commas).toBe(false)
207
+ expect(config.tooltips.dateDisplayFormat).toBe('')
208
+ expect(config.xAxis.numTicks).toBe('')
209
+ // New properties should NOT be backfilled for old configs
210
+ expect(config.xAxis.dateDisplayFormat).toBeUndefined()
211
+ expect(config.xAxis.viewportNumTicks).toBeUndefined()
212
+ expect(config.general.useIntelligentLineChartLabels).toBeUndefined()
213
+ })
214
+ })
215
+
216
+ // ============================================================================
217
+ // Map legacy defaults protection
218
+ // ============================================================================
219
+
220
+ describe('map legacy defaults protection', () => {
221
+ it('old config with legend.style: "circles" keeps "circles" (not replaced with "gradient")', () => {
222
+ const config = { legend: { style: 'circles' } }
223
+ backfillDefaults(config, mapDefaults, LEGACY_MAP_DEFAULTS)
224
+ expect(config.legend.style).toBe('circles')
225
+ })
226
+
227
+ it('old config with legend.position: "side" keeps "side" (not replaced with "top")', () => {
228
+ const config = { legend: { position: 'side' } }
229
+ backfillDefaults(config, mapDefaults, LEGACY_MAP_DEFAULTS)
230
+ expect(config.legend.position).toBe('side')
231
+ })
232
+
233
+ it('old config with legend.numberOfItems: 3 keeps 3 (not replaced with 5)', () => {
234
+ const config = { legend: { numberOfItems: 3 } }
235
+ backfillDefaults(config, mapDefaults, LEGACY_MAP_DEFAULTS)
236
+ expect(config.legend.numberOfItems).toBe(3)
237
+ })
238
+
239
+ it('old config with legend.hideBorder: false keeps false (not replaced with true)', () => {
240
+ const config = { legend: { hideBorder: false } }
241
+ backfillDefaults(config, mapDefaults, LEGACY_MAP_DEFAULTS)
242
+ expect(config.legend.hideBorder).toBe(false)
243
+ })
244
+
245
+ it('old config missing one property gets the legacy value, not the new default', () => {
246
+ const config = { legend: { style: 'circles' } } as any
247
+ backfillDefaults(config, mapDefaults, LEGACY_MAP_DEFAULTS)
248
+ expect(config.legend.style).toBe('circles')
249
+ expect(config.legend.position).toBe('side')
250
+ expect(config.legend.numberOfItems).toBe(3)
251
+ expect(config.legend.hideBorder).toBe(false)
252
+ })
253
+ })
@@ -61,4 +61,50 @@ describe('formatDate', () => {
61
61
  const result = formatDate('%b. %-d, %Y', date)
62
62
  expect(result).toContain('Jan.')
63
63
  })
64
+
65
+ describe('locale support', () => {
66
+ it('formats month names in Spanish when locale is es-MX', () => {
67
+ const date = new Date(2025, 0, 15) // Jan 15, 2025
68
+ const result = formatDate('%B %-d, %Y', date, 'es-MX')
69
+ expect(result).toContain('enero')
70
+ })
71
+
72
+ it('formats abbreviated month names in Spanish when locale is es-MX', () => {
73
+ const date = new Date(2025, 2, 10) // Mar 10, 2025
74
+ const result = formatDate('%b %Y', date, 'es-MX')
75
+ expect(result).toContain('mar')
76
+ })
77
+
78
+ it('formats day names in Spanish when locale is es-MX', () => {
79
+ const date = new Date(2025, 0, 13) // Monday, Jan 13, 2025
80
+ const result = formatDate('%A', date, 'es-MX')
81
+ expect(result).toContain('lunes')
82
+ })
83
+
84
+ it('uses English formatting when locale is en-US', () => {
85
+ const date = new Date(2025, 0, 15)
86
+ const result = formatDate('%B %-d, %Y', date, 'en-US')
87
+ expect(result).toContain('January')
88
+ })
89
+
90
+ it('falls back to default English when locale is unknown', () => {
91
+ const date = new Date(2025, 0, 15)
92
+ const result = formatDate('%B %-d, %Y', date, 'xx-XX')
93
+ expect(result).toContain('January')
94
+ })
95
+
96
+ it('falls back to default English when locale is undefined', () => {
97
+ const date = new Date(2025, 0, 15)
98
+ const result = formatDate('%B %-d, %Y', date, undefined)
99
+ expect(result).toContain('January')
100
+ })
101
+
102
+ it('formats numeric-only patterns identically regardless of locale', () => {
103
+ const date = new Date(2025, 0, 15)
104
+ const enResult = formatDate('%Y-%m-%d', date, 'en-US')
105
+ const esResult = formatDate('%Y-%m-%d', date, 'es-MX')
106
+ expect(enResult).toBe('2025-01-15')
107
+ expect(esResult).toBe('2025-01-15')
108
+ })
109
+ })
64
110
  })
@@ -0,0 +1,93 @@
1
+ import { extractDataAndMetadata } from '../extractDataAndMetadata'
2
+ import { expect, describe, it } from 'vitest'
3
+
4
+ describe('extractDataAndMetadata', () => {
5
+ it('should return plain array input as data with empty metadata', () => {
6
+ const input = [{ state: 'CA' }, { state: 'TX' }]
7
+ const result = extractDataAndMetadata(input)
8
+
9
+ expect(result.data).toEqual(input)
10
+ expect(result.dataMetadata).toEqual({})
11
+ })
12
+
13
+ it('should extract data array and metadata siblings from wrapper object', () => {
14
+ const input = {
15
+ lastUpdated: 'January 15, 2026',
16
+ source: 'CDC NREVSS',
17
+ data: [{ state: 'CA' }, { state: 'TX' }]
18
+ }
19
+ const result = extractDataAndMetadata(input)
20
+
21
+ expect(result.data).toEqual([{ state: 'CA' }, { state: 'TX' }])
22
+ expect(result.dataMetadata).toEqual({
23
+ lastUpdated: 'January 15, 2026',
24
+ source: 'CDC NREVSS'
25
+ })
26
+ })
27
+
28
+ it('should handle wrapper with multiple metadata keys', () => {
29
+ const input = {
30
+ lastUpdated: '2026-01-15',
31
+ source: 'CDC',
32
+ reportingPeriod: 'Q1 2026',
33
+ data: [{ value: '100' }]
34
+ }
35
+ const result = extractDataAndMetadata(input)
36
+
37
+ expect(result.dataMetadata).toEqual({
38
+ lastUpdated: '2026-01-15',
39
+ source: 'CDC',
40
+ reportingPeriod: 'Q1 2026'
41
+ })
42
+ expect(result.data).toEqual([{ value: '100' }])
43
+ })
44
+
45
+ it('should handle wrapper with empty data array', () => {
46
+ const input = {
47
+ lastUpdated: '2026-01-15',
48
+ source: 'CDC',
49
+ data: []
50
+ }
51
+ const result = extractDataAndMetadata(input)
52
+
53
+ expect(result.data).toEqual([])
54
+ expect(result.dataMetadata).toEqual({
55
+ lastUpdated: '2026-01-15',
56
+ source: 'CDC'
57
+ })
58
+ })
59
+
60
+ it('should return non-array non-object input as data with empty metadata', () => {
61
+ expect(extractDataAndMetadata('hello')).toEqual({ data: 'hello', dataMetadata: {} })
62
+ expect(extractDataAndMetadata(42)).toEqual({ data: 42, dataMetadata: {} })
63
+ expect(extractDataAndMetadata(null)).toEqual({ data: null, dataMetadata: {} })
64
+ expect(extractDataAndMetadata(undefined)).toEqual({ data: undefined, dataMetadata: {} })
65
+ })
66
+
67
+ it('should return object without data property as data with empty metadata', () => {
68
+ const input = { foo: 'bar', baz: 123 }
69
+ const result = extractDataAndMetadata(input)
70
+
71
+ expect(result.data).toEqual(input)
72
+ expect(result.dataMetadata).toEqual({})
73
+ })
74
+
75
+ it('should return object where data is not an array as data with empty metadata', () => {
76
+ const input = { data: 'string value', source: 'CDC' }
77
+ const result = extractDataAndMetadata(input)
78
+
79
+ expect(result.data).toEqual(input)
80
+ expect(result.dataMetadata).toEqual({})
81
+ })
82
+
83
+ it('should pass through existing plain-array format unchanged (backward compatibility)', () => {
84
+ const legacyData = [
85
+ { state: 'California', population: '39538223' },
86
+ { state: 'Texas', population: '29145505' }
87
+ ]
88
+ const result = extractDataAndMetadata(legacyData)
89
+
90
+ expect(result.data).toBe(legacyData)
91
+ expect(result.dataMetadata).toEqual({})
92
+ })
93
+ })