@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.
- 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/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
- 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/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/data-table.css +217 -210
- package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +54 -28
- package/components/EditorPanel/EditorPanel.styles.css +439 -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/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +44 -8
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +57 -10
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- 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/Layout/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +63 -34
- package/components/PaletteConversionModal.tsx +7 -4
- package/components/PaletteSelector/PaletteSelector.css +49 -6
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/Filters.stories.tsx +20 -1
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +768 -3
- package/components/_stories/Inputs.stories.tsx +2 -2
- package/components/_stories/styles.scss +0 -1
- package/components/ui/Accordion.jsx +1 -1
- 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/components/ui/accordion.styles.css +57 -0
- package/data/chartColorPalettes.ts +1 -1
- package/dist/cove-main.css +75 -6
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +8 -1
- package/helpers/addValuesToFilters.ts +11 -1
- package/helpers/constants.ts +37 -0
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +20 -11
- package/helpers/embedCodeGenerator.ts +109 -0
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/markupProcessor.ts +27 -12
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +268 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +17 -4
- 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.25.11.ts +13 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/viewports.ts +2 -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 +6 -4
- package/styles/_button-section.scss +0 -3
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +25 -5
- package/styles/base.scss +0 -50
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/editor.scss +14 -6
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/MarkupInclude.ts +5 -3
- package/types/MarkupVariable.ts +1 -1
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +1 -0
- package/types/VizFilter.ts +2 -0
- package/LICENSE +0 -201
- package/styles/_mixins.scss +0 -13
- package/styles/_typography.scss +0 -0
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- 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
|
|
@@ -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:
|
|
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
|
}
|
|
@@ -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 '
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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={{
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
border:
|
|
35
|
+
.color-palette a {
|
|
36
|
+
display: inline-block;
|
|
37
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.8);
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
4
|
-
|
|
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,
|
|
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={{
|
|
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
|