@cdc/core 4.25.10 → 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 (134) 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/_stories/StoryRenderingTests.stories.tsx +164 -0
  17. package/assets/icon-magnifying-glass.svg +5 -0
  18. package/assets/icon-warming-stripes.svg +13 -0
  19. package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
  20. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  21. package/components/ComboBox/ComboBox.tsx +345 -0
  22. package/components/ComboBox/combobox.styles.css +185 -0
  23. package/components/ComboBox/index.ts +1 -0
  24. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  25. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  26. package/components/CustomColorsEditor/index.ts +1 -0
  27. package/components/DataTable/DataTable.tsx +132 -58
  28. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  29. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  30. package/components/DataTable/data-table.css +217 -210
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
  32. package/components/DataTable/helpers/standardizeState.js +2 -2
  33. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  34. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  35. package/components/EditorPanel/DataTableEditor.tsx +54 -28
  36. package/components/EditorPanel/EditorPanel.styles.css +439 -0
  37. package/components/EditorPanel/EditorPanel.tsx +144 -0
  38. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  39. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  40. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  41. package/components/EditorPanel/Inputs.tsx +44 -8
  42. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  43. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
  44. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  45. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  46. package/components/Filters/Filters.tsx +57 -10
  47. package/components/Filters/components/Dropdown.tsx +6 -1
  48. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  49. package/components/Filters/helpers/handleSorting.ts +1 -1
  50. package/components/Footnotes/Footnotes.tsx +35 -25
  51. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  52. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  54. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  55. package/components/HeaderThemeSelector/index.ts +2 -0
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  57. package/components/Layout/components/Visualization/index.tsx +16 -1
  58. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  59. package/components/Layout/styles/editor.scss +2 -1
  60. package/components/Legend/Legend.Gradient.tsx +1 -1
  61. package/components/Loader/Loader.tsx +1 -1
  62. package/components/MediaControls.tsx +63 -34
  63. package/components/PaletteConversionModal.tsx +7 -4
  64. package/components/PaletteSelector/PaletteSelector.css +49 -6
  65. package/components/Table/components/Cell.tsx +23 -2
  66. package/components/Table/components/Row.tsx +5 -3
  67. package/components/_stories/Filters.stories.tsx +20 -1
  68. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  69. package/components/_stories/Footnotes.stories.tsx +768 -3
  70. package/components/_stories/Inputs.stories.tsx +2 -2
  71. package/components/_stories/styles.scss +0 -1
  72. package/components/ui/Accordion.jsx +1 -1
  73. package/components/ui/Icon.tsx +3 -1
  74. package/components/ui/Title/index.tsx +30 -2
  75. package/components/ui/Title/title.styles.css +42 -0
  76. package/components/ui/accordion.styles.css +57 -0
  77. package/data/chartColorPalettes.ts +1 -1
  78. package/dist/cove-main.css +75 -6
  79. package/dist/cove-main.css.map +1 -1
  80. package/generateViteConfig.js +8 -1
  81. package/helpers/addValuesToFilters.ts +11 -1
  82. package/helpers/constants.ts +37 -0
  83. package/helpers/cove/number.ts +33 -12
  84. package/helpers/coveUpdateWorker.ts +20 -11
  85. package/helpers/embedCodeGenerator.ts +109 -0
  86. package/helpers/fetchRemoteData.ts +3 -15
  87. package/helpers/getUniqueValues.ts +19 -0
  88. package/helpers/hashObj.ts +25 -0
  89. package/helpers/isRightAlignedTableValue.js +5 -0
  90. package/helpers/markupProcessor.ts +27 -12
  91. package/helpers/mergeCustomOrderValues.ts +37 -0
  92. package/helpers/metrics/helpers.ts +1 -0
  93. package/helpers/parseCsvWithQuotes.ts +65 -0
  94. package/helpers/pivotData.ts +2 -2
  95. package/helpers/prepareScreenshot.ts +268 -0
  96. package/helpers/queryStringUtils.ts +29 -0
  97. package/helpers/testing.ts +17 -4
  98. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  99. package/helpers/tests/queryStringUtils.test.ts +381 -0
  100. package/helpers/tests/testStandaloneBuild.ts +23 -5
  101. package/helpers/useDataVizClasses.ts +0 -1
  102. package/helpers/ver/4.25.11.ts +13 -0
  103. package/helpers/ver/4.26.1.ts +80 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useDataColumns.ts +63 -0
  106. package/hooks/useFilterManagement.ts +94 -0
  107. package/hooks/useLegendSeparators.ts +26 -0
  108. package/hooks/useListManagement.ts +192 -0
  109. package/package.json +6 -4
  110. package/styles/_button-section.scss +0 -3
  111. package/styles/_common-components.css +73 -0
  112. package/styles/_global.scss +25 -5
  113. package/styles/base.scss +0 -50
  114. package/styles/cove-main.scss +3 -1
  115. package/styles/filters.scss +10 -3
  116. package/styles/v2/base/index.scss +0 -1
  117. package/styles/v2/components/editor.scss +14 -6
  118. package/styles/v2/utils/_breakpoints.scss +1 -1
  119. package/styles/v2/utils/index.scss +0 -1
  120. package/styles/waiting.scss +1 -1
  121. package/types/Axis.ts +1 -0
  122. package/types/ForecastingSeriesKey.ts +1 -0
  123. package/types/MarkupInclude.ts +5 -3
  124. package/types/MarkupVariable.ts +1 -1
  125. package/types/Series.ts +3 -0
  126. package/types/Table.ts +1 -0
  127. package/types/Visualization.ts +1 -0
  128. package/types/VizFilter.ts +2 -0
  129. package/LICENSE +0 -201
  130. package/styles/_mixins.scss +0 -13
  131. package/styles/_typography.scss +0 -0
  132. package/styles/v2/base/_typography.scss +0 -0
  133. package/styles/v2/components/guidance-block.scss +0 -74
  134. package/styles/v2/utils/_functions.scss +0 -0
