@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,169 @@
1
+ import { ReactNode } from 'react'
2
+ import { CheckBox } from '../Inputs'
3
+ import { HeaderThemeSelector } from '../../HeaderThemeSelector'
4
+ import { UpdateFieldFunc } from '../../../types/UpdateFieldFunc'
5
+
6
+ export interface VisualSectionConfig {
7
+ visual?: {
8
+ border?: boolean
9
+ borderColorTheme?: boolean
10
+ accent?: boolean
11
+ background?: boolean
12
+ hideBackgroundColor?: boolean
13
+ }
14
+ theme?: string
15
+ }
16
+
17
+ export interface VisualSectionProps<TConfig extends VisualSectionConfig = VisualSectionConfig> {
18
+ /** The visualization config object */
19
+ config: TConfig
20
+
21
+ /** Update function for individual fields */
22
+ updateField: UpdateFieldFunc<TConfig>
23
+
24
+ /** Update function for the entire config (used by HeaderThemeSelector) */
25
+ updateConfig: (config: TConfig) => void
26
+
27
+ /** Optional content to render before the standard checkboxes */
28
+ beforeCheckboxes?: ReactNode
29
+
30
+ /** Optional content to render after the standard checkboxes */
31
+ afterCheckboxes?: ReactNode
32
+
33
+ /** Position of HeaderThemeSelector. Defaults to 'before' */
34
+ themeSelectorPosition?: 'before' | 'after' | 'none'
35
+
36
+ /** Whether to show the border checkbox. Defaults to true */
37
+ showBorder?: boolean
38
+
39
+ /** Whether to show the borderColorTheme checkbox. Defaults to true */
40
+ showBorderColorTheme?: boolean
41
+
42
+ /** Whether to show the accent checkbox. Defaults to true */
43
+ showAccent?: boolean
44
+
45
+ /** Whether to show the background checkbox. Defaults to true */
46
+ showBackground?: boolean
47
+
48
+ /** Whether to show the hideBackgroundColor checkbox. Defaults to true */
49
+ showHideBackgroundColor?: boolean
50
+ }
51
+
52
+ /**
53
+ * Reusable Visual section component for EditorPanels
54
+ *
55
+ * Provides common visual configuration options including:
56
+ * - Theme selection
57
+ * - Border controls
58
+ * - Background and accent styling
59
+ *
60
+ * Note: Must be wrapped in an Accordion.Section when used
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * <Accordion.Section title='Visual'>
65
+ * <VisualSection
66
+ * config={config}
67
+ * updateField={updateField}
68
+ * updateConfig={updateConfig}
69
+ * beforeCheckboxes={
70
+ * <Select
71
+ * value={config.fontSize}
72
+ * fieldName='fontSize'
73
+ * label='Font Size'
74
+ * updateField={updateField}
75
+ * options={['small', 'medium', 'large']}
76
+ * />
77
+ * }
78
+ * />
79
+ * </Accordion.Section>
80
+ * ```
81
+ */
82
+ export const VisualSection = <TConfig extends VisualSectionConfig = VisualSectionConfig>({
83
+ config,
84
+ updateField,
85
+ updateConfig,
86
+ beforeCheckboxes,
87
+ afterCheckboxes,
88
+ themeSelectorPosition = 'before',
89
+ showBorder = true,
90
+ showBorderColorTheme = true,
91
+ showAccent = true,
92
+ showBackground = true,
93
+ showHideBackgroundColor = true
94
+ }: VisualSectionProps<TConfig>) => {
95
+ const visual = config.visual || {}
96
+ const theme = config.theme
97
+
98
+ const renderThemeSelector = () => {
99
+ if (themeSelectorPosition === 'none') return null
100
+
101
+ return (
102
+ <HeaderThemeSelector
103
+ selectedTheme={theme}
104
+ onThemeSelect={theme => updateConfig({ ...config, theme } as TConfig)}
105
+ label='Theme'
106
+ />
107
+ )
108
+ }
109
+
110
+ const renderCheckboxes = () => (
111
+ <div className='checkbox-group'>
112
+ {showBorder && (
113
+ <CheckBox
114
+ value={visual.border}
115
+ section='visual'
116
+ fieldName='border'
117
+ label='Display Border'
118
+ updateField={updateField}
119
+ />
120
+ )}
121
+ {showBorderColorTheme && (
122
+ <CheckBox
123
+ value={visual.borderColorTheme}
124
+ section='visual'
125
+ fieldName='borderColorTheme'
126
+ label='Use Border Color Theme'
127
+ updateField={updateField}
128
+ />
129
+ )}
130
+ {showAccent && (
131
+ <CheckBox
132
+ value={visual.accent}
133
+ section='visual'
134
+ fieldName='accent'
135
+ label='Use Accent Style'
136
+ updateField={updateField}
137
+ />
138
+ )}
139
+ {showBackground && (
140
+ <CheckBox
141
+ value={visual.background}
142
+ section='visual'
143
+ fieldName='background'
144
+ label='Use Theme Background Color'
145
+ updateField={updateField}
146
+ />
147
+ )}
148
+ {showHideBackgroundColor && (
149
+ <CheckBox
150
+ value={visual.hideBackgroundColor}
151
+ section='visual'
152
+ fieldName='hideBackgroundColor'
153
+ label='Hide Background Color'
154
+ updateField={updateField}
155
+ />
156
+ )}
157
+ </div>
158
+ )
159
+
160
+ return (
161
+ <>
162
+ {beforeCheckboxes}
163
+ {themeSelectorPosition === 'before' && renderThemeSelector()}
164
+ {renderCheckboxes()}
165
+ {themeSelectorPosition === 'after' && renderThemeSelector()}
166
+ {afterCheckboxes}
167
+ </>
168
+ )
169
+ }
@@ -5,6 +5,7 @@ import parse from 'html-react-parser'
5
5
  // CDC
6
6
  import Button from '../elements/Button'
7
7
  import MultiSelect from '../MultiSelect'
8
+ import ComboBox from '../ComboBox'
8
9
  import { Visualization } from '../../types/Visualization'
9
10
  import { MultiSelectFilter, VizFilter } from '../../types/VizFilter'
10
11
  import { addValuesToFilters } from '../../helpers/addValuesToFilters'
@@ -14,7 +15,7 @@ import { getNestedOptions } from './helpers/getNestedOptions'
14
15
  import { getWrappingStatuses } from './helpers/filterWrapping'
15
16
  import { handleSorting } from './helpers/handleSorting'
16
17
  import { getChangedFilters } from './helpers/getChangedFilters'
17
- import { getUniqueValues } from '@cdc/map/src/helpers'
18
+ import { getUniqueValues } from '../../helpers/getUniqueValues'
18
19
  import { getQueryParams, updateQueryString } from '../../helpers/queryStringUtils'
19
20
  import { applyQueuedActive } from './helpers/applyQueuedActive'
20
21
  import Tabs from './components/Tabs'
@@ -23,13 +24,14 @@ import { publishAnalyticsEvent } from '../../helpers/metrics/helpers'
23
24
  import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
24
25
 
25
26
  export const VIZ_FILTER_STYLE = {
27
+ combobox: 'combobox',
26
28
  dropdown: 'dropdown',
29
+ multiSelect: 'multi-select',
27
30
  nestedDropdown: 'nested-dropdown',
28
31
  pill: 'pill',
29
32
  tab: 'tab',
30
33
  tabSimple: 'tab-simple',
31
- tabBar: 'tab bar',
32
- multiSelect: 'multi-select'
34
+ tabBar: 'tab bar'
33
35
  } as const
34
36
 
35
37
  export type VizFilterStyle = (typeof VIZ_FILTER_STYLE)[keyof typeof VIZ_FILTER_STYLE]
@@ -95,7 +97,9 @@ const Filters: React.FC<FilterProps> = ({
95
97
  eventAction: 'change',
96
98
  eventLabel: interactionLabel,
97
99
  vizTitle: getVizTitle(visualizationConfig),
98
- specifics: `key: ${String(newFilters?.[index]?.columnName).toLowerCase()}, value: ${String(newFilters?.[index]?.active).toLowerCase()}`
100
+ specifics: `key: ${String(newFilters?.[index]?.columnName).toLowerCase()}, value: ${String(
101
+ newFilters?.[index]?.active
102
+ ).toLowerCase()}`
99
103
  })
100
104
  }
101
105
 
@@ -202,6 +206,9 @@ const Filters: React.FC<FilterProps> = ({
202
206
 
203
207
  if (visualizationConfig?.filters?.length === 0) return <></>
204
208
 
209
+ const hasVisibleFilters = filters?.some(filter => filter.showDropdown !== false)
210
+ if (!hasVisibleFilters) return <></>
211
+
205
212
  const getClasses = () => {
206
213
  const { visualizationType, legend } = visualizationConfig || {}
207
214
  const baseClass = 'filters-section'
@@ -216,6 +223,12 @@ const Filters: React.FC<FilterProps> = ({
216
223
  return (singleFilter.queuedActive || [singleFilter.active, singleFilter.subGrouping?.active]) as [string, string]
217
224
  }
218
225
 
226
+ // Don't render filter section if all filters are hidden
227
+ const allFiltersHidden = vizFiltersWithValues.every(filter => filter.showDropdown === false)
228
+ if (allFiltersHidden) {
229
+ return null
230
+ }
231
+
219
232
  return (
220
233
  <section className={getClasses().join(' ')}>
221
234
  {visualizationConfig.filterIntro && (
@@ -235,7 +248,9 @@ const Filters: React.FC<FilterProps> = ({
235
248
  'form-group',
236
249
  mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`
237
250
  ]
238
- const mobileExempt = ['nested-dropdown', 'multi-select', VIZ_FILTER_STYLE.tabSimple].includes(filterStyle)
251
+ const mobileExempt = ['nested-dropdown', 'multi-select', 'combobox', VIZ_FILTER_STYLE.tabSimple].includes(
252
+ filterStyle
253
+ )
239
254
  const { isDropdown } = wrappingFilters[columnName] || {}
240
255
  const showDefaultDropdown =
241
256
  ((filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt) || isDropdown
@@ -302,6 +317,17 @@ const Filters: React.FC<FilterProps> = ({
302
317
  handleSelectedItems={value => changeFilterActive(outerIndex, value)}
303
318
  />
304
319
  )}
320
+ {filterStyle === 'combobox' && (
321
+ <ComboBox
322
+ options={singleFilter.values.map(v => ({ value: v, label: v }))}
323
+ fieldName={outerIndex}
324
+ updateField={(_section, _subSection, fieldName, value) => {
325
+ changeFilterActive(fieldName, value)
326
+ }}
327
+ selected={(singleFilter.queuedActive || singleFilter.active) as string}
328
+ label={label}
329
+ />
330
+ )}
305
331
  </div>
306
332
  )
307
333
  })}
@@ -8,9 +8,10 @@ type GetOptionsMemoParams = {
8
8
  }
9
9
 
10
10
  export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOptionsMemoParams): NestedOptions => {
11
+ if (!values?.length && !orderedValues?.length) return []
11
12
  // keep custom ordered value order
12
13
  const filteredValues = orderedValues?.length
13
- ? orderedValues.filter(orderedValue => values.includes(orderedValue))
14
+ ? orderedValues.filter(orderedValue => values?.includes(orderedValue))
14
15
  : values
15
16
  const options: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
16
17
  if (!subGrouping) return [[value], []]
@@ -2,7 +2,7 @@ import _ from 'lodash'
2
2
 
3
3
  export const handleSorting = singleFilter => {
4
4
  const singleFilterValues = _.cloneDeep(singleFilter.values)
5
- if (singleFilter.order === 'cust' && singleFilter.filterStyle !== 'nested-dropdown') {
5
+ if (singleFilter.order === 'cust') {
6
6
  singleFilter.values = singleFilter.orderedValues?.length ? singleFilter.orderedValues : singleFilterValues
7
7
  return singleFilter
8
8
  }
@@ -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
  }
@@ -544,9 +626,9 @@
544
626
 
545
627
  .sort-list {
546
628
  list-style: none;
629
+ padding: 0;
547
630
 
548
631
  > li {
549
- margin-right: 0.3em;
550
632
  margin-bottom: 0.3em;
551
633
  }
552
634
  }
@@ -559,8 +641,8 @@
559
641
  background: #f1f1f1;
560
642
  padding: 0.4em 0.6em;
561
643
  font-size: 0.8em;
562
- margin-bottom: 0.3em;
563
644
  cursor: move;
645
+ width: 100%;
564
646
  }
565
647
 
566
648
  .info {
@@ -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
@@ -64,6 +71,10 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
64
71
  if (config?.runtime?.editorErrorMessage.length !== 0) classes.push('type-map--has-error')
65
72
  }
66
73
 
74
+ if (config.type === 'table') {
75
+ classes.push('type-data-table')
76
+ }
77
+
67
78
  if (config.type === 'data-bite') {
68
79
  classes.push('cdc-open-viz-module', 'type-data-bite', currentViewport, config.theme, `font-${config.fontSize}`)
69
80
  if (isEditor) {
@@ -89,6 +100,21 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
89
100
  classes.push('is-editor')
90
101
  }
91
102
 
103
+ // Add TP5 style classes
104
+ if (config.visualizationType === 'TP5 Waffle') {
105
+ classes.push('waffle__style--tp5')
106
+ if (config.visual?.whiteBackground) {
107
+ classes.push('white-background-style')
108
+ }
109
+ }
110
+
111
+ if (config.visualizationType === 'TP5 Gauge') {
112
+ classes.push('gauge__style--tp5')
113
+ if (config.visual?.whiteBackground) {
114
+ classes.push('white-background-style')
115
+ }
116
+ }
117
+
92
118
  classes.push('cove-component', 'waffle-chart')
93
119
  }
94
120
  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
  }
@@ -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
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
- // import html2pdf from 'html2pdf.js'
3
2
  import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
4
3
  import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
4
+ import { prepareScreenshotContainer } from '@cdc/core/helpers/prepareScreenshot'
5
5
 
6
6
  const buttonText = {
7
7
  pdf: 'Download PDF',
@@ -35,7 +35,7 @@ const saveImageAs = (uri, filename) => {
35
35
  }
36
36
  }
37
37
 
38
- const generateMedia = (state, type, elementToCapture, interactionLabel) => {
38
+ const generateMedia = (state, type, elementToCapture, interactionLabel, includeContextInDownload = false) => {
39
39
  // Identify Selector
40
40
  const baseSvg = document.querySelector(`[data-download-id=${elementToCapture}]`)
41
41
 
@@ -58,22 +58,13 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
58
58
  // Apparently some packages use state.title where others use state.general.title
59
59
  const handleFileName = state => {
60
60
  // dashboard titles
61
- if (state?.dashboard?.title)
62
- return (
63
- `${state.dashboard.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
64
- )
61
+ if (state?.dashboard?.title) return `${state.dashboard.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
65
62
 
66
63
  // map titles
67
- if (state?.general?.title)
68
- return (
69
- `${state.general.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
70
- )
64
+ if (state?.general?.title) return `${state.general.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
71
65
 
72
66
  // chart titles
73
- if (state?.title)
74
- return (
75
- `${state.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
76
- )
67
+ if (state?.title) return `${state.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
77
68
 
78
69
  return 'no-title'
79
70
  }
@@ -82,18 +73,13 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
82
73
 
83
74
  switch (type) {
84
75
  case 'image':
85
- const container = document.createElement('div')
86
-
87
- // Simple configurable padding (main fix for spacing issues)
88
- const downloadPadding = state.downloadImagePadding !== undefined ? state.downloadImagePadding : (!state.showTitle ? 35 : 0)
89
- if (downloadPadding > 0) {
90
- container.style.padding = `${downloadPadding}px`
91
- }
92
-
93
- container.appendChild(baseSvg.cloneNode(true));
76
+ // Prepare screenshot container with all cloning, styling, and transformations
77
+ const container = prepareScreenshotContainer(baseSvg, includeContextInDownload, elementToCapture)
94
78
 
95
79
  const downloadImage = async () => {
96
- document.body.appendChild(container) // Append container to the DOM
80
+ // Append to main element if exists, otherwise body
81
+ const targetElement = document.querySelector('main') || document.body
82
+ targetElement.appendChild(container)
97
83
 
98
84
  // Fix select elements to show their current selected values before screenshot
99
85
  const selectElements = container.querySelectorAll('select')
@@ -119,10 +105,10 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
119
105
  el.className.search(/download-buttons|download-links|data-table-container/) !== -1,
120
106
  useCORS: true,
121
107
  scale: 2, // Better quality
122
- allowTaint: true,
108
+ allowTaint: true
123
109
  })
124
110
  .then(canvas => {
125
- document.body.removeChild(container) // Clean up container
111
+ targetElement.removeChild(container) // Clean up container from wherever we appended it
126
112
  saveImageAs(canvas.toDataURL(), filename + '.png')
127
113
  publishAnalyticsEvent({
128
114
  vizType: state.type,
@@ -163,13 +149,23 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
163
149
  }
164
150
  }
165
151
 
166
- const Button = ({ state, text, type, title, elementToCapture, interactionLabel = '' }) => {
152
+ // Button component for Dashboard downloads (renders as actual button)
153
+ const Button = ({
154
+ state,
155
+ text,
156
+ type,
157
+ title,
158
+ elementToCapture,
159
+ interactionLabel = '',
160
+ includeContextInDownload = false
161
+ }) => {
167
162
  const buttonClasses = ['btn', 'btn-primary']
163
+
168
164
  return (
169
165
  <button
170
166
  className={buttonClasses.join(' ')}
171
167
  title={title}
172
- onClick={() => generateMedia(state, type, elementToCapture, interactionLabel)}
168
+ onClick={() => generateMedia(state, type, elementToCapture, interactionLabel, includeContextInDownload)}
173
169
  style={{ lineHeight: '1.4em' }}
174
170
  >
175
171
  {buttonText[type]}
@@ -177,6 +173,34 @@ const Button = ({ state, text, type, title, elementToCapture, interactionLabel =
177
173
  )
178
174
  }
179
175
 
176
+ // DownloadLink component for Chart/Map downloads (renders as text link)
177
+ const DownloadLink = ({
178
+ state,
179
+ type,
180
+ title,
181
+ elementToCapture,
182
+ interactionLabel = '',
183
+ includeContextInDownload = false
184
+ }) => {
185
+ const vizType = state?.type === 'map' ? 'Map' : 'Chart'
186
+ const format = type === 'pdf' ? 'PDF' : 'PNG'
187
+ const linkText = `Download ${vizType} (${format})`
188
+
189
+ return (
190
+ <a
191
+ role='button'
192
+ onClick={() => generateMedia(state, type, elementToCapture, interactionLabel, includeContextInDownload)}
193
+ aria-label={title}
194
+ title={title}
195
+ className={`no-border`}
196
+ style={{ cursor: 'pointer' }}
197
+ data-html2canvas-ignore
198
+ >
199
+ {linkText}
200
+ </a>
201
+ )
202
+ }
203
+
180
204
  // Link to CSV/JSON data
181
205
  const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
182
206
  let dataConfig = dashboardDataConfig || config
@@ -238,6 +262,7 @@ const MediaControls = () => null
238
262
  MediaControls.Section = Section
239
263
  MediaControls.Link = Link
240
264
  MediaControls.Button = Button
265
+ MediaControls.DownloadLink = DownloadLink
241
266
  MediaControls.generateMedia = generateMedia
242
267
 
243
268
  export default MediaControls
@@ -0,0 +1,37 @@
1
+ import React, { useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import CustomColorsEditor from '../CustomColorsEditor/CustomColorsEditor'
4
+
5
+ const meta: Meta<typeof CustomColorsEditor> = {
6
+ title: 'Components/Atoms/CustomColorsEditor',
7
+ component: CustomColorsEditor
8
+ }
9
+
10
+ export default meta
11
+ type Story = StoryObj<typeof CustomColorsEditor>
12
+
13
+ const fiftyColors = [
14
+ '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
15
+ '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
16
+ '#469990', '#dcbeff', '#9a6324', '#fffac8', '#800000',
17
+ '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9',
18
+ '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
19
+ '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
20
+ '#469990', '#dcbeff', '#9a6324', '#fffac8', '#800000',
21
+ '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9',
22
+ '#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00',
23
+ '#ffff33', '#a65628', '#f781bf', '#999999', '#66c2a5'
24
+ ]
25
+
26
+ const Wrapper = ({ initialColors }: { initialColors: string[] }) => {
27
+ const [colors, setColors] = useState(initialColors)
28
+ return <CustomColorsEditor colors={colors} onChange={setColors} label='Custom Color Order' />
29
+ }
30
+
31
+ export const Default: Story = {
32
+ render: () => <Wrapper initialColors={['#3366cc', '#dc3912', '#ff9900']} />
33
+ }
34
+
35
+ export const FiftyColors: Story = {
36
+ render: () => <Wrapper initialColors={fiftyColors} />
37
+ }
@@ -45,6 +45,7 @@ export const CityState: Story = {
45
45
  tabbingId: '#asdf',
46
46
  columns: CityStateExample.columns,
47
47
  applyLegendToRow: () => ['#000'],
48
+ getPatternForRow: () => null,
48
49
  displayGeoName
49
50
  },
50
51
  decorators: [
@@ -36,6 +36,7 @@ import iconTable from '../../assets/icon-table.svg'
36
36
  import iconSankey from '../../assets/icon-sankey.svg'
37
37
  import iconRotateLeft from '../../assets/icon-rotate-left.svg'
38
38
  import iconCommand from '../../assets/icon-command.svg'
39
+ import iconMagnifyingGlass from '../../assets/icon-magnifying-glass.svg'
39
40
 
40
41
  import '../../styles/v2/components/icon.scss'
41
42
 
@@ -75,7 +76,8 @@ const iconHash = {
75
76
  table: iconTable,
76
77
  sankey: iconSankey,
77
78
  rotateLeft: iconRotateLeft,
78
- command: iconCommand
79
+ command: iconCommand,
80
+ magnifyingGlass: iconMagnifyingGlass
79
81
  }
80
82
 
81
83
  export type IconType = keyof typeof iconHash