@cdc/core 4.25.11 → 4.26.2
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/_stories/Gallery.Charts.stories.tsx +300 -0
- package/_stories/Gallery.DataBite.stories.tsx +79 -0
- package/_stories/Gallery.Maps.stories.tsx +239 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +193 -0
- package/_stories/PageBRFSS.stories.tsx +294 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
- package/_stories/PageMaternalMortality.stories.tsx +193 -0
- package/_stories/PageOralHealth.stories.tsx +201 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +200 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
- package/_stories/PageWastewater.stories.tsx +477 -0
- 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/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +51 -25
- package/components/EditorPanel/EditorPanel.styles.css +16 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/Inputs.tsx +33 -7
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +31 -5
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
- package/components/Layout/components/Visualization/index.tsx +27 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -28
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4448
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +118 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +21 -12
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/metrics/helpers.ts +1 -0
- 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/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +288 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/ver/4.26.2.ts +84 -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/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +29 -33
- package/styles/_button-section.scss +0 -3
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Palette.ts +21 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +7 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { getQueryStringFilterValue, isFilterHiddenByQuery, getQueryParams, getQueryParam } from '../queryStringUtils'
|
|
2
|
+
import { expect, describe, it, beforeEach, afterEach, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
describe('getQueryStringFilterValue', () => {
|
|
5
|
+
let originalLocation: Location
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Save original location
|
|
9
|
+
originalLocation = window.location
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
// Restore original location
|
|
14
|
+
Object.defineProperty(window, 'location', {
|
|
15
|
+
value: originalLocation,
|
|
16
|
+
writable: true,
|
|
17
|
+
configurable: true
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const mockLocation = (search: string) => {
|
|
22
|
+
delete (window as any).location
|
|
23
|
+
Object.defineProperty(window, 'location', {
|
|
24
|
+
value: { search },
|
|
25
|
+
writable: true,
|
|
26
|
+
configurable: true
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it('should return the matching filter value from query string (case-insensitive)', () => {
|
|
31
|
+
mockLocation('?location=home')
|
|
32
|
+
const filter = {
|
|
33
|
+
setByQueryParameter: 'location',
|
|
34
|
+
values: ['Home', 'School', 'Work']
|
|
35
|
+
}
|
|
36
|
+
const result = getQueryStringFilterValue(filter)
|
|
37
|
+
expect(result).toBe('Home') // Returns value with original casing
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should return the matching filter value when query param is uppercase', () => {
|
|
41
|
+
mockLocation('?state=CALIFORNIA')
|
|
42
|
+
const filter = {
|
|
43
|
+
setByQueryParameter: 'state',
|
|
44
|
+
values: ['California', 'Texas', 'New York']
|
|
45
|
+
}
|
|
46
|
+
const result = getQueryStringFilterValue(filter)
|
|
47
|
+
expect(result).toBe('California')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should return undefined when setByQueryParameter is not defined', () => {
|
|
51
|
+
mockLocation('?location=home')
|
|
52
|
+
const filter = {
|
|
53
|
+
values: ['Home', 'School', 'Work']
|
|
54
|
+
}
|
|
55
|
+
const result = getQueryStringFilterValue(filter)
|
|
56
|
+
expect(result).toBeUndefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should return undefined when query parameter is not in URL', () => {
|
|
60
|
+
mockLocation('?other=value')
|
|
61
|
+
const filter = {
|
|
62
|
+
setByQueryParameter: 'location',
|
|
63
|
+
values: ['Home', 'School', 'Work']
|
|
64
|
+
}
|
|
65
|
+
const result = getQueryStringFilterValue(filter)
|
|
66
|
+
expect(result).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should return undefined when query value does not match any filter values', () => {
|
|
70
|
+
mockLocation('?location=park')
|
|
71
|
+
const filter = {
|
|
72
|
+
setByQueryParameter: 'location',
|
|
73
|
+
values: ['Home', 'School', 'Work']
|
|
74
|
+
}
|
|
75
|
+
const result = getQueryStringFilterValue(filter)
|
|
76
|
+
expect(result).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should return undefined when filter has no values', () => {
|
|
80
|
+
mockLocation('?location=home')
|
|
81
|
+
const filter = {
|
|
82
|
+
setByQueryParameter: 'location',
|
|
83
|
+
values: []
|
|
84
|
+
}
|
|
85
|
+
const result = getQueryStringFilterValue(filter)
|
|
86
|
+
expect(result).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should handle filters with null values in array', () => {
|
|
90
|
+
mockLocation('?location=school')
|
|
91
|
+
const filter = {
|
|
92
|
+
setByQueryParameter: 'location',
|
|
93
|
+
values: ['Home', null, 'School', 'Work']
|
|
94
|
+
}
|
|
95
|
+
const result = getQueryStringFilterValue(filter)
|
|
96
|
+
expect(result).toBe('School')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('isFilterHiddenByQuery', () => {
|
|
101
|
+
let originalLocation: Location
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
originalLocation = window.location
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
Object.defineProperty(window, 'location', {
|
|
109
|
+
value: originalLocation,
|
|
110
|
+
writable: true,
|
|
111
|
+
configurable: true
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const mockLocation = (search: string) => {
|
|
116
|
+
delete (window as any).location
|
|
117
|
+
Object.defineProperty(window, 'location', {
|
|
118
|
+
value: { search },
|
|
119
|
+
writable: true,
|
|
120
|
+
configurable: true
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
it('should return true when query parameter is "true"', () => {
|
|
125
|
+
mockLocation('?hideState=true')
|
|
126
|
+
const filter = {
|
|
127
|
+
setByQueryParameter: 'State'
|
|
128
|
+
}
|
|
129
|
+
const result = isFilterHiddenByQuery(filter)
|
|
130
|
+
expect(result).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should return true when query parameter is "TRUE" (case insensitive)', () => {
|
|
134
|
+
mockLocation('?hideState=TRUE')
|
|
135
|
+
const filter = {
|
|
136
|
+
setByQueryParameter: 'State'
|
|
137
|
+
}
|
|
138
|
+
const result = isFilterHiddenByQuery(filter)
|
|
139
|
+
expect(result).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should return true when query parameter is "1"', () => {
|
|
143
|
+
mockLocation('?hideLocation=1')
|
|
144
|
+
const filter = {
|
|
145
|
+
setByQueryParameter: 'Location'
|
|
146
|
+
}
|
|
147
|
+
const result = isFilterHiddenByQuery(filter)
|
|
148
|
+
expect(result).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should return true when query parameter is "yes"', () => {
|
|
152
|
+
mockLocation('?hideYear=yes')
|
|
153
|
+
const filter = {
|
|
154
|
+
setByQueryParameter: 'Year'
|
|
155
|
+
}
|
|
156
|
+
const result = isFilterHiddenByQuery(filter)
|
|
157
|
+
expect(result).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should return true when query parameter is "YES" (case insensitive)', () => {
|
|
161
|
+
mockLocation('?hideYear=YES')
|
|
162
|
+
const filter = {
|
|
163
|
+
setByQueryParameter: 'Year'
|
|
164
|
+
}
|
|
165
|
+
const result = isFilterHiddenByQuery(filter)
|
|
166
|
+
expect(result).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should return false when setByQueryParameter is not defined', () => {
|
|
170
|
+
mockLocation('?hideState=true')
|
|
171
|
+
const filter = {}
|
|
172
|
+
const result = isFilterHiddenByQuery(filter)
|
|
173
|
+
expect(result).toBe(false)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should return false when filter is null', () => {
|
|
177
|
+
mockLocation('?hideState=true')
|
|
178
|
+
const result = isFilterHiddenByQuery(null)
|
|
179
|
+
expect(result).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should return false when filter is undefined', () => {
|
|
183
|
+
mockLocation('?hideState=true')
|
|
184
|
+
const result = isFilterHiddenByQuery(undefined)
|
|
185
|
+
expect(result).toBe(false)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should return false when query parameter is not in URL', () => {
|
|
189
|
+
mockLocation('?other=value')
|
|
190
|
+
const filter = {
|
|
191
|
+
setByQueryParameter: 'State'
|
|
192
|
+
}
|
|
193
|
+
const result = isFilterHiddenByQuery(filter)
|
|
194
|
+
expect(result).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should return false when query parameter is "false"', () => {
|
|
198
|
+
mockLocation('?hideState=false')
|
|
199
|
+
const filter = {
|
|
200
|
+
setByQueryParameter: 'State'
|
|
201
|
+
}
|
|
202
|
+
const result = isFilterHiddenByQuery(filter)
|
|
203
|
+
expect(result).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should return false when query parameter is "0"', () => {
|
|
207
|
+
mockLocation('?hideLocation=0')
|
|
208
|
+
const filter = {
|
|
209
|
+
setByQueryParameter: 'Location'
|
|
210
|
+
}
|
|
211
|
+
const result = isFilterHiddenByQuery(filter)
|
|
212
|
+
expect(result).toBe(false)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should return false when query parameter is "no"', () => {
|
|
216
|
+
mockLocation('?hideYear=no')
|
|
217
|
+
const filter = {
|
|
218
|
+
setByQueryParameter: 'Year'
|
|
219
|
+
}
|
|
220
|
+
const result = isFilterHiddenByQuery(filter)
|
|
221
|
+
expect(result).toBe(false)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should return false when query parameter is empty string', () => {
|
|
225
|
+
mockLocation('?hideState=')
|
|
226
|
+
const filter = {
|
|
227
|
+
setByQueryParameter: 'State'
|
|
228
|
+
}
|
|
229
|
+
const result = isFilterHiddenByQuery(filter)
|
|
230
|
+
expect(result).toBe(false)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should return false when query parameter has an arbitrary value', () => {
|
|
234
|
+
mockLocation('?hideState=maybe')
|
|
235
|
+
const filter = {
|
|
236
|
+
setByQueryParameter: 'State'
|
|
237
|
+
}
|
|
238
|
+
const result = isFilterHiddenByQuery(filter)
|
|
239
|
+
expect(result).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should handle multiple query parameters correctly', () => {
|
|
243
|
+
mockLocation('?location=home&hideState=true&year=2024')
|
|
244
|
+
const filter = {
|
|
245
|
+
setByQueryParameter: 'State'
|
|
246
|
+
}
|
|
247
|
+
const result = isFilterHiddenByQuery(filter)
|
|
248
|
+
expect(result).toBe(true)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should not hide when different parameter is true', () => {
|
|
252
|
+
mockLocation('?hideLocation=true&hideState=false')
|
|
253
|
+
const filter = {
|
|
254
|
+
setByQueryParameter: 'State'
|
|
255
|
+
}
|
|
256
|
+
const result = isFilterHiddenByQuery(filter)
|
|
257
|
+
expect(result).toBe(false)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should construct parameter name with exact case from setByQueryParameter', () => {
|
|
261
|
+
mockLocation('?hidecolor=true')
|
|
262
|
+
const filter = {
|
|
263
|
+
setByQueryParameter: 'color'
|
|
264
|
+
}
|
|
265
|
+
const result = isFilterHiddenByQuery(filter)
|
|
266
|
+
expect(result).toBe(true)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should work with multi-word parameter names', () => {
|
|
270
|
+
mockLocation('?hideAnimal Category=1')
|
|
271
|
+
const filter = {
|
|
272
|
+
setByQueryParameter: 'Animal Category'
|
|
273
|
+
}
|
|
274
|
+
const result = isFilterHiddenByQuery(filter)
|
|
275
|
+
expect(result).toBe(true)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should only check setByQueryParameter (not key, label, or columnName)', () => {
|
|
279
|
+
mockLocation('?hidegeography=true')
|
|
280
|
+
const filter = {
|
|
281
|
+
setByQueryParameter: 'State',
|
|
282
|
+
key: 'geography',
|
|
283
|
+
label: 'geography',
|
|
284
|
+
columnName: 'geography'
|
|
285
|
+
}
|
|
286
|
+
const result = isFilterHiddenByQuery(filter)
|
|
287
|
+
expect(result).toBe(false) // Should NOT match because setByQueryParameter is 'State', not 'geography'
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should match when setByQueryParameter matches URL param', () => {
|
|
291
|
+
mockLocation('?hideState=true')
|
|
292
|
+
const filter = {
|
|
293
|
+
setByQueryParameter: 'State',
|
|
294
|
+
key: 'geography',
|
|
295
|
+
label: 'Select Geography',
|
|
296
|
+
columnName: 'geography'
|
|
297
|
+
}
|
|
298
|
+
const result = isFilterHiddenByQuery(filter)
|
|
299
|
+
expect(result).toBe(true) // Should match because setByQueryParameter is 'State'
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('getQueryParams', () => {
|
|
304
|
+
let originalLocation: Location
|
|
305
|
+
|
|
306
|
+
beforeEach(() => {
|
|
307
|
+
originalLocation = window.location
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
afterEach(() => {
|
|
311
|
+
Object.defineProperty(window, 'location', {
|
|
312
|
+
value: originalLocation,
|
|
313
|
+
writable: true,
|
|
314
|
+
configurable: true
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const mockLocation = (search: string) => {
|
|
319
|
+
delete (window as any).location
|
|
320
|
+
Object.defineProperty(window, 'location', {
|
|
321
|
+
value: { search },
|
|
322
|
+
writable: true,
|
|
323
|
+
configurable: true
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
it('should return an object of query parameters', () => {
|
|
328
|
+
mockLocation('?state=CA&year=2024')
|
|
329
|
+
const result = getQueryParams()
|
|
330
|
+
expect(result).toEqual({ state: 'CA', year: '2024' })
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('should return empty object when no query parameters', () => {
|
|
334
|
+
mockLocation('')
|
|
335
|
+
const result = getQueryParams()
|
|
336
|
+
expect(result).toEqual({})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should handle multiple values for the same key', () => {
|
|
340
|
+
mockLocation('?tag=health&tag=safety&tag=education')
|
|
341
|
+
const result = getQueryParams()
|
|
342
|
+
expect(result.tag).toEqual(['health', 'safety', 'education'])
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe('getQueryParam', () => {
|
|
347
|
+
let originalLocation: Location
|
|
348
|
+
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
originalLocation = window.location
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
afterEach(() => {
|
|
354
|
+
Object.defineProperty(window, 'location', {
|
|
355
|
+
value: originalLocation,
|
|
356
|
+
writable: true,
|
|
357
|
+
configurable: true
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const mockLocation = (search: string) => {
|
|
362
|
+
delete (window as any).location
|
|
363
|
+
Object.defineProperty(window, 'location', {
|
|
364
|
+
value: { search },
|
|
365
|
+
writable: true,
|
|
366
|
+
configurable: true
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
it('should return the value of a specific query parameter', () => {
|
|
371
|
+
mockLocation('?state=CA&year=2024')
|
|
372
|
+
const result = getQueryParam('state')
|
|
373
|
+
expect(result).toBe('CA')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('should return undefined for non-existent parameter', () => {
|
|
377
|
+
mockLocation('?state=CA')
|
|
378
|
+
const result = getQueryParam('year')
|
|
379
|
+
expect(result).toBeUndefined()
|
|
380
|
+
})
|
|
381
|
+
})
|
|
@@ -21,24 +21,42 @@ function copyDirSync(src, dest) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Tests if a package can be built in isolation
|
|
25
|
+
// See DOCS/PACKAGE_DEPENDENCIES.md for more details
|
|
24
26
|
export function testStandaloneBuild(pkgDir) {
|
|
25
|
-
// This test can't be turned on until we've published the new version of @cdc/core
|
|
26
|
-
return true
|
|
27
|
-
|
|
28
27
|
pkgDir = pkgDir.replace('/src', '')
|
|
29
28
|
const pkgName = pkgDir.split('/')[pkgDir.split('/').length - 1]
|
|
30
29
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `cdc-open-viz-${pkgName}-`))
|
|
31
30
|
copyDirSync(pkgDir, tmpDir)
|
|
32
31
|
|
|
32
|
+
let coreTarballPath = null
|
|
33
|
+
|
|
33
34
|
try {
|
|
34
|
-
execSync('npm install', { cwd: tmpDir })
|
|
35
|
-
|
|
35
|
+
execSync('npm install --include=dev', { cwd: tmpDir })
|
|
36
|
+
|
|
37
|
+
// Pack core into tmp directory with unique name then install from package tarball
|
|
38
|
+
const coreDir = path.join(pkgDir, '..', 'core')
|
|
39
|
+
const uniqueId = `${Date.now()}-${Math.random().toString(36).substring(7)}`
|
|
40
|
+
const uniqueTarballDir = fs.mkdtempSync(path.join(os.tmpdir(), `cdc-pack-${uniqueId}-`))
|
|
41
|
+
const packOutput = execSync(`npm pack --pack-destination="${uniqueTarballDir}"`, {
|
|
42
|
+
cwd: coreDir,
|
|
43
|
+
encoding: 'utf-8'
|
|
44
|
+
})
|
|
45
|
+
const tarballName = packOutput.trim().split('\n').pop()
|
|
46
|
+
coreTarballPath = path.join(uniqueTarballDir, tarballName)
|
|
47
|
+
execSync(`npm install "${coreTarballPath}"`, { cwd: tmpDir, stdio: 'inherit' })
|
|
48
|
+
|
|
36
49
|
execSync('npm run build', { cwd: tmpDir })
|
|
37
50
|
return true
|
|
38
51
|
} catch (err) {
|
|
39
52
|
console.error(`❌ Isolated build for ${pkgName} package failed`)
|
|
53
|
+
console.error(err.message)
|
|
40
54
|
return false
|
|
41
55
|
} finally {
|
|
56
|
+
if (coreTarballPath && fs.existsSync(coreTarballPath)) {
|
|
57
|
+
const uniqueTarballDir = path.dirname(coreTarballPath)
|
|
58
|
+
fs.rmSync(uniqueTarballDir, { recursive: true, force: true })
|
|
59
|
+
}
|
|
42
60
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
43
61
|
}
|
|
44
62
|
}
|
package/helpers/vegaConfig.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
2
2
|
import { formatDate } from '@cdc/core/helpers/cove/date.js'
|
|
3
|
-
import
|
|
3
|
+
import _ from 'lodash'
|
|
4
4
|
import { compile as vegaLiteCompile } from 'vega-lite'
|
|
5
5
|
import { parse as vegaParse, View as vegaView } from 'vega'
|
|
6
6
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Vega-to-COVE conversion helper for stories.
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
convertVegaConfig,
|
|
6
|
+
getVegaConfigType,
|
|
7
|
+
getVegaErrors,
|
|
8
|
+
getVegaWarnings,
|
|
9
|
+
isVegaConfig,
|
|
10
|
+
parseVegaConfig
|
|
11
|
+
} from './vegaConfig'
|
|
12
|
+
|
|
13
|
+
/** Chart-type "button" definitions used to seed new COVE configs. */
|
|
14
|
+
const buttons = [
|
|
15
|
+
{
|
|
16
|
+
id: 1,
|
|
17
|
+
category: 'Charts',
|
|
18
|
+
label: 'Bar',
|
|
19
|
+
type: 'chart',
|
|
20
|
+
subType: 'Bar',
|
|
21
|
+
orientation: 'vertical',
|
|
22
|
+
barThickness: '0.37',
|
|
23
|
+
visualizationSubType: 'regular',
|
|
24
|
+
xAxis: { type: 'categorical', size: 75, maxTickRotation: 45, labelOffset: 0 },
|
|
25
|
+
content: 'Use bars to show comparisons between data categories.'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 3,
|
|
29
|
+
category: 'Charts',
|
|
30
|
+
label: 'Combo Chart',
|
|
31
|
+
type: 'chart',
|
|
32
|
+
subType: 'Combo',
|
|
33
|
+
orientation: 'vertical',
|
|
34
|
+
content: 'Use bars to show comparisons between data categories.'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 4,
|
|
38
|
+
category: 'Charts',
|
|
39
|
+
label: 'Line',
|
|
40
|
+
type: 'chart',
|
|
41
|
+
subType: 'Line',
|
|
42
|
+
orientation: 'vertical',
|
|
43
|
+
content: 'Present one or more data trends over time.'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 6,
|
|
47
|
+
category: 'Charts',
|
|
48
|
+
label: 'Area Chart',
|
|
49
|
+
type: 'chart',
|
|
50
|
+
subType: 'Area Chart',
|
|
51
|
+
orientation: 'vertical',
|
|
52
|
+
content: 'Display an area chart to visualize quantities over time.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 8,
|
|
56
|
+
category: 'Charts',
|
|
57
|
+
label: 'Scatter Plot',
|
|
58
|
+
type: 'chart',
|
|
59
|
+
subType: 'Scatter Plot',
|
|
60
|
+
orientation: 'vertical',
|
|
61
|
+
content: 'Display a scatter plot to explore relationships between numeric variables.'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 12,
|
|
65
|
+
category: 'Charts',
|
|
66
|
+
label: 'Horizontal Bar (Stacked)',
|
|
67
|
+
type: 'chart',
|
|
68
|
+
subType: 'Bar',
|
|
69
|
+
visualizationSubType: 'stacked',
|
|
70
|
+
orientation: 'horizontal',
|
|
71
|
+
content: 'Use bars to show comparisons between data categories.'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 19,
|
|
75
|
+
category: 'Maps',
|
|
76
|
+
label: 'United States (State- or County-Level)',
|
|
77
|
+
type: 'map',
|
|
78
|
+
subType: 'us',
|
|
79
|
+
content: 'Present a U.S. choropleth map at state or county level.',
|
|
80
|
+
position: 'right'
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
/** Build a seed COVE config from a button definition. */
|
|
85
|
+
const generateNewConfig = (props: any) => {
|
|
86
|
+
let newConfig: any = {}
|
|
87
|
+
switch (props.category) {
|
|
88
|
+
case 'Charts': {
|
|
89
|
+
const visualizationType = props.subType
|
|
90
|
+
const visualizationSubType = !props.visualizationSubType ? 'regular' : props.visualizationSubType
|
|
91
|
+
newConfig = {
|
|
92
|
+
...props,
|
|
93
|
+
visualizationType,
|
|
94
|
+
visualizationSubType,
|
|
95
|
+
newViz: true,
|
|
96
|
+
datasets: {}
|
|
97
|
+
}
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
case 'Maps': {
|
|
101
|
+
newConfig = { ...props, newViz: true, datasets: {}, type: 'map' }
|
|
102
|
+
newConfig['general'] = { geoType: props.subType, type: props?.generalType }
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return newConfig
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert a raw Vega/Vega-Lite config into a COVE config.
|
|
111
|
+
* Returns `null` if the config has errors.
|
|
112
|
+
*/
|
|
113
|
+
export const importVegaConfig = async (rawConfig: any): Promise<any | null> => {
|
|
114
|
+
const vegaConfig = await parseVegaConfig(rawConfig)
|
|
115
|
+
const vegaErrors = getVegaErrors(rawConfig, vegaConfig)
|
|
116
|
+
if (vegaErrors.length > 0) {
|
|
117
|
+
console.warn('Vega import errors:', vegaErrors)
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const configType = getVegaConfigType(vegaConfig)
|
|
122
|
+
const configSubType = configType === 'Map' ? 'United States (State- or County-Level)' : configType
|
|
123
|
+
const button = buttons.find(b => b.label === configSubType)
|
|
124
|
+
if (!button) {
|
|
125
|
+
console.warn(`No button found for config type "${configSubType}"`)
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const coveConfig = generateNewConfig(JSON.parse(JSON.stringify(button)))
|
|
130
|
+
try {
|
|
131
|
+
const warnings = getVegaWarnings(rawConfig, vegaConfig)
|
|
132
|
+
if (warnings.length) {
|
|
133
|
+
console.warn('Vega import warnings:', warnings)
|
|
134
|
+
}
|
|
135
|
+
const result = convertVegaConfig(configType, vegaConfig, coveConfig)
|
|
136
|
+
|
|
137
|
+
// Ensure imported Vega configs use the v1 qualitative palette so CdcChart
|
|
138
|
+
// doesn't apply v2 sequential-blue defaults (which would change the colors).
|
|
139
|
+
if (result && result.type !== 'map') {
|
|
140
|
+
result.general = result.general || {}
|
|
141
|
+
result.general.palette = {
|
|
142
|
+
name: 'qualitative-bold',
|
|
143
|
+
version: '1.0'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('Vega conversion error:', err)
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Check if a config is a Vega config and convert it; otherwise pass through. */
|
|
155
|
+
export const maybeConvertVega = async (config: any): Promise<any | null> => {
|
|
156
|
+
if (isVegaConfig(config)) {
|
|
157
|
+
return importVegaConfig(config)
|
|
158
|
+
}
|
|
159
|
+
return config
|
|
160
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import cloneConfig from '../cloneConfig'
|
|
2
|
+
import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
|
|
3
|
+
|
|
4
|
+
const normalizeFilterParents = config => {
|
|
5
|
+
if (config.type === 'dashboard') {
|
|
6
|
+
if (config.dashboard?.sharedFilters) {
|
|
7
|
+
config.dashboard.sharedFilters.forEach(filter => {
|
|
8
|
+
if (filter.type === 'datafilter' && filter.parents && typeof filter.parents === 'string') {
|
|
9
|
+
filter.parents = [filter.parents]
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const removeOldBrushKeys = config => {
|
|
17
|
+
if (config.type === 'chart') {
|
|
18
|
+
// Remove old brush object entirely - brush feature is new in 4.26.1
|
|
19
|
+
// and any existing brush config is from development/testing
|
|
20
|
+
delete config.brush
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (config.type === 'dashboard' && config.visualizations) {
|
|
24
|
+
Object.values((config as DashboardConfig).visualizations).forEach(visualization => {
|
|
25
|
+
removeOldBrushKeys(visualization)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const migrateTitleStyle = config => {
|
|
31
|
+
// Migrate ALL visualizations to use titleStyle
|
|
32
|
+
// Since titleStyle is new in 4.26.1, any config running this migration won't have it yet
|
|
33
|
+
// so we can unconditionally set it based on whether a title exists
|
|
34
|
+
// - If title exists and is not empty: use 'legacy' (preserve existing appearance)
|
|
35
|
+
// - If title is empty/missing: use 'small' (new default)
|
|
36
|
+
|
|
37
|
+
if (config.type === 'dashboard') {
|
|
38
|
+
// Migrate dashboard title
|
|
39
|
+
if (!config.dashboard) config.dashboard = {}
|
|
40
|
+
const hasTitle = config.dashboard.title && config.dashboard.title.trim() !== ''
|
|
41
|
+
config.dashboard.titleStyle = hasTitle ? 'legacy' : 'small'
|
|
42
|
+
|
|
43
|
+
// Migrate all visualizations in dashboard
|
|
44
|
+
if (config.visualizations) {
|
|
45
|
+
Object.values((config as DashboardConfig).visualizations).forEach(visualization => {
|
|
46
|
+
migrateTitleStyle(visualization)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
} else if (config.type === 'map') {
|
|
50
|
+
// Map stores titleStyle under general
|
|
51
|
+
if (!config.general) config.general = {}
|
|
52
|
+
const hasTitle = config.general.title && config.general.title.trim() !== ''
|
|
53
|
+
config.general.titleStyle = hasTitle ? 'legacy' : 'small'
|
|
54
|
+
} else if (config.type === 'markup-include') {
|
|
55
|
+
// Markup-include stores titleStyle under contentEditor (same location as title)
|
|
56
|
+
if (!config.contentEditor) config.contentEditor = {}
|
|
57
|
+
const hasTitle = config.contentEditor.title && config.contentEditor.title.trim() !== ''
|
|
58
|
+
config.contentEditor.titleStyle = hasTitle ? 'legacy' : 'small'
|
|
59
|
+
} else if (config.type === 'data-bite' || config.type === 'waffle-chart') {
|
|
60
|
+
// Data bites and waffle charts always use legacy title style - no migration needed
|
|
61
|
+
// The components hardcode 'legacy' for the Title component
|
|
62
|
+
} else if (config.type) {
|
|
63
|
+
// For all other visualization types (chart, filtered-text, etc.)
|
|
64
|
+
// titleStyle is at root level
|
|
65
|
+
const hasTitle = config.title && config.title.trim() !== ''
|
|
66
|
+
config.titleStyle = hasTitle ? 'legacy' : 'small'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const update_4_26_1 = config => {
|
|
71
|
+
const ver = '4.26.1'
|
|
72
|
+
const newConfig = cloneConfig(config)
|
|
73
|
+
normalizeFilterParents(newConfig)
|
|
74
|
+
removeOldBrushKeys(newConfig)
|
|
75
|
+
migrateTitleStyle(newConfig)
|
|
76
|
+
newConfig.version = ver
|
|
77
|
+
return newConfig
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default update_4_26_1
|