@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.
Files changed (147) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +300 -0
  4. package/_stories/Gallery.DataBite.stories.tsx +79 -0
  5. package/_stories/Gallery.Maps.stories.tsx +239 -0
  6. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  7. package/_stories/PageART.stories.tsx +193 -0
  8. package/_stories/PageBRFSS.stories.tsx +294 -0
  9. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
  12. package/_stories/PageMaternalMortality.stories.tsx +193 -0
  13. package/_stories/PageOralHealth.stories.tsx +201 -0
  14. package/_stories/PageRespiratory.stories.tsx +332 -0
  15. package/_stories/PageSmokingTobacco.stories.tsx +200 -0
  16. package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
  17. package/_stories/PageWastewater.stories.tsx +477 -0
  18. package/_stories/VegaImport.stories.tsx +401 -0
  19. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  20. package/_stories/vega-fixtures/bars.json +58 -0
  21. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  22. package/_stories/vega-fixtures/combo.json +68 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  25. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  26. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  27. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  28. package/_stories/vega-fixtures/lines.json +227 -0
  29. package/_stories/vega-fixtures/measles-bars.json +348 -0
  30. package/_stories/vega-fixtures/measles-map.json +11101 -0
  31. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  32. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  33. package/_stories/vega-fixtures/no-data.json +14 -0
  34. package/_stories/vega-fixtures/pie-chart.json +94 -0
  35. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  36. package/_stories/vega-fixtures/stacked-area.json +222 -0
  37. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  38. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  39. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  40. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  41. package/_stories/vega-fixtures/warning-combo.json +59 -0
  42. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  43. package/assets/icon-chart-area.svg +1 -0
  44. package/assets/icon-chart-radar.svg +23 -0
  45. package/assets/icon-magnifying-glass.svg +5 -0
  46. package/assets/icon-warming-stripes.svg +13 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  49. package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
  50. package/components/ComboBox/ComboBox.tsx +345 -0
  51. package/components/ComboBox/combobox.styles.css +185 -0
  52. package/components/ComboBox/index.ts +1 -0
  53. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  54. package/components/DataTable/DataTable.tsx +132 -58
  55. package/components/DataTable/data-table.css +216 -215
  56. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  57. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  58. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  59. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  60. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  61. package/components/EditorPanel/EditorPanel.tsx +144 -0
  62. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  63. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  64. package/components/EditorPanel/Inputs.tsx +33 -7
  65. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  66. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
  67. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  68. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  69. package/components/Filters/Filters.tsx +31 -5
  70. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  71. package/components/Filters/helpers/handleSorting.ts +1 -1
  72. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
  73. package/components/Layout/components/Visualization/index.tsx +27 -1
  74. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  75. package/components/Legend/Legend.Gradient.tsx +1 -1
  76. package/components/MediaControls.tsx +53 -28
  77. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  78. package/components/_stories/DataTable.stories.tsx +1 -0
  79. package/components/ui/Icon.tsx +3 -1
  80. package/components/ui/Title/index.tsx +30 -2
  81. package/components/ui/Title/title.styles.css +42 -0
  82. package/data/colorPalettes.ts +18 -5
  83. package/data/mapColorPalettes.ts +10 -0
  84. package/devTemplate/dev.js +235 -0
  85. package/devTemplate/index.html +30 -0
  86. package/devTemplate/preview.html +1503 -0
  87. package/devTemplate/sidebar.css +151 -0
  88. package/dist/cove-main.css +2803 -4448
  89. package/dist/cove-main.css.map +1 -1
  90. package/generateViteConfig.js +118 -2
  91. package/helpers/DataTransform.ts +1 -5
  92. package/helpers/addValuesToFilters.ts +6 -1
  93. package/helpers/cove/date.ts +33 -1
  94. package/helpers/cove/string.ts +29 -0
  95. package/helpers/coveUpdateWorker.ts +21 -12
  96. package/helpers/embed/embedCodeGenerator.ts +80 -0
  97. package/helpers/embed/embedHelper.js +158 -0
  98. package/helpers/embed/filterUtils.ts +121 -0
  99. package/helpers/embed/index.ts +21 -0
  100. package/helpers/embed/urlValidation.ts +119 -0
  101. package/helpers/filterVizData.ts +6 -1
  102. package/helpers/getFileExtension.ts +0 -6
  103. package/helpers/getUniqueValues.ts +19 -0
  104. package/helpers/hashObj.ts +25 -0
  105. package/helpers/isRightAlignedTableValue.js +5 -0
  106. package/helpers/metrics/helpers.ts +1 -0
  107. package/helpers/metrics/types.ts +3 -0
  108. package/helpers/palettes/colorDistributions.ts +1 -1
  109. package/helpers/palettes/utils.ts +12 -12
  110. package/helpers/parseCsvWithQuotes.ts +15 -14
  111. package/helpers/pivotData.ts +2 -2
  112. package/helpers/prepareScreenshot.ts +288 -0
  113. package/helpers/queryStringUtils.ts +29 -0
  114. package/helpers/testing.ts +44 -0
  115. package/helpers/tests/DataTransform.test.ts +125 -0
  116. package/helpers/tests/date.test.ts +64 -0
  117. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  118. package/helpers/tests/queryStringUtils.test.ts +381 -0
  119. package/helpers/tests/testStandaloneBuild.ts +23 -5
  120. package/helpers/useDataVizClasses.ts +0 -1
  121. package/helpers/vegaConfig.ts +1 -1
  122. package/helpers/vegaConfigImport.ts +160 -0
  123. package/helpers/ver/4.26.1.ts +80 -0
  124. package/helpers/ver/4.26.2.ts +84 -0
  125. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  126. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  127. package/helpers/viewports.ts +2 -0
  128. package/hooks/useDataColumns.ts +63 -0
  129. package/hooks/useFilterManagement.ts +94 -0
  130. package/hooks/useLegendSeparators.ts +26 -0
  131. package/hooks/useListManagement.ts +192 -0
  132. package/package.json +29 -33
  133. package/styles/_button-section.scss +0 -3
  134. package/styles/v2/components/editor.scss +9 -9
  135. package/styles/v2/utils/_grid.scss +8 -3
  136. package/types/Annotation.ts +10 -11
  137. package/types/Axis.ts +1 -0
  138. package/types/ForecastingSeriesKey.ts +1 -0
  139. package/types/General.ts +2 -0
  140. package/types/MarkupInclude.ts +1 -0
  141. package/types/Palette.ts +21 -0
  142. package/types/Series.ts +3 -0
  143. package/types/Table.ts +1 -0
  144. package/types/Visualization.ts +7 -0
  145. package/types/VizFilter.ts +1 -0
  146. package/LICENSE +0 -201
  147. 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
- execSync('npm link @cdc/core', { cwd: tmpDir })
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
  }
@@ -1,4 +1,3 @@
1
- import useResizeObserver from '@cdc/map/src/hooks/useResizeObserver'
2
1
  import { isBelowBreakpoint } from './viewports'
3
2
 
4
3
  export default function useDataVizClasses(config, viewport = null) {
@@ -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 { _ } from 'lodash'
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