@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,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for preparing screenshot containers for image downloads
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ClonedElements {
|
|
6
|
+
clonedTree: HTMLElement
|
|
7
|
+
clonedViz: HTMLElement
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface VisualizationWrapper {
|
|
11
|
+
parentChildren: Element[]
|
|
12
|
+
wrapperIndex: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Walk up from viz to find a parent container that has:
|
|
17
|
+
* - Multiple children
|
|
18
|
+
* - At least one child (before viz) that contains H2/H3 but NOT the viz
|
|
19
|
+
*
|
|
20
|
+
* Stops climbing at section boundaries (never goes above <section> or .dfe-section)
|
|
21
|
+
*/
|
|
22
|
+
function findParentWithContext(viz: HTMLElement): HTMLElement | null {
|
|
23
|
+
let current = viz.parentElement
|
|
24
|
+
|
|
25
|
+
while (current) {
|
|
26
|
+
const isSection = current.tagName === 'SECTION' || current.classList.contains('dfe-section')
|
|
27
|
+
|
|
28
|
+
const children = Array.from(current.children)
|
|
29
|
+
|
|
30
|
+
// Need at least 2 children (heading + viz wrapper)
|
|
31
|
+
if (children.length < 2) {
|
|
32
|
+
if (isSection) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
current = current.parentElement
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Find which child contains the viz (guaranteed to exist since we're walking up from viz)
|
|
40
|
+
const vizChildIndex = children.findIndex(child => child.contains(viz))
|
|
41
|
+
|
|
42
|
+
// Check children before the viz for heading
|
|
43
|
+
for (let i = 0; i < vizChildIndex; i++) {
|
|
44
|
+
const child = children[i] as HTMLElement
|
|
45
|
+
const isHeading = child.tagName === 'H2' || child.tagName === 'H3'
|
|
46
|
+
const containsHeading = child.querySelector('h2, h3') !== null
|
|
47
|
+
|
|
48
|
+
if (isHeading || containsHeading) {
|
|
49
|
+
return current
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Don't climb above section boundaries
|
|
54
|
+
if (isSection) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// No suitable heading found in this parent's children, go up
|
|
59
|
+
current = current.parentElement
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find which direct child of the parent container contains the visualization
|
|
67
|
+
*/
|
|
68
|
+
function findVisualizationWrapper(viz: HTMLElement, parent: Element): VisualizationWrapper {
|
|
69
|
+
const parentChildren = Array.from(parent.children)
|
|
70
|
+
const wrapperIndex = parentChildren.findIndex(child => child.contains(viz))
|
|
71
|
+
return { parentChildren, wrapperIndex }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Walk backwards from viz to find nearest H2 or H3
|
|
76
|
+
* Stops early if another visualization is encountered
|
|
77
|
+
*/
|
|
78
|
+
function findNearestHeadingIndex(parentChildren: Element[], vizWrapperIndex: number): number {
|
|
79
|
+
for (let i = vizWrapperIndex - 1; i >= 0; i--) {
|
|
80
|
+
const child = parentChildren[i] as HTMLElement
|
|
81
|
+
|
|
82
|
+
// STOP: Another visualization found - don't include its content
|
|
83
|
+
if (child.classList.contains('cdc-open-viz-module') || child.querySelector('.cdc-open-viz-module')) {
|
|
84
|
+
return -1
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Found direct H2/H3
|
|
88
|
+
if (child.tagName === 'H2' || child.tagName === 'H3') {
|
|
89
|
+
return i
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Found nested H2/H3
|
|
93
|
+
if (child.querySelector('h2, h3')) {
|
|
94
|
+
return i
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return -1 // No heading found
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build cloned parent container with only selected children (heading through viz)
|
|
103
|
+
*/
|
|
104
|
+
function buildContextClone(
|
|
105
|
+
parent: Element,
|
|
106
|
+
headingIndex: number,
|
|
107
|
+
vizWrapperIndex: number,
|
|
108
|
+
elementToCapture: string
|
|
109
|
+
): ClonedElements {
|
|
110
|
+
const parentChildren = Array.from(parent.children)
|
|
111
|
+
|
|
112
|
+
// Shallow clone parent (preserves classes/styling)
|
|
113
|
+
const clonedTree = parent.cloneNode(false) as HTMLElement
|
|
114
|
+
|
|
115
|
+
// Append only selected children (heading → viz wrapper)
|
|
116
|
+
for (let i = headingIndex; i <= vizWrapperIndex; i++) {
|
|
117
|
+
clonedTree.appendChild(parentChildren[i].cloneNode(true))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Find viz in cloned tree
|
|
121
|
+
const clonedViz = clonedTree.querySelector(`[data-download-id="${elementToCapture}"]`) as HTMLElement
|
|
122
|
+
|
|
123
|
+
// Container styling adjustments
|
|
124
|
+
clonedTree.style.marginBottom = '0'
|
|
125
|
+
|
|
126
|
+
// Remove top margin from context headings (not inside viz)
|
|
127
|
+
const allHeadings = clonedTree.querySelectorAll('h2, h3')
|
|
128
|
+
allHeadings.forEach(heading => {
|
|
129
|
+
if (!clonedViz.contains(heading)) {
|
|
130
|
+
;(heading as HTMLElement).style.marginTop = '0'
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return { clonedTree, clonedViz }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isInEditorMode(element: HTMLElement): boolean {
|
|
138
|
+
return element.closest('.cdc-open-viz-module.isEditor') !== null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Prepare cloned elements (with or without context)
|
|
143
|
+
* Returns viz-only clone as fallback for any failure
|
|
144
|
+
* Exported for testing
|
|
145
|
+
*/
|
|
146
|
+
export function prepareClonedElements(
|
|
147
|
+
baseSvg: HTMLElement,
|
|
148
|
+
includeContextInDownload: boolean,
|
|
149
|
+
elementToCapture: string
|
|
150
|
+
): ClonedElements {
|
|
151
|
+
const defaultClone = (): ClonedElements => {
|
|
152
|
+
const tree = baseSvg.cloneNode(true) as HTMLElement
|
|
153
|
+
return { clonedTree: tree, clonedViz: tree }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Early return: context not requested or in editor mode (editor downloads should not include context)
|
|
157
|
+
if (!includeContextInDownload || isInEditorMode(baseSvg)) {
|
|
158
|
+
return defaultClone()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Early return: no parent with context found
|
|
162
|
+
const parent = findParentWithContext(baseSvg)
|
|
163
|
+
if (!parent) {
|
|
164
|
+
return defaultClone()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get viz wrapper (guaranteed to exist since findParentWithContext found it)
|
|
168
|
+
const { parentChildren, wrapperIndex } = findVisualizationWrapper(baseSvg, parent as Element)
|
|
169
|
+
|
|
170
|
+
// Early return: no heading found (or another viz in the way)
|
|
171
|
+
const headingIndex = findNearestHeadingIndex(parentChildren, wrapperIndex)
|
|
172
|
+
if (headingIndex < 0) {
|
|
173
|
+
return defaultClone()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Build with context
|
|
177
|
+
return buildContextClone(parent, headingIndex, wrapperIndex, elementToCapture)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Strip all <a> tags from cloned tree (links aren't clickable in PNG)
|
|
182
|
+
*/
|
|
183
|
+
function stripLinks(clonedTree: HTMLElement): void {
|
|
184
|
+
const allLinks = clonedTree.querySelectorAll('a')
|
|
185
|
+
allLinks.forEach(link => {
|
|
186
|
+
const textNode = document.createTextNode(link.textContent || '')
|
|
187
|
+
link.parentNode?.replaceChild(textNode, link)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Convert canvas elements to images (canvas pixel data doesn't clone)
|
|
193
|
+
*/
|
|
194
|
+
function convertCanvasToImages(baseSvg: HTMLElement, clonedViz: HTMLElement): void {
|
|
195
|
+
const originalCanvases = baseSvg.querySelectorAll('canvas')
|
|
196
|
+
const clonedCanvases = clonedViz.querySelectorAll('canvas')
|
|
197
|
+
|
|
198
|
+
clonedCanvases.forEach((clonedCanvas, index) => {
|
|
199
|
+
const originalCanvas = originalCanvases[index] as HTMLCanvasElement
|
|
200
|
+
if (originalCanvas && originalCanvas.width > 0 && originalCanvas.height > 0) {
|
|
201
|
+
const img = document.createElement('img')
|
|
202
|
+
img.src = originalCanvas.toDataURL('image/png')
|
|
203
|
+
img.width = originalCanvas.width
|
|
204
|
+
img.height = originalCanvas.height
|
|
205
|
+
img.className = originalCanvas.className
|
|
206
|
+
clonedCanvas.parentNode?.replaceChild(img, clonedCanvas)
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Expand SVG widths and remove animation classes
|
|
213
|
+
*/
|
|
214
|
+
function expandSvgWidths(clonedViz: HTMLElement): void {
|
|
215
|
+
const svgWidthBuffer = 25
|
|
216
|
+
const svgElements = clonedViz.querySelectorAll('svg')
|
|
217
|
+
|
|
218
|
+
svgElements.forEach(svg => {
|
|
219
|
+
const currentWidth = parseInt(svg.getAttribute('width') || '0')
|
|
220
|
+
if (currentWidth > 0) {
|
|
221
|
+
svg.setAttribute('width', (currentWidth + svgWidthBuffer).toString())
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Remove animation classes to show final state immediately
|
|
225
|
+
svg.classList.remove('animated', 'animate')
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Main function: Prepare a complete container ready for html2canvas screenshot
|
|
231
|
+
*/
|
|
232
|
+
export function prepareScreenshotContainer(
|
|
233
|
+
baseSvg: HTMLElement,
|
|
234
|
+
includeContextInDownload: boolean,
|
|
235
|
+
elementToCapture: string
|
|
236
|
+
): HTMLDivElement {
|
|
237
|
+
// 1. Clone elements (with or without context)
|
|
238
|
+
const { clonedTree, clonedViz } = prepareClonedElements(baseSvg, includeContextInDownload, elementToCapture)
|
|
239
|
+
|
|
240
|
+
// 2. Strip all links (not clickable in static image)
|
|
241
|
+
stripLinks(clonedTree)
|
|
242
|
+
|
|
243
|
+
// 3. Convert canvas elements to images
|
|
244
|
+
convertCanvasToImages(baseSvg, clonedViz)
|
|
245
|
+
|
|
246
|
+
// 4. Expand SVG widths to prevent clipping
|
|
247
|
+
expandSvgWidths(clonedViz)
|
|
248
|
+
|
|
249
|
+
// 5. Calculate viz dimensions
|
|
250
|
+
const computedStyle = getComputedStyle(baseSvg)
|
|
251
|
+
const vizWidth =
|
|
252
|
+
parseFloat(computedStyle.width) -
|
|
253
|
+
(parseFloat(computedStyle.paddingLeft) || 0) -
|
|
254
|
+
(parseFloat(computedStyle.paddingRight) || 0)
|
|
255
|
+
|
|
256
|
+
// 6. Create and style container
|
|
257
|
+
const container = document.createElement('div')
|
|
258
|
+
container.style.width = `${vizWidth + 36}px`
|
|
259
|
+
container.style.padding = '18px'
|
|
260
|
+
|
|
261
|
+
// 7. Reset viz padding
|
|
262
|
+
clonedViz.style.padding = '0'
|
|
263
|
+
|
|
264
|
+
// 8. Append cloned tree to container
|
|
265
|
+
container.appendChild(clonedTree)
|
|
266
|
+
|
|
267
|
+
return container
|
|
268
|
+
}
|
|
@@ -13,6 +13,35 @@ export function getQueryStringFilterValue(filter) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Check if a filter should be hidden based on URL query parameters
|
|
18
|
+
* Checks for hide{setByQueryParameter}=true|1|yes in the URL
|
|
19
|
+
*
|
|
20
|
+
* @param filter - Filter object with setByQueryParameter property
|
|
21
|
+
* @returns true if filter should be hidden, false otherwise
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // If filter has setByQueryParameter: "state"
|
|
25
|
+
* // URL: ?hidestate=true -> returns true
|
|
26
|
+
* // URL: ?hideState=true -> returns false (case sensitive!)
|
|
27
|
+
*/
|
|
28
|
+
export function isFilterHiddenByQuery(filter) {
|
|
29
|
+
if (!filter) return false
|
|
30
|
+
|
|
31
|
+
// Only check setByQueryParameter - this is the standard way filters are identified in URL params
|
|
32
|
+
const paramName = filter.setByQueryParameter
|
|
33
|
+
if (!paramName) return false
|
|
34
|
+
|
|
35
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
36
|
+
const hideParamName = `hide${paramName}`
|
|
37
|
+
const hideValue = urlParams.get(hideParamName)
|
|
38
|
+
|
|
39
|
+
if (!hideValue) return false
|
|
40
|
+
|
|
41
|
+
const lower = hideValue.toLowerCase()
|
|
42
|
+
return lower === 'true' || hideValue === '1' || lower === 'yes'
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
export function getQueryParams() {
|
|
17
46
|
const queryParams = {}
|
|
18
47
|
for (const [key, value] of Array.from(new URLSearchParams(window.location.search).entries())) {
|