@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.
- package/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/LICENSE +201 -0
- package/_stories/Gallery.Charts.stories.tsx +35 -42
- package/_stories/Gallery.DataBite.stories.tsx +15 -8
- package/_stories/Gallery.Maps.stories.tsx +37 -28
- package/_stories/Gallery.WaffleChart.stories.tsx +1 -1
- package/_stories/PageART.stories.tsx +5 -4
- package/_stories/PageBRFSS.stories.tsx +21 -16
- package/_stories/PageCancerRegistries.stories.tsx +15 -15
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +33 -19
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +148 -143
- package/_stories/PageMaternalMortality.stories.tsx +5 -4
- package/_stories/PageOralHealth.stories.tsx +15 -10
- package/_stories/PageRespiratory.stories.tsx +4 -4
- package/_stories/PageSmokingTobacco.stories.tsx +15 -10
- package/_stories/PageStateDiabetesProfiles.stories.tsx +15 -10
- package/_stories/PageWastewater.stories.tsx +44 -30
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/callout-flag.svg +7 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +270 -38
- package/components/Alert/components/Alert.styles.css +2 -2
- package/components/ComboBox/combobox.styles.css +48 -48
- package/components/CustomColorsEditor/CustomColorsEditor.css +53 -53
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +46 -18
- package/components/DataTable/DataTableStandAlone.tsx +1 -0
- package/components/DataTable/components/ChartHeader.tsx +21 -12
- package/components/DataTable/components/MapHeader.tsx +34 -28
- package/components/DataTable/components/SortIcon/sort-icon.css +5 -5
- package/components/DataTable/data-table.css +50 -52
- package/components/DataTable/helpers/applyCustomOrder.ts +17 -0
- package/components/DataTable/helpers/getChartCellValue.ts +10 -7
- package/components/DataTable/helpers/getMapDataTableColumnKeys.ts +22 -0
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +33 -23
- package/components/DataTable/helpers/tests/mapCellMatrix.test.ts +33 -0
- package/components/DownloadButton.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +38 -31
- package/components/EditorPanel/CustomSortOrder.tsx +94 -0
- package/components/EditorPanel/DataTableEditor.tsx +139 -23
- package/components/EditorPanel/EditorPanel.styles.css +71 -71
- package/components/EditorPanel/EditorPanel.tsx +3 -8
- package/components/EditorPanel/EditorPanelDispatch.tsx +4 -4
- package/components/EditorPanel/FootnotesEditor.tsx +2 -2
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +21 -12
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +16 -10
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +160 -106
- package/components/EditorPanel/components/PanelMarkup.tsx +5 -1
- package/{styles/v2/components → components/EditorPanel}/editor.scss +76 -22
- package/components/EditorPanel/sections/StyleTreatmentSection.tsx +99 -0
- package/components/EditorPanel/sections/VisualSection.tsx +11 -0
- package/components/EditorWrapper/editor-wrapper.style.css +1 -1
- package/components/Filters/Filters.tsx +3 -5
- package/components/Filters/components/Tabs.tsx +19 -7
- package/{styles → components/Filters}/filters.scss +3 -3
- package/components/Footnotes/FootnotesStandAlone.tsx +4 -2
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +61 -5
- package/components/Layout/components/Responsive.tsx +14 -6
- package/components/Layout/components/Sidebar/components/Sidebar.tsx +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +14 -20
- package/components/Layout/components/Visualization/index.tsx +50 -38
- package/components/Layout/components/Visualization/visualizations.scss +232 -15
- package/components/Layout/components/VisualizationContainer.test.tsx +67 -0
- package/components/Layout/components/VisualizationContainer.tsx +37 -0
- package/components/Layout/components/VisualizationContent.test.tsx +182 -0
- package/components/Layout/components/VisualizationContent.tsx +75 -0
- package/components/Layout/index.tsx +5 -5
- package/components/Layout/styles/editor-utils.scss +3 -3
- package/components/Layout/styles/editor.scss +4 -4
- package/components/Legend/Legend.Gradient.tsx +7 -1
- package/components/Loader/loader.styles.css +2 -2
- package/components/Loading.jsx +1 -1
- package/components/MediaControls.tsx +10 -3
- package/components/MultiSelect/multiselect.styles.css +19 -19
- package/components/NestedDropdown/nesteddropdown.styles.css +15 -15
- package/components/PaletteSelector/PaletteSelector.css +15 -15
- package/components/RichTooltip/richTooltip.css +6 -6
- package/components/Table/table.styles.css +2 -2
- package/components/Waiting.tsx +1 -1
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/_stories/Filters.stories.tsx +1 -1
- package/components/_stories/styles.scss +0 -1
- package/components/elements/Button.jsx +1 -1
- package/components/elements/Card.jsx +1 -1
- package/{styles/v2/components → components/elements}/button.scss +9 -8
- package/components/inputs/InputCheckbox.jsx +1 -1
- package/components/inputs/InputSelect.tsx +1 -1
- package/components/inputs/InputText.jsx +1 -1
- package/components/inputs/InputToggle.tsx +1 -1
- package/{styles/v2/components/input → components/inputs}/_input-check-radio.scss +2 -2
- package/{styles/v2/components/input → components/inputs}/_input-group.scss +3 -3
- package/{styles/v2/components/input → components/inputs}/_input-slider.scss +2 -2
- package/{styles/v2/components/input → components/inputs}/_input.scss +5 -5
- package/{styles/v2/components/input → components/inputs}/index.scss +2 -2
- package/{styles → components}/loading.scss +1 -1
- package/components/managers/DataDesigner.tsx +1 -1
- package/{styles/v2/components → components/managers}/data-designer.scss +6 -7
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Icon.tsx +1 -1
- package/components/ui/LoadSpin.jsx +1 -1
- package/components/ui/Modal.jsx +1 -1
- package/components/ui/Overlay.jsx +1 -1
- package/components/ui/Title/index.test.tsx +34 -0
- package/components/ui/Title/index.tsx +24 -7
- package/components/ui/Title/title.styles.css +119 -25
- package/components/ui/Tooltip.tsx +1 -1
- package/components/ui/_stories/Title.stories.tsx +1 -1
- package/{styles/v2/components → components/ui}/accordion.scss +3 -3
- package/components/ui/accordion.styles.css +11 -11
- package/{styles/v2/components → components/ui}/modal.scss +2 -2
- package/{styles/v2/components → components/ui}/overlay.scss +6 -6
- package/{styles/v2/components → components}/ui/tooltip.scss +1 -1
- package/{styles → components}/waiting.scss +9 -3
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +285 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2530 -3901
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +111 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/backfillDefaults.ts +35 -0
- package/helpers/constants.ts +12 -0
- package/helpers/cove/date.ts +64 -3
- package/helpers/cove/number.ts +29 -15
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +14 -8
- package/helpers/displayDataAsText.ts +1 -1
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +169 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +17 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/extractDataAndMetadata.ts +20 -0
- package/helpers/fetchRemoteData.ts +14 -8
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/labelHash.ts +9 -0
- package/helpers/markupProcessor.ts +56 -38
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/prepareScreenshot.ts +33 -10
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/abbreviateNumber.test.ts +59 -0
- package/helpers/tests/backfillDefaults.test.ts +253 -0
- package/helpers/tests/date.test.ts +110 -0
- package/helpers/tests/extractDataAndMetadata.test.ts +93 -0
- package/helpers/tests/markupProcessor.test.ts +315 -124
- package/helpers/tests/number.test.ts +42 -0
- package/helpers/tests/prepareScreenshot.test.ts +28 -28
- package/helpers/tests/testStandaloneBuild.ts +36 -26
- package/helpers/tests/useDataVizClasses.test.ts +66 -0
- package/helpers/tests/visualizationWrapperUsage.test.ts +57 -0
- package/helpers/useDataVizClasses.ts +13 -7
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.24.4.ts +24 -0
- package/helpers/ver/4.26.1.ts +1 -1
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/4.26.3.ts +44 -0
- package/helpers/ver/4.26.4.ts +31 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -0
- package/helpers/ver/tests/4.26.3.test.ts +168 -0
- package/helpers/ver/tests/4.26.4.test.ts +88 -0
- package/helpers/ver/tests/coveUpdateWorker.test.ts +57 -0
- package/helpers/viewports.ts +2 -0
- package/package.json +27 -32
- package/styles/_global.scss +7 -7
- package/styles/_reset.scss +2 -2
- package/styles/{v2/base → base}/_file-selector.scss +4 -4
- package/styles/{v2/base → base}/_general.scss +2 -4
- package/styles/{v2/base → base}/index.scss +1 -1
- package/styles/base.scss +107 -165
- package/styles/cove-main.scss +3 -6
- package/styles/layout/_component.scss +110 -0
- package/styles/{v2/layout → layout}/_data-table.scss +7 -7
- package/styles/layout/_wrapper-padding.scss +27 -0
- package/styles/{v2/main.scss → main.scss} +3 -1
- package/styles/{v2/themes → themes}/_color-definitions.scss +46 -41
- package/styles/{_accessibility.scss → utils/_accessibility.scss} +1 -1
- package/styles/{v2/utils → utils}/_grid.scss +8 -3
- package/styles/{_global-variables.scss → utils/_properties.scss} +133 -112
- package/styles/{v2/utils → utils}/index.scss +2 -1
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +2 -0
- package/types/ComponentStyles.ts +1 -0
- package/types/ConfigureData.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/MarkupVariable.ts +2 -1
- package/types/Palette.ts +22 -0
- package/types/Table.ts +9 -0
- package/types/Visualization.ts +7 -0
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
- package/helpers/embedCodeGenerator.ts +0 -109
- package/styles/_common-components.css +0 -73
- package/styles/_variables.scss +0 -63
- package/styles/v2/layout/_component.scss +0 -21
- package/styles/v2/utils/_variables.scss +0 -9
- package/{styles/v2/components/card.scss → components/elements/card.css} +2 -2
- /package/{styles/v2/components → components/ui}/icon.scss +0 -0
- /package/{styles/v2/components → components/ui}/loadspin.scss +0 -0
- /package/styles/{v2/base → base}/_heading.scss +0 -0
- /package/styles/{v2/base → base}/_reset.scss +0 -0
- /package/styles/{v2/layout → layout}/_alert.scss +0 -0
- /package/styles/{v2/layout → layout}/_progression.scss +0 -0
- /package/styles/{v2/layout → layout}/_tooltip.scss +0 -0
- /package/styles/{v2/layout → layout}/index.scss +0 -0
- /package/styles/{v2/themes → themes}/index.scss +0 -0
- /package/styles/{v2/utils → utils}/_align.scss +0 -0
- /package/styles/{v2/utils → utils}/_animations.scss +0 -0
- /package/styles/{v2/utils → utils}/_breakpoints.scss +0 -0
- /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
|
+
})
|