@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.
Files changed (77) hide show
  1. package/_stories/Gallery.Charts.stories.tsx +307 -0
  2. package/_stories/Gallery.DataBite.stories.tsx +72 -0
  3. package/_stories/Gallery.Maps.stories.tsx +230 -0
  4. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  5. package/_stories/PageART.stories.tsx +192 -0
  6. package/_stories/PageBRFSS.stories.tsx +289 -0
  7. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
  10. package/_stories/PageMaternalMortality.stories.tsx +192 -0
  11. package/_stories/PageOralHealth.stories.tsx +196 -0
  12. package/_stories/PageRespiratory.stories.tsx +332 -0
  13. package/_stories/PageSmokingTobacco.stories.tsx +195 -0
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
  15. package/_stories/PageWastewater.stories.tsx +463 -0
  16. package/assets/icon-magnifying-glass.svg +5 -0
  17. package/assets/icon-warming-stripes.svg +13 -0
  18. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  19. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  20. package/components/ComboBox/ComboBox.tsx +345 -0
  21. package/components/ComboBox/combobox.styles.css +185 -0
  22. package/components/ComboBox/index.ts +1 -0
  23. package/components/DataTable/DataTable.tsx +132 -58
  24. package/components/DataTable/data-table.css +216 -215
  25. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  26. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  27. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  28. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  29. package/components/EditorPanel/EditorPanel.tsx +144 -0
  30. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  31. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  32. package/components/EditorPanel/Inputs.tsx +33 -7
  33. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +236 -175
  34. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  35. package/components/Filters/Filters.tsx +31 -5
  36. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  37. package/components/Filters/helpers/handleSorting.ts +1 -1
  38. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  39. package/components/Layout/components/Visualization/index.tsx +16 -1
  40. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  41. package/components/Legend/Legend.Gradient.tsx +1 -1
  42. package/components/MediaControls.tsx +53 -27
  43. package/components/ui/Icon.tsx +3 -1
  44. package/components/ui/Title/index.tsx +30 -2
  45. package/components/ui/Title/title.styles.css +42 -0
  46. package/dist/cove-main.css +26 -3
  47. package/dist/cove-main.css.map +1 -1
  48. package/generateViteConfig.js +8 -1
  49. package/helpers/addValuesToFilters.ts +6 -1
  50. package/helpers/coveUpdateWorker.ts +19 -12
  51. package/helpers/embedCodeGenerator.ts +109 -0
  52. package/helpers/getUniqueValues.ts +19 -0
  53. package/helpers/hashObj.ts +25 -0
  54. package/helpers/isRightAlignedTableValue.js +5 -0
  55. package/helpers/metrics/helpers.ts +1 -0
  56. package/helpers/pivotData.ts +2 -2
  57. package/helpers/prepareScreenshot.ts +268 -0
  58. package/helpers/queryStringUtils.ts +29 -0
  59. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  60. package/helpers/tests/queryStringUtils.test.ts +381 -0
  61. package/helpers/tests/testStandaloneBuild.ts +23 -5
  62. package/helpers/useDataVizClasses.ts +0 -1
  63. package/helpers/ver/4.26.1.ts +80 -0
  64. package/hooks/useDataColumns.ts +63 -0
  65. package/hooks/useFilterManagement.ts +94 -0
  66. package/hooks/useLegendSeparators.ts +26 -0
  67. package/hooks/useListManagement.ts +192 -0
  68. package/package.json +4 -3
  69. package/styles/_button-section.scss +0 -3
  70. package/types/Axis.ts +1 -0
  71. package/types/ForecastingSeriesKey.ts +1 -0
  72. package/types/MarkupInclude.ts +1 -0
  73. package/types/Series.ts +3 -0
  74. package/types/Table.ts +1 -0
  75. package/types/Visualization.ts +1 -0
  76. package/types/VizFilter.ts +1 -0
  77. 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())) {