@cdc/core 4.25.11 → 4.26.1
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/_stories/Gallery.Charts.stories.tsx +307 -0
- package/_stories/Gallery.DataBite.stories.tsx +72 -0
- package/_stories/Gallery.Maps.stories.tsx +230 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +192 -0
- package/_stories/PageBRFSS.stories.tsx +289 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
- package/_stories/PageMaternalMortality.stories.tsx +192 -0
- package/_stories/PageOralHealth.stories.tsx +196 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +195 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
- package/_stories/PageWastewater.stories.tsx +463 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +281 -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/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- 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/VizFilterEditor.tsx +236 -175
- 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 +82 -0
- package/components/Layout/components/Visualization/index.tsx +16 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -27
- 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/dist/cove-main.css +26 -3
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +8 -1
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/coveUpdateWorker.ts +19 -12
- package/helpers/embedCodeGenerator.ts +109 -0
- 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/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +268 -0
- package/helpers/queryStringUtils.ts +29 -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/ver/4.26.1.ts +80 -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 +4 -3
- package/styles/_button-section.scss +0 -3
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +1 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
|
@@ -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
|
}
|
|
@@ -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') {
|
|
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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface UseDataColumnsOptions {
|
|
4
|
+
/** Columns to exclude from the result */
|
|
5
|
+
excludeColumns?: string[]
|
|
6
|
+
/** Include only columns with specific data types */
|
|
7
|
+
dataTypes?: ('string' | 'number' | 'boolean' | 'date')[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts unique column names from data with memoization
|
|
12
|
+
*
|
|
13
|
+
* Performance optimization: Replaces the common getColumns() pattern
|
|
14
|
+
* that was duplicated across multiple packages and called multiple times per render.
|
|
15
|
+
*
|
|
16
|
+
* @param data - Array of data objects
|
|
17
|
+
* @param options - Optional configuration for filtering columns
|
|
18
|
+
* @returns Sorted array of unique column names
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Basic usage
|
|
22
|
+
* const columns = useDataColumns(data)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // With exclusions
|
|
26
|
+
* const columns = useDataColumns(data, { excludeColumns: [config.groupBy] })
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Filter by data type
|
|
30
|
+
* const numericColumns = useDataColumns(data, { dataTypes: ['number'] })
|
|
31
|
+
*/
|
|
32
|
+
export const useDataColumns = (data: any[], options?: UseDataColumnsOptions): string[] => {
|
|
33
|
+
const { excludeColumns = [], dataTypes } = options || {}
|
|
34
|
+
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
if (!data?.length) return []
|
|
37
|
+
|
|
38
|
+
const columnsSet = new Set<string>()
|
|
39
|
+
|
|
40
|
+
// Single iteration through all rows (optimized from previous pattern)
|
|
41
|
+
data.forEach(row => {
|
|
42
|
+
Object.keys(row).forEach(columnName => {
|
|
43
|
+
if (excludeColumns.includes(columnName)) return
|
|
44
|
+
|
|
45
|
+
// Optional: filter by data type
|
|
46
|
+
if (dataTypes && dataTypes.length > 0) {
|
|
47
|
+
const value = row[columnName]
|
|
48
|
+
const valueType = typeof value
|
|
49
|
+
if (!dataTypes.includes(valueType as any)) return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
columnsSet.add(columnName)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return Array.from(columnsSet).sort()
|
|
57
|
+
}, [
|
|
58
|
+
data,
|
|
59
|
+
// Stringify arrays for stable dependency tracking
|
|
60
|
+
excludeColumns.join(','),
|
|
61
|
+
dataTypes?.join(',')
|
|
62
|
+
])
|
|
63
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hook for managing filter operations in visualizations
|
|
3
|
+
*
|
|
4
|
+
* Provides common filter management functionality including:
|
|
5
|
+
* - Adding new filters
|
|
6
|
+
* - Removing filters
|
|
7
|
+
* - Updating filter properties
|
|
8
|
+
* - Getting unique column values for filter options
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { addNewFilter, removeFilter, updateFilterProp, getFilterColumnValues } =
|
|
13
|
+
* useFilterManagement(config, updateConfig, data)
|
|
14
|
+
*
|
|
15
|
+
* // Add a new filter
|
|
16
|
+
* <Button onClick={addNewFilter}>Add Filter</Button>
|
|
17
|
+
*
|
|
18
|
+
* // Remove a filter
|
|
19
|
+
* <button onClick={() => removeFilter(index)}>Remove</button>
|
|
20
|
+
*
|
|
21
|
+
* // Update filter property
|
|
22
|
+
* <Select onChange={e => updateFilterProp('columnName', index, e.target.value)} />
|
|
23
|
+
*
|
|
24
|
+
* // Get column values for filter dropdown
|
|
25
|
+
* <Select options={getFilterColumnValues(index)} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const useFilterManagement = <TConfig extends { filters?: any[] }>(
|
|
29
|
+
config: TConfig,
|
|
30
|
+
updateConfig: (config: TConfig) => void,
|
|
31
|
+
data: any[]
|
|
32
|
+
) => {
|
|
33
|
+
/**
|
|
34
|
+
* Adds a new empty filter to the config
|
|
35
|
+
*/
|
|
36
|
+
const addNewFilter = () => {
|
|
37
|
+
const filters = config.filters ? [...config.filters] : []
|
|
38
|
+
filters.push({ values: [] })
|
|
39
|
+
updateConfig({ ...config, filters })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Removes a filter at the specified index
|
|
44
|
+
* @param index - The index of the filter to remove
|
|
45
|
+
*/
|
|
46
|
+
const removeFilter = (index: number) => {
|
|
47
|
+
const filters = [...(config.filters || [])]
|
|
48
|
+
filters.splice(index, 1)
|
|
49
|
+
updateConfig({ ...config, filters })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Updates a specific property of a filter
|
|
54
|
+
* @param name - The property name to update
|
|
55
|
+
* @param index - The index of the filter
|
|
56
|
+
* @param value - The new value for the property
|
|
57
|
+
*/
|
|
58
|
+
const updateFilterProp = (name: string, index: number, value: any) => {
|
|
59
|
+
const filters = [...(config.filters || [])]
|
|
60
|
+
filters[index][name] = value
|
|
61
|
+
updateConfig({ ...config, filters })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets unique values from a data column for filter options
|
|
66
|
+
* @param index - The index of the filter (to get its columnName)
|
|
67
|
+
* @returns Array of unique, sorted values from the specified column
|
|
68
|
+
*/
|
|
69
|
+
const getFilterColumnValues = (index: number): any[] => {
|
|
70
|
+
const filterDataOptions: any[] = []
|
|
71
|
+
const filterColumnName = config.filters?.[index]?.columnName
|
|
72
|
+
|
|
73
|
+
// Return empty array if no column name or no data
|
|
74
|
+
if (!filterColumnName || !data || !Array.isArray(data) || data.length === 0) {
|
|
75
|
+
return filterDataOptions
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
data.forEach(function (row: any) {
|
|
79
|
+
if (undefined !== row[filterColumnName] && -1 === filterDataOptions.indexOf(row[filterColumnName])) {
|
|
80
|
+
filterDataOptions.push(row[filterColumnName])
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
filterDataOptions.sort()
|
|
84
|
+
|
|
85
|
+
return filterDataOptions
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
addNewFilter,
|
|
90
|
+
removeFilter,
|
|
91
|
+
updateFilterProp,
|
|
92
|
+
getFilterColumnValues
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { clamp } from 'lodash'
|
|
2
|
+
|
|
3
|
+
// TODO: generalize this to be used in legends other than linear block gradient
|
|
4
|
+
|
|
5
|
+
const LEGEND_SEPARATOR_SIZE = 0.02
|
|
6
|
+
const LEGEND_SEPARATOR_SIZE_MAX = 20
|
|
7
|
+
const LEGEND_SEPARATOR_SIZE_MIN = 8
|
|
8
|
+
|
|
9
|
+
const useLegendSeparators = (separators, legendWidth, allowsLegendSeparators) => {
|
|
10
|
+
const legendSeparators = allowsLegendSeparators
|
|
11
|
+
? separators?.replace(' ', '').split(',').map(Number).filter(Boolean) || []
|
|
12
|
+
: []
|
|
13
|
+
const separatorSize = clamp(legendWidth * LEGEND_SEPARATOR_SIZE, LEGEND_SEPARATOR_SIZE_MIN, LEGEND_SEPARATOR_SIZE_MAX)
|
|
14
|
+
const legendSeparatorsToSubtract = legendSeparators.length * separatorSize
|
|
15
|
+
const getTickSeparatorsAdjustment = (index: number) =>
|
|
16
|
+
legendSeparators.reduce((acc, separators) => (index >= separators ? acc + separatorSize : acc), 0)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
legendSeparators,
|
|
20
|
+
separatorSize,
|
|
21
|
+
legendSeparatorsToSubtract,
|
|
22
|
+
getTickSeparatorsAdjustment
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default useLegendSeparators
|