@cdc/core 4.26.1 → 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 (249) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/LICENSE +201 -0
  4. package/_stories/Gallery.Charts.stories.tsx +35 -42
  5. package/_stories/Gallery.DataBite.stories.tsx +15 -8
  6. package/_stories/Gallery.Maps.stories.tsx +37 -28
  7. package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
  8. package/_stories/PageART.stories.tsx +5 -4
  9. package/_stories/PageBRFSS.stories.tsx +21 -16
  10. package/_stories/PageCancerRegistries.stories.tsx +15 -15
  11. package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
  12. package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
  13. package/_stories/PageMaternalMortality.stories.tsx +5 -4
  14. package/_stories/PageOralHealth.stories.tsx +15 -10
  15. package/_stories/PageRespiratory.stories.tsx +4 -4
  16. package/_stories/PageSmokingTobacco.stories.tsx +15 -10
  17. package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
  18. package/_stories/PageWastewater.stories.tsx +44 -30
  19. package/_stories/VegaImport.stories.tsx +401 -0
  20. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  21. package/_stories/vega-fixtures/bars.json +58 -0
  22. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  23. package/_stories/vega-fixtures/combo.json +68 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  25. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  26. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  27. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  28. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  29. package/_stories/vega-fixtures/lines.json +227 -0
  30. package/_stories/vega-fixtures/measles-bars.json +348 -0
  31. package/_stories/vega-fixtures/measles-map.json +11101 -0
  32. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  33. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  34. package/_stories/vega-fixtures/no-data.json +14 -0
  35. package/_stories/vega-fixtures/pie-chart.json +94 -0
  36. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  37. package/_stories/vega-fixtures/stacked-area.json +222 -0
  38. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  39. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  40. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  41. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  42. package/_stories/vega-fixtures/warning-combo.json +59 -0
  43. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  44. package/assets/callout-flag.svg +7 -0
  45. package/assets/icon-chart-area.svg +1 -0
  46. package/assets/icon-chart-radar.svg +23 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
  49. package/components/Alert/components/Alert.styles.css +2 -2
  50. package/components/ComboBox/combobox.styles.css +48 -48
  51. package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
  52. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  53. package/components/DataTable/DataTable.tsx +46 -18
  54. package/components/DataTable/DataTableStandAlone.tsx +1 -0
  55. package/components/DataTable/components/ChartHeader.tsx +21 -12
  56. package/components/DataTable/components/MapHeader.tsx +34 -28
  57. package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
  58. package/components/DataTable/data-table.css +50 -52
  59. package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
  60. package/components/DataTable/helpers/getChartCellValue.ts +10 -7
  61. package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
  62. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  63. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
  64. package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
  65. package/components/DownloadButton.tsx +14 -6
  66. package/components/EditorPanel/ColumnsEditor.tsx +38 -31
  67. package/components/EditorPanel/CustomSortOrder.tsx +94 -0
  68. package/components/EditorPanel/DataTableEditor.tsx +139 -23
  69. package/components/EditorPanel/EditorPanel.styles.css +71 -71
  70. package/components/EditorPanel/EditorPanel.tsx +3 -8
  71. package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
  72. package/components/EditorPanel/FootnotesEditor.tsx +2 -2
  73. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
  74. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
  75. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  76. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
  77. package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
  78. package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
  79. package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
  80. package/components/EditorPanel/sections/VisualSection.tsx +11 -0
  81. package/components/EditorWrapper/editor-wrapper.style.css +1 -1
  82. package/components/Filters/Filters.tsx +3 -5
  83. package/components/Filters/components/Tabs.tsx +19 -7
  84. package/{styles → components/Filters}/filters.scss +3 -3
  85. package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
  86. package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
  87. package/components/Layout/components/Responsive.tsx +14 -6
  88. package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
  89. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
  90. package/components/Layout/components/Visualization/index.tsx +50 -38
  91. package/components/Layout/components/Visualization/visualizations.scss +232 -15
  92. package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
  93. package/components/Layout/components/VisualizationContainer.tsx +37 -0
  94. package/components/Layout/components/VisualizationContent.test.tsx +182 -0
  95. package/components/Layout/components/VisualizationContent.tsx +75 -0
  96. package/components/Layout/index.tsx +5 -5
  97. package/components/Layout/styles/editor-utils.scss +3 -3
  98. package/components/Layout/styles/editor.scss +4 -4
  99. package/components/Legend/Legend.Gradient.tsx +7 -1
  100. package/components/Loader/loader.styles.css +2 -2
  101. package/components/Loading.jsx +1 -1
  102. package/components/MediaControls.tsx +10 -3
  103. package/components/MultiSelect/multiselect.styles.css +19 -19
  104. package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
  105. package/components/PaletteSelector/PaletteSelector.css +15 -15
  106. package/components/RichTooltip/richTooltip.css +6 -6
  107. package/components/Table/table.styles.css +2 -2
  108. package/components/Waiting.tsx +1 -1
  109. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  110. package/components/_stories/DataTable.stories.tsx +1 -0
  111. package/components/_stories/Filters.stories.tsx +1 -1
  112. package/components/_stories/styles.scss +0 -1
  113. package/components/elements/Button.jsx +1 -1
  114. package/components/elements/Card.jsx +1 -1
  115. package/{styles/v2/components → components/elements}/button.scss +9 -8
  116. package/components/inputs/InputCheckbox.jsx +1 -1
  117. package/components/inputs/InputSelect.tsx +1 -1
  118. package/components/inputs/InputText.jsx +1 -1
  119. package/components/inputs/InputToggle.tsx +1 -1
  120. package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
  121. package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
  122. package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
  123. package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
  124. package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
  125. package/{styles → components}/loading.scss +1 -1
  126. package/components/managers/DataDesigner.tsx +1 -1
  127. package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
  128. package/components/ui/Accordion.jsx +1 -1
  129. package/components/ui/Icon.tsx +1 -1
  130. package/components/ui/LoadSpin.jsx +1 -1
  131. package/components/ui/Modal.jsx +1 -1
  132. package/components/ui/Overlay.jsx +1 -1
  133. package/components/ui/Title/index.test.tsx +34 -0
  134. package/components/ui/Title/index.tsx +24 -7
  135. package/components/ui/Title/title.styles.css +119 -25
  136. package/components/ui/Tooltip.tsx +1 -1
  137. package/components/ui/_stories/Title.stories.tsx +1 -1
  138. package/{styles/v2/components → components/ui}/accordion.scss +3 -3
  139. package/components/ui/accordion.styles.css +11 -11
  140. package/{styles/v2/components → components/ui}/modal.scss +2 -2
  141. package/{styles/v2/components → components/ui}/overlay.scss +6 -6
  142. package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
  143. package/{styles → components}/waiting.scss +9 -3
  144. package/data/colorPalettes.ts +18 -5
  145. package/data/mapColorPalettes.ts +10 -0
  146. package/devTemplate/dev.js +285 -0
  147. package/devTemplate/index.html +30 -0
  148. package/devTemplate/preview.html +1503 -0
  149. package/devTemplate/sidebar.css +151 -0
  150. package/dist/cove-main.css +2530 -3901
  151. package/dist/cove-main.css.map +1 -1
  152. package/generateViteConfig.js +111 -2
  153. package/helpers/DataTransform.ts +1 -5
  154. package/helpers/backfillDefaults.ts +35 -0
  155. package/helpers/constants.ts +12 -0
  156. package/helpers/cove/date.ts +64 -3
  157. package/helpers/cove/number.ts +29 -15
  158. package/helpers/cove/string.ts +29 -0
  159. package/helpers/coveUpdateWorker.ts +14 -8
  160. package/helpers/displayDataAsText.ts +1 -1
  161. package/helpers/embed/embedCodeGenerator.ts +80 -0
  162. package/helpers/embed/embedHelper.js +169 -0
  163. package/helpers/embed/filterUtils.ts +121 -0
  164. package/helpers/embed/index.ts +17 -0
  165. package/helpers/embed/urlValidation.ts +119 -0
  166. package/helpers/extractDataAndMetadata.ts +20 -0
  167. package/helpers/fetchRemoteData.ts +14 -8
  168. package/helpers/filterVizData.ts +6 -1
  169. package/helpers/getFileExtension.ts +0 -6
  170. package/helpers/labelHash.ts +9 -0
  171. package/helpers/markupProcessor.ts +56 -38
  172. package/helpers/metrics/types.ts +3 -0
  173. package/helpers/palettes/colorDistributions.ts +1 -1
  174. package/helpers/palettes/utils.ts +12 -12
  175. package/helpers/parseCsvWithQuotes.ts +15 -14
  176. package/helpers/prepareScreenshot.ts +33 -10
  177. package/helpers/testing.ts +44 -0
  178. package/helpers/tests/DataTransform.test.ts +125 -0
  179. package/helpers/tests/abbreviateNumber.test.ts +59 -0
  180. package/helpers/tests/backfillDefaults.test.ts +253 -0
  181. package/helpers/tests/date.test.ts +110 -0
  182. package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
  183. package/helpers/tests/markupProcessor.test.ts +315 -124
  184. package/helpers/tests/number.test.ts +42 -0
  185. package/helpers/tests/prepareScreenshot.test.ts +28 -28
  186. package/helpers/tests/testStandaloneBuild.ts +36 -26
  187. package/helpers/tests/useDataVizClasses.test.ts +66 -0
  188. package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
  189. package/helpers/useDataVizClasses.ts +13 -7
  190. package/helpers/vegaConfig.ts +1 -1
  191. package/helpers/vegaConfigImport.ts +160 -0
  192. package/helpers/ver/4.24.4.ts +24 -0
  193. package/helpers/ver/4.26.1.ts +1 -1
  194. package/helpers/ver/4.26.2.ts +84 -0
  195. package/helpers/ver/4.26.3.ts +44 -0
  196. package/helpers/ver/4.26.4.ts +31 -0
  197. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  198. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  199. package/helpers/ver/tests/4.26.3.test.ts +168 -0
  200. package/helpers/ver/tests/4.26.4.test.ts +88 -0
  201. package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
  202. package/helpers/viewports.ts +2 -0
  203. package/package.json +27 -32
  204. package/styles/_global.scss +7 -7
  205. package/styles/_reset.scss +2 -2
  206. package/styles/{v2/base → base}/_file-selector.scss +4 -4
  207. package/styles/{v2/base → base}/_general.scss +2 -4
  208. package/styles/{v2/base → base}/index.scss +1 -1
  209. package/styles/base.scss +107 -165
  210. package/styles/cove-main.scss +3 -6
  211. package/styles/layout/_component.scss +110 -0
  212. package/styles/{v2/layout → layout}/_data-table.scss +7 -7
  213. package/styles/layout/_wrapper-padding.scss +27 -0
  214. package/styles/{v2/main.scss → main.scss} +3 -1
  215. package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
  216. package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
  217. package/styles/{v2/utils → utils}/_grid.scss +8 -3
  218. package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
  219. package/styles/{v2/utils → utils}/index.scss +2 -1
  220. package/types/Annotation.ts +10 -11
  221. package/types/Axis.ts +2 -0
  222. package/types/ComponentStyles.ts +1 -0
  223. package/types/ConfigureData.ts +1 -0
  224. package/types/General.ts +2 -0
  225. package/types/MarkupInclude.ts +1 -0
  226. package/types/MarkupVariable.ts +2 -1
  227. package/types/Palette.ts +22 -0
  228. package/types/Table.ts +9 -0
  229. package/types/Visualization.ts +7 -0
  230. package/_stories/StoryRenderingTests.stories.tsx +0 -164
  231. package/helpers/embedCodeGenerator.ts +0 -109
  232. package/styles/_common-components.css +0 -73
  233. package/styles/_variables.scss +0 -63
  234. package/styles/v2/layout/_component.scss +0 -21
  235. package/styles/v2/utils/_variables.scss +0 -9
  236. package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
  237. /package/{styles/v2/components → components/ui}/icon.scss +0 -0
  238. /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
  239. /package/styles/{v2/base → base}/_heading.scss +0 -0
  240. /package/styles/{v2/base → base}/_reset.scss +0 -0
  241. /package/styles/{v2/layout → layout}/_alert.scss +0 -0
  242. /package/styles/{v2/layout → layout}/_progression.scss +0 -0
  243. /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
  244. /package/styles/{v2/layout → layout}/index.scss +0 -0
  245. /package/styles/{v2/themes → themes}/index.scss +0 -0
  246. /package/styles/{v2/utils → utils}/_align.scss +0 -0
  247. /package/styles/{v2/utils → utils}/_animations.scss +0 -0
  248. /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
  249. /package/styles/{v2/utils → utils}/_mixins.scss +0 -0
@@ -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
+ })
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getDateRenderFormat, formatDate } from '../cove/date'
3
+
4
+ const NBSP = '\u00A0'
5
+
6
+ describe('getDateRenderFormat', () => {
7
+ it('replaces space between %b. and %-d with NBSP', () => {
8
+ expect(getDateRenderFormat('%b. %-d %Y')).toBe(`%b.${NBSP}%-d %Y`)
9
+ })
10
+
11
+ it('replaces space between %b and %-d with NBSP', () => {
12
+ expect(getDateRenderFormat('%b %-d %Y')).toBe(`%b${NBSP}%-d %Y`)
13
+ })
14
+
15
+ it('replaces space between %B and %-d (full month name)', () => {
16
+ expect(getDateRenderFormat('%B %-d, %Y')).toBe(`%B${NBSP}%-d, %Y`)
17
+ })
18
+
19
+ it('leaves format unchanged when no month-day space pattern', () => {
20
+ expect(getDateRenderFormat('%Y-%m-%d')).toBe('%Y-%m-%d')
21
+ })
22
+
23
+ it('does not replace existing NBSP (idempotent)', () => {
24
+ const alreadyNbsp = `%b.${NBSP}%-d %Y`
25
+ expect(getDateRenderFormat(alreadyNbsp)).toBe(alreadyNbsp)
26
+ })
27
+
28
+ it('returns undefined for undefined input', () => {
29
+ expect(getDateRenderFormat(undefined)).toBeUndefined()
30
+ })
31
+
32
+ it('returns empty string for empty string input', () => {
33
+ expect(getDateRenderFormat('')).toBe('')
34
+ })
35
+ })
36
+
37
+ describe('formatDate', () => {
38
+ it('renders date with NBSP when format has space between month and day', () => {
39
+ const date = new Date(2025, 0, 15) // Jan 15, 2025
40
+ const result = formatDate('%b. %-d %Y', date)
41
+ expect(result).toContain(NBSP)
42
+ expect(result).toBe(`Jan.${NBSP}15 2025`)
43
+ })
44
+
45
+ it('strips trailing period from "May." when using %b. format', () => {
46
+ const date = new Date(2025, 4, 15) // May 15, 2025
47
+ const result = formatDate('%b. %-d, %Y', date)
48
+ expect(result).not.toContain('May.')
49
+ expect(result).toContain('May')
50
+ })
51
+
52
+ it('leaves "May" alone when format does not use %b.', () => {
53
+ const date = new Date(2025, 4, 15) // May 15, 2025
54
+ const result = formatDate('%b %-d, %Y', date)
55
+ expect(result).toContain('May')
56
+ expect(result).not.toContain('May.')
57
+ })
58
+
59
+ it('does not strip period from other months when using %b. format', () => {
60
+ const date = new Date(2025, 0, 15) // Jan 15, 2025
61
+ const result = formatDate('%b. %-d, %Y', date)
62
+ expect(result).toContain('Jan.')
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
+ })
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
+ })