@@ -0,0 +1,74 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import HeaderThemeSelector from '../HeaderThemeSelector'
3
+
4
+ const meta: Meta<typeof HeaderThemeSelector> = {
5
+ title: 'Components/Atoms/HeaderThemeSelector',
6
+ component: HeaderThemeSelector,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: 'A reusable component for selecting header themes across different visualization types.'
11
+ }
12
+ }
13
+ },
14
+ argTypes: {
15
+ onThemeSelect: { action: 'theme-selected' },
16
+ selectedTheme: {
17
+ control: 'select',
18
+ options: [
19
+ 'theme-blue',
20
+ 'theme-purple',
21
+ 'theme-brown',
22
+ 'theme-teal',
23
+ 'theme-pink',
24
+ 'theme-orange',
25
+ 'theme-slate',
26
+ 'theme-indigo',
27
+ 'theme-cyan',
28
+ 'theme-green',
29
+ 'theme-amber'
30
+ ]
31
+ }
32
+ }
33
+ } satisfies Meta<typeof HeaderThemeSelector>
34
+
35
+ export default meta
36
+ type Story = StoryObj<typeof meta>
37
+
38
+ const defaultHeaderColors = [
39
+ 'theme-blue',
40
+ 'theme-purple',
41
+ 'theme-brown',
42
+ 'theme-teal',
43
+ 'theme-pink',
44
+ 'theme-orange',
45
+ 'theme-slate',
46
+ 'theme-indigo',
47
+ 'theme-cyan',
48
+ 'theme-green',
49
+ 'theme-amber'
50
+ ]
51
+
52
+ export const Default: Story = {
53
+ args: {}
54
+ }
55
+
56
+ export const WithSelectedTheme: Story = {
57
+ args: {
58
+ selectedTheme: 'theme-purple'
59
+ }
60
+ }
61
+
62
+ export const CustomLabel: Story = {
63
+ args: {
64
+ label: 'Choose Color Theme',
65
+ selectedTheme: 'theme-teal'
66
+ }
67
+ }
68
+
69
+ export const CustomColors: Story = {
70
+ args: {
71
+ headerColors: ['theme-blue', 'theme-purple', 'theme-orange', 'theme-green'],
72
+ selectedTheme: 'theme-blue'
73
+ }
74
+ }
@@ -0,0 +1,61 @@
1
+ import React from 'react'
2
+ import './HeaderThemeSelector.css'
3
+
4
+ // Default header theme colors used across all CDC Open Viz packages
5
+ const DEFAULT_HEADER_COLORS = [
6
+ 'theme-blue',
7
+ 'theme-purple',
8
+ 'theme-brown',
9
+ 'theme-teal',
10
+ 'theme-pink',
11
+ 'theme-orange',
12
+ 'theme-slate',
13
+ 'theme-indigo',
14
+ 'theme-cyan',
15
+ 'theme-green',
16
+ 'theme-amber'
17
+ ]
18
+
19
+ interface HeaderThemeSelectorProps {
20
+ /** Array of theme color names to display. Defaults to standard CDC theme colors */
21
+ headerColors?: string[]
22
+ /** Currently selected theme */
23
+ selectedTheme?: string
24
+ /** Callback when a theme is selected */
25
+ onThemeSelect: (theme: string) => void
26
+ /** Optional label for the selector */
27
+ label?: string
28
+ /** Optional CSS class name */
29
+ className?: string
30
+ }
31
+
32
+ const HeaderThemeSelector: React.FC<HeaderThemeSelectorProps> = ({
33
+ headerColors = DEFAULT_HEADER_COLORS,
34
+ selectedTheme,
35
+ onThemeSelect,
36
+ label = 'Header Theme',
37
+ className = 'color-palette'
38
+ }) => {
39
+ const handleThemeSelection = (theme: string) => (e: React.MouseEvent) => {
40
+ e.preventDefault()
41
+ onThemeSelect(theme)
42
+ }
43
+
44
+ return (
45
+ <label className='header'>
46
+ <span className='edit-label'>{label}</span>
47
+ <ul className={className}>
48
+ {headerColors.map(theme => (
49
+ <button
50
+ title={theme}
51
+ key={theme}
52
+ onClick={handleThemeSelection(theme)}
53
+ className={selectedTheme === theme ? `selected ${theme}` : theme}
54
+ />
55
+ ))}
56
+ </ul>
57
+ </label>
58
+ )
59
+ }
60
+
61
+ export default HeaderThemeSelector
@@ -0,0 +1,2 @@
1
+ export { default as HeaderThemeSelector } from './HeaderThemeSelector'
2
+ export { default } from './HeaderThemeSelector'
@@ -186,6 +186,88 @@
186
186
  overflow: hidden;
187
187
  }
188
188
 
189
+ .editor-field-item {
190
+ position: relative;
191
+ padding: 5px;
192
+ background-color: #fff;
193
+ border: 1px solid #ccc;
194
+ margin-bottom: 10px;
195
+
196
+ &:last-child {
197
+ padding-bottom: 5px;
198
+ }
199
+
200
+ &__header {
201
+ width: 100%;
202
+ background-color: #f5f5f5;
203
+ border: 1px solid #ccc;
204
+ display: flex;
205
+ align-items: center;
206
+ padding: 5px;
207
+ padding-left: 5px !important;
208
+
209
+ .cove-icon {
210
+ flex-shrink: 0;
211
+ padding-right: 5px;
212
+ margin-right: 10px;
213
+ }
214
+
215
+ .btn {
216
+ flex-shrink: 0;
217
+ }
218
+ }
219
+
220
+ &__name {
221
+ margin-left: 0.5rem;
222
+ user-select: none;
223
+ flex: 1;
224
+ }
225
+
226
+ &__content {
227
+ padding: 10px;
228
+ background-color: #fff;
229
+ }
230
+
231
+ &__remove-wrapper {
232
+ display: flex;
233
+ justify-content: flex-end;
234
+ margin-bottom: 10px;
235
+
236
+ .btn {
237
+ border: 1px solid red;
238
+ border-radius: 10px;
239
+ }
240
+ }
241
+ }
242
+
243
+ .draggable-field-list {
244
+ list-style: none;
245
+ padding: 0;
246
+ margin: 0;
247
+
248
+ .currently-dragging {
249
+ opacity: 0.8;
250
+ }
251
+
252
+ .editor-field-item {
253
+ cursor: grab;
254
+
255
+ &:active {
256
+ cursor: grabbing;
257
+ }
258
+
259
+ &__header .cove-icon {
260
+ cursor: grab;
261
+ }
262
+ }
263
+ }
264
+
265
+ .filters-list {
266
+ list-style: none;
267
+ padding: 0;
268
+ margin: 0;
269
+ }
270
+
189
271
  .accordion__heading {
190
272
  background: var(--lightestGray);
191
273
  }
@@ -11,7 +11,14 @@ import { MapConfig } from '@cdc/map/src/types/MapConfig'
11
11
 
12
12
  type VisualizationWrapper = {
13
13
  children: React.ReactNode
14
- config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters | MapConfig | DataTableConfig
14
+ config:
15
+ | ChartConfig
16
+ | DataBiteConfig
17
+ | WaffleChartConfig
18
+ | MarkupIncludeConfig
19
+ | DashboardFilters
20
+ | MapConfig
21
+ | DataTableConfig
15
22
  currentViewport?: string
16
23
  imageId?: string
17
24
  isEditor: boolean
@@ -89,6 +96,14 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
89
96
  classes.push('is-editor')
90
97
  }
91
98
 
99
+ // Add TP5 style classes
100
+ if (config.visualizationType === 'TP5 Waffle') {
101
+ classes.push('waffle__style--tp5')
102
+ if (config.visual?.whiteBackground) {
103
+ classes.push('white-background-style')
104
+ }
105
+ }
106
+
92
107
  classes.push('cove-component', 'waffle-chart')
93
108
  }
94
109
  return classes
@@ -34,6 +34,13 @@
34
34
  left: 0;
35
35
  width: 100% !important;
36
36
  grid-area: content;
37
+ padding: 1rem;
38
+
39
+ // Prevent double padding on nested .cove-component__content divs
40
+ // (e.g., in markup-include, waffle-chart, filtered-text)
41
+ .cove-component__content {
42
+ padding: 0;
43
+ }
37
44
  }
38
45
  }
39
46
  }
@@ -1,5 +1,6 @@
1
+ @import '../../../styles/v2/utils/variables';
2
+
1
3
  $editorAnimationTimer: 400ms;
2
- $editorWidth: 350px;
3
4
  $mediumGray: #e6e6e6;
4
5
 
5
6
  @import 'editor-grid-view.scss';
@@ -4,7 +4,7 @@ import { type MapConfig } from '@cdc/map/src/types/MapConfig'
4
4
  import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
5
5
  import { getTextWidth } from '../../helpers/getTextWidth'
6
6
  import { DimensionsType } from '../../types/Dimensions'
7
- import useLegendSeparators from '@cdc/map/src/hooks/useLegendSeparators'
7
+ import useLegendSeparators from '../../hooks/useLegendSeparators'
8
8
 
9
9
  const MARGIN = 1
10
10
  const BORDER_SIZE = 1
@@ -12,7 +12,7 @@ type LoaderProps = {
12
12
 
13
13
  const Spinner = ({ spinnerType }: { spinnerType: SpinnerType }) => (
14
14
  <div className={`spinner-border ${spinnerType}`} role='status'>
15
- <span className='sr-only'>Loading...</span>
15
+ <span className='sr-only' style={{ display: 'none' }}>Loading...</span>
16
16
  </div>
17
17
  )
18
18
 
@@ -2,6 +2,7 @@ import React from 'react'
2
2
  // import html2pdf from 'html2pdf.js'
3
3
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
4
4
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
5
+ import { prepareScreenshotContainer } from '@cdc/core/helpers/prepareScreenshot'
5
6
 
6
7
  const buttonText = {
7
8
  pdf: 'Download PDF',
@@ -35,7 +36,7 @@ const saveImageAs = (uri, filename) => {
35
36
  }
36
37
  }
37
38
 
38
- const generateMedia = (state, type, elementToCapture, interactionLabel) => {
39
+ const generateMedia = (state, type, elementToCapture, interactionLabel, includeContextInDownload = false) => {
39
40
  // Identify Selector
40
41
  const baseSvg = document.querySelector(`[data-download-id=${elementToCapture}]`)
41
42
 
@@ -47,53 +48,39 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
47
48
  return undefined
48
49
  }
49
50
 
51
+ // Generate timestamp once for consistency
52
+ const date = new Date()
53
+ const day = date.getDate()
54
+ const month = date.getMonth() + 1
55
+ const year = date.getFullYear()
56
+ const timestamp = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
57
+
50
58
  // Handles different state title locations between components
51
59
  // Apparently some packages use state.title where others use state.general.title
52
60
  const handleFileName = state => {
53
61
  // dashboard titles
54
- if (state?.dashboard?.title)
55
- return (
56
- state.dashboard.title.replace(/\s+/g, '-').toLowerCase() +
57
- '-' +
58
- date.getDate() +
59
- date.getMonth() +
60
- date.getFullYear()
61
- )
62
+ if (state?.dashboard?.title) return `${state.dashboard.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
62
63
 
63
64
  // map titles
64
- if (state?.general?.title)
65
- return (
66
- state.general.title.replace(/\s+/g, '-').toLowerCase() +
67
- '-' +
68
- date.getDate() +
69
- date.getMonth() +
70
- date.getFullYear()
71
- )
65
+ if (state?.general?.title) return `${state.general.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
72
66
 
73
67
  // chart titles
74
- if (state?.title)
75
- return (
76
- state.title.replace(/\s+/g, '-').toLowerCase() + '-' + date.getDate() + date.getMonth() + date.getFullYear()
77
- )
68
+ if (state?.title) return `${state.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
78
69
 
79
70
  return 'no-title'
80
71
  }
81
72
 
82
- // Construct filename with timestamp
83
- const date = new Date()
84
73
  const filename = handleFileName(state)
85
74
 
86
75
  switch (type) {
87
76
  case 'image':
88
- const container = document.createElement('div')
89
- // On screenshots without a title (like some charts), add padding around the chart svg
90
- if (!state.showTitle) {
91
- container.style.padding = '35px'
92
- }
93
- container.appendChild(baseSvg.cloneNode(true)) // Clone baseSvg to avoid modifying the original
77
+ // Prepare screenshot container with all cloning, styling, and transformations
78
+ const container = prepareScreenshotContainer(baseSvg, includeContextInDownload, elementToCapture)
94
79
 
95
80
  const downloadImage = async () => {
96
- document.body.appendChild(container) // Append container to the DOM
81
+ // Append to main element if exists, otherwise body
82
+ const targetElement = document.querySelector('main') || document.body
83
+ targetElement.appendChild(container)
97
84
 
98
85
  // Fix select elements to show their current selected values before screenshot
99
86
  const selectElements = container.querySelectorAll('select')
@@ -116,10 +103,13 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
116
103
  .default(container, {
117
104
  ignoreElements: el =>
118
105
  el.className?.indexOf &&
119
- el.className.search(/download-buttons|download-links|data-table-container/) !== -1
106
+ el.className.search(/download-buttons|download-links|data-table-container/) !== -1,
107
+ useCORS: true,
108
+ scale: 2, // Better quality
109
+ allowTaint: true
120
110
  })
121
111
  .then(canvas => {
122
- document.body.removeChild(container) // Clean up container
112
+ targetElement.removeChild(container) // Clean up container from wherever we appended it
123
113
  saveImageAs(canvas.toDataURL(), filename + '.png')
124
114
  publishAnalyticsEvent({
125
115
  vizType: state.type,
@@ -160,13 +150,23 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
160
150
  }
161
151
  }
162
152
 
163
- const Button = ({ state, text, type, title, elementToCapture, interactionLabel = '' }) => {
153
+ // Button component for Dashboard downloads (renders as actual button)
154
+ const Button = ({
155
+ state,
156
+ text,
157
+ type,
158
+ title,
159
+ elementToCapture,
160
+ interactionLabel = '',
161
+ includeContextInDownload = false
162
+ }) => {
164
163
  const buttonClasses = ['btn', 'btn-primary']
164
+
165
165
  return (
166
166
  <button
167
167
  className={buttonClasses.join(' ')}
168
168
  title={title}
169
- onClick={() => generateMedia(state, type, elementToCapture, interactionLabel)}
169
+ onClick={() => generateMedia(state, type, elementToCapture, interactionLabel, includeContextInDownload)}
170
170
  style={{ lineHeight: '1.4em' }}
171
171
  >
172
172
  {buttonText[type]}
@@ -174,6 +174,34 @@ const Button = ({ state, text, type, title, elementToCapture, interactionLabel =
174
174
  )
175
175
  }
176
176
 
177
+ // DownloadLink component for Chart/Map downloads (renders as text link)
178
+ const DownloadLink = ({
179
+ state,
180
+ type,
181
+ title,
182
+ elementToCapture,
183
+ interactionLabel = '',
184
+ includeContextInDownload = false
185
+ }) => {
186
+ const vizType = state?.type === 'map' ? 'Map' : 'Chart'
187
+ const format = type === 'pdf' ? 'PDF' : 'PNG'
188
+ const linkText = `Download ${vizType} (${format})`
189
+
190
+ return (
191
+ <a
192
+ role='button'
193
+ onClick={() => generateMedia(state, type, elementToCapture, interactionLabel, includeContextInDownload)}
194
+ aria-label={title}
195
+ title={title}
196
+ className={`no-border`}
197
+ style={{ cursor: 'pointer' }}
198
+ data-html2canvas-ignore
199
+ >
200
+ {linkText}
201
+ </a>
202
+ )
203
+ }
204
+
177
205
  // Link to CSV/JSON data
178
206
  const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
179
207
  let dataConfig = dashboardDataConfig || config
@@ -235,6 +263,7 @@ const MediaControls = () => null
235
263
  MediaControls.Section = Section
236
264
  MediaControls.Link = Link
237
265
  MediaControls.Button = Button
266
+ MediaControls.DownloadLink = DownloadLink
238
267
  MediaControls.generateMedia = generateMedia
239
268
 
240
269
  export default MediaControls
@@ -43,11 +43,14 @@ const PaletteConversionModal: React.FC<PaletteConversionModalProps> = ({
43
43
  <div
44
44
  className='modal-header'
45
45
  style={{
46
- padding: '20px 20px 0 20px',
47
- borderBottom: '1px solid #e0e0e0'
46
+ padding: '15px',
47
+ borderBottom: '1px solid #e0e0e0',
48
+ backgroundColor: '#005eaa',
49
+ display: 'flex',
50
+ justifyContent: 'center',
48
51
  }}
49
52
  >
50
- <h3 style={{ margin: '0 0 20px 0' }}>Color Palette Conversion</h3>
53
+ <h3 style={{ color: 'white', textAlign: 'center' }}>Color Palette Conversion</h3>
51
54
  </div>
52
55
 
53
56
  <div className='modal-body' style={{ padding: '20px' }}>
@@ -84,4 +87,4 @@ const PaletteConversionModal: React.FC<PaletteConversionModalProps> = ({
84
87
  )
85
88
  }
86
89
 
87
- export default PaletteConversionModal
90
+ export default PaletteConversionModal
@@ -2,9 +2,28 @@
2
2
  /* Shared styles for palette color swatches across all visualization types */
3
3
 
4
4
  .color-palette {
5
- /* Button-based palette selector (used by charts) */
5
+ display: flex;
6
6
  }
7
7
 
8
+ /* List item-based palette selector (used by maps) */
9
+ .color-palette li {
10
+ width: 1.5em;
11
+ height: 1.5em;
12
+ display: inline-block;
13
+ margin-right: 0.5em;
14
+ cursor: pointer;
15
+ border: rgba(0, 0, 0, 0.3) 3px solid;
16
+ }
17
+
18
+ .color-palette li.active {
19
+ border: rgba(0, 0, 0, 0.8) 3px solid;
20
+ }
21
+
22
+ .color-palette li.selected {
23
+ border: black 2px solid;
24
+ }
25
+
26
+ /* Button-based palette selector (used by charts) */
8
27
  .color-palette button:not(.selected) {
9
28
  border: var(--cool-gray-30) 2px solid !important;
10
29
  }
@@ -13,13 +32,37 @@
13
32
  border: black 2px solid !important;
14
33
  }
15
34
 
16
- /* List item-based palette selector (used by maps) */
17
- .color-palette li {
18
- border: var(--cool-gray-30) 2px solid;
35
+ .color-palette a {
36
+ display: inline-block;
37
+ border-bottom: 1px solid rgba(0, 0, 0, 0.8);
19
38
  }
20
39
 
21
- .color-palette li.selected {
22
- border: black 2px solid;
40
+ /* Series list variant */
41
+ .color-palette.series-list {
42
+ flex-direction: column;
43
+ padding: 0;
44
+ border: none;
45
+ }
46
+
47
+ .color-palette.series-list li {
48
+ padding: 0.3em 0.5em;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ width: auto;
53
+ height: auto;
54
+ border: 0;
55
+ }
56
+
57
+ .color-palette.series-list li:not(:last-child) {
58
+ border-bottom: rgba(0, 0, 0, 0.2) 1px solid;
59
+ }
60
+
61
+ /* Header variant */
62
+ .header .color-palette li {
63
+ width: 1.5em;
64
+ height: 1.5em;
65
+ display: inline-block;
23
66
  }
24
67
 
25
68
  /* Developer rollback component styles */
@@ -1,7 +1,28 @@
1
1
  const Cell = ({ children, style, isBold = false, ariaLabel }) => {
2
+ // Use whiteSpace from style prop, defaulting to 'pre-line' for backwards compatibility
3
+ const whiteSpace = style?.whiteSpace || 'pre-line'
4
+
5
+ const contentWrapperStyle = {
6
+ whiteSpace: whiteSpace as any,
7
+ lineHeight: '1.4',
8
+ display: 'block' as const,
9
+ margin: 0,
10
+ padding: 0,
11
+ wordBreak: 'break-word' as const
12
+ }
13
+
14
+ // Only include aria-label if it has a value
15
+ const ariaProps = ariaLabel ? { 'aria-label': ariaLabel } : {}
16
+
17
+ // Keep whiteSpace on td style so it can be detected by tests and for proper rendering
18
+ const tdStyle = { ...style }
19
+ delete tdStyle.textOverflow
20
+
2
21
  return (
3
- <td aria-label={ariaLabel} tabIndex={0} role='gridcell' style={style}>
4
- {isBold ? <strong>{children}</strong> : children}
22
+ <td {...ariaProps} role='gridcell' style={tdStyle}>
23
+ <div style={contentWrapperStyle}>
24
+ {isBold ? <strong>{children}</strong> : children}
25
+ </div>
5
26
  </td>
6
27
  )
7
28
  }
@@ -16,8 +16,7 @@ type RowProps = {
16
16
  }
17
17
 
18
18
  const Row: FC<RowProps> = props => {
19
- const { childRow, rowKey, wrapColumns, cellMinWidth = 0, isTotal, preliminaryData, rightAlignedCols } = props
20
- const whiteSpace = wrapColumns ? 'unset' : 'nowrap'
19
+ const { childRow, rowKey, cellMinWidth = 0, isTotal, preliminaryData, rightAlignedCols, wrapColumns } = props
21
20
  const minWidth = cellMinWidth + 'px'
22
21
  const isHtmlString = (str: any): str is string => typeof str === 'string' && /<\/?[a-z][\s\S]*>/i.test(str)
23
22
  const isReactNode = (val: any): boolean => React.isValidElement(val) || typeof val === 'object'
@@ -32,6 +31,9 @@ const Row: FC<RowProps> = props => {
32
31
  {}
33
32
 
34
33
  const textAlign = rightAlignedCols && rightAlignedCols[i] ? 'right' : ''
34
+ // Set whiteSpace based on wrapColumns prop (default to wrapping for backwards compatibility)
35
+ const whiteSpace = wrapColumns === false ? 'nowrap' : 'normal'
36
+
35
37
  // handle Parsing
36
38
  let content: ReactNode
37
39
  if (isHtmlString(child)) {
@@ -46,7 +48,7 @@ const Row: FC<RowProps> = props => {
46
48
  <Cell
47
49
  ariaLabel={style?.color ? 'suppressed data' : ''}
48
50
  key={rowKey + '__' + i}
49
- style={{ whiteSpace, minWidth, textAlign, textOverflow: 'ellipsis', ...style }}
51
+ style={{ minWidth, textAlign, whiteSpace, ...style }}
50
52
  isBold={isTotal}
51
53
  >
52
54
  {content}
@@ -8,7 +8,14 @@ import { Visualization } from '../../types/Visualization'
8
8
 
9
9
  const meta: Meta<typeof Filters> = {
10
10
  title: 'Components/Molecules/Visualization Filters',
11
- component: Filters
11
+ component: Filters,
12
+ decorators: [
13
+ Story => (
14
+ <div className='cdc-open-viz-module'>
15
+ <Story />
16
+ </div>
17
+ )
18
+ ]
12
19
  }
13
20
 
14
21
  type Story = StoryObj<typeof Filters>
@@ -54,4 +61,16 @@ export const Tab: Story = generateConfig('tab')
54
61
 
55
62
  export const TabBar: Story = generateConfig('tab bar')
56
63
 
64
+ export const WithApplyButton: Story = {
65
+ args: {
66
+ config: {
67
+ filters: generateFilters('dropdown'),
68
+ data: animalData,
69
+ filterBehavior: 'Apply Button',
70
+ type: 'chart'
71
+ } as any,
72
+ setFilters: () => {}
73
+ }
74
+ }
75
+
57
76
  export default meta