@cdc/core 4.25.8 → 4.25.11
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/StoryRenderingTests.stories.tsx +164 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +32 -9
- 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 +56 -38
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/ChartHeader.tsx +44 -14
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/components/ExpandCollapse.tsx +10 -1
- package/components/DataTable/components/MapHeader.tsx +24 -13
- package/components/DataTable/data-table.css +12 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
- package/components/DataTable/helpers/mapCellMatrix.tsx +33 -4
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/DownloadButton.tsx +40 -14
- package/components/EditorPanel/DataTableEditor.tsx +3 -3
- package/components/EditorPanel/EditorPanel.styles.css +423 -0
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +12 -2
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
- package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +450 -0
- package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
- package/components/ErrorBoundary.jsx +3 -1
- package/components/Filters/Filters.tsx +52 -24
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/components/Tabs.tsx +1 -0
- 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/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +72 -21
- package/components/PaletteConversionModal.tsx +90 -0
- package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
- package/components/PaletteSelector/PaletteSelector.css +94 -0
- package/components/PaletteSelector/PaletteSelector.tsx +112 -0
- package/components/PaletteSelector/index.ts +2 -0
- package/components/RichTooltip/RichTooltip.tsx +1 -0
- package/components/Table/Table.tsx +3 -1
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
- package/components/_stories/DataTable.stories.tsx +1 -1
- package/components/_stories/Filters.stories.tsx +21 -2
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +769 -4
- package/components/_stories/Inputs.stories.tsx +3 -3
- package/components/_stories/MultiSelect.stories.tsx +3 -3
- package/components/_stories/NestedDropdown.stories.tsx +1 -1
- package/components/_stories/Table.stories.tsx +1 -1
- package/components/_stories/styles.scss +0 -1
- package/components/elements/_stories/Button.stories.tsx +1 -1
- package/components/elements/_stories/Card.stories.tsx +1 -1
- package/components/inputs/InputToggle.tsx +2 -0
- package/components/managers/DataDesigner.tsx +10 -9
- package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Tooltip.tsx +2 -1
- package/components/ui/_stories/Accordion.stories.tsx +1 -1
- package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
- package/components/ui/_stories/Colors.stories.tsx +330 -0
- package/components/ui/_stories/IconGallery.stories.tsx +316 -0
- package/components/ui/_stories/Title.stories.tsx +1 -1
- package/components/ui/accordion.styles.css +57 -0
- package/contexts/EditorContext.ts +18 -0
- package/contexts/editor.actions.ts +28 -0
- package/contexts/editor.reducer.ts +94 -0
- package/data/chartColorPalettes.ts +118 -0
- package/data/colorPalettes.ts +9 -0
- package/data/mapColorPalettes.ts +45 -0
- package/data/sharedPalettes.ts +50 -0
- package/dist/cove-main.css +63 -14
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +80 -0
- package/helpers/addValuesToFilters.ts +7 -3
- package/helpers/cloneConfig.ts +31 -0
- package/helpers/configDataHelpers.ts +128 -0
- package/helpers/configHelpers.ts +27 -0
- package/helpers/constants.ts +42 -2
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +15 -3
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/filterColorPalettes.ts +152 -0
- package/helpers/generateColorsArray.ts +13 -0
- package/helpers/getColorPaletteVersion.ts +33 -0
- package/helpers/getPaletteAccessor.ts +18 -0
- package/helpers/markupProcessor.ts +220 -0
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +42 -19
- package/helpers/metrics/types.ts +48 -9
- package/helpers/metrics/utils.ts +34 -0
- package/helpers/palettes/colorDistributions.ts +56 -0
- package/helpers/palettes/migratePaletteName.ts +150 -0
- package/helpers/palettes/standardizePaletteNames.ts +77 -0
- package/helpers/palettes/utils.ts +267 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/queryStringUtils.ts +13 -0
- package/helpers/testing.ts +358 -0
- package/helpers/tests/addValuesToFilters.test.ts +1 -2
- package/helpers/tests/generateColorsArray.test.ts +24 -0
- package/helpers/tests/markupProcessor.test.ts +538 -0
- package/helpers/tests/testStandaloneBuild.ts +44 -0
- package/helpers/useMarkupVariables.ts +31 -0
- package/helpers/vegaConfig.ts +0 -1
- package/helpers/ver/4.24.10.ts +2 -1
- package/helpers/ver/4.24.11.ts +2 -1
- package/helpers/ver/4.24.3.ts +2 -1
- package/helpers/ver/4.24.4.ts +2 -1
- package/helpers/ver/4.24.5.ts +2 -1
- package/helpers/ver/4.24.7.ts +2 -1
- package/helpers/ver/4.24.9.ts +2 -1
- package/helpers/ver/4.25.1.ts +2 -1
- package/helpers/ver/4.25.10.ts +36 -0
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/ver/4.25.3.ts +2 -1
- package/helpers/ver/4.25.4.ts +2 -1
- package/helpers/ver/4.25.6.ts +2 -1
- package/helpers/ver/4.25.7.ts +2 -1
- package/helpers/ver/4.25.8.ts +2 -1
- package/helpers/ver/4.25.9.ts +293 -0
- package/helpers/ver/tests/4.25.10.test.ts +204 -0
- package/helpers/ver/tests/4.25.8.test.ts +1 -1
- package/helpers/ver/tests/4.25.9.test.ts +51 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useColorPalette.ts +79 -0
- package/package.json +13 -4
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +32 -10
- package/styles/base.scss +8 -55
- 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/button.scss +4 -3
- package/styles/v2/components/editor.scss +16 -7
- package/styles/v2/layout/_data-table.scss +3 -2
- package/styles/v2/themes/_color-definitions.scss +18 -17
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/testing-setup.js +32 -0
- package/types/MarkupInclude.ts +8 -2
- package/types/MarkupVariable.ts +19 -0
- package/types/VizFilter.ts +2 -0
- package/vitest.config.ts +16 -0
- package/components/ui/_stories/Colors.stories.mdx +0 -220
- package/components/ui/_stories/IconGallery.stories.mdx +0 -14
- package/data/colorPalettes.js +0 -171
- package/helpers/formatConfigBeforeSave.ts +0 -135
- package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
- package/styles/_mixins.scss +0 -13
- 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
- /package/{styles/_typography.scss → testBuild.js} +0 -0
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
import { Footnote } from '../../types/Footnotes'
|
|
2
|
-
import '
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import { Footnote } from '../../types/Footnotes'
|
|
2
|
+
import parse from 'html-react-parser'
|
|
3
|
+
import './footnotes.css'
|
|
4
|
+
|
|
5
|
+
type FootnotesProps = {
|
|
6
|
+
footnotes: Footnote[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
|
|
10
|
+
// Convert newlines to <br> tags and parse HTML
|
|
11
|
+
const processFootnoteText = (text: string) => {
|
|
12
|
+
if (!text) return ''
|
|
13
|
+
// Convert newline characters to <br> tags
|
|
14
|
+
const textWithBreaks = text.replace(/\n/g, '<br>')
|
|
15
|
+
// Parse HTML (html-react-parser handles sanitization)
|
|
16
|
+
return parse(textWithBreaks)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<footer className='col-12 m-3 mt-1 mb-0'>
|
|
21
|
+
<ul className='cove-footnotes'>
|
|
22
|
+
{footnotes.map((note, i) => {
|
|
23
|
+
return (
|
|
24
|
+
<li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
|
|
25
|
+
{note.symbol && <span className='me-1'>{note.symbol}</span>}
|
|
26
|
+
{processFootnoteText(note.text)}
|
|
27
|
+
</li>
|
|
28
|
+
)
|
|
29
|
+
})}
|
|
30
|
+
</ul>
|
|
31
|
+
</footer>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default Footnotes
|
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
import Footnotes from './Footnotes'
|
|
2
|
-
import FootnotesConfig from '../../types/Footnotes'
|
|
2
|
+
import FootnotesConfig, { Footnote } from '../../types/Footnotes'
|
|
3
3
|
import _ from 'lodash'
|
|
4
4
|
import { useMemo } from 'react'
|
|
5
5
|
import { filterVizData } from '../../helpers/filterVizData'
|
|
6
6
|
import { VizFilter } from '../../types/VizFilter'
|
|
7
|
+
import { MarkupVariable } from '../../types/MarkupVariable'
|
|
8
|
+
import { processMarkupVariables } from '../../helpers/markupProcessor'
|
|
7
9
|
|
|
8
10
|
type StandAloneProps = {
|
|
9
11
|
config: FootnotesConfig
|
|
10
12
|
filters?: VizFilter[]
|
|
13
|
+
markupVariables?: MarkupVariable[]
|
|
14
|
+
enableMarkupVariables?: boolean
|
|
15
|
+
data?: Object[]
|
|
11
16
|
}
|
|
12
17
|
|
|
13
|
-
const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) => {
|
|
18
|
+
const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters, markupVariables = [], enableMarkupVariables = false, data = [] }) => {
|
|
14
19
|
if (!config) return null
|
|
20
|
+
|
|
21
|
+
// Helper function to process markup variables in footnote text
|
|
22
|
+
const processFootnoteText = (text: string): string => {
|
|
23
|
+
if (!enableMarkupVariables || !markupVariables || markupVariables.length === 0) {
|
|
24
|
+
return text
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Use data from props if available, otherwise use config.data
|
|
28
|
+
const footnoteData = data.length > 0 ? data : config.data || []
|
|
29
|
+
|
|
30
|
+
const { processedContent } = processMarkupVariables(
|
|
31
|
+
text,
|
|
32
|
+
footnoteData,
|
|
33
|
+
markupVariables,
|
|
34
|
+
{
|
|
35
|
+
filters,
|
|
36
|
+
isEditor: false
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return processedContent
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
// get the api footnotes from the config
|
|
16
44
|
const apiFootnotes = useMemo(() => {
|
|
17
45
|
// If filters exist and should filter footnotes, apply them, otherwise use data as-is
|
|
@@ -20,13 +48,21 @@ const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) =>
|
|
|
20
48
|
const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
|
|
21
49
|
const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
|
|
22
50
|
_data.sort((a, b) => a[orderColumn] - b[orderColumn])
|
|
23
|
-
return _data.map(row => ({
|
|
51
|
+
return _data.map(row => ({
|
|
52
|
+
symbol: row[symbolColumn],
|
|
53
|
+
text: processFootnoteText(row[textColumn])
|
|
54
|
+
}))
|
|
24
55
|
}
|
|
25
56
|
return []
|
|
26
|
-
}, [config.dynamicFootnotes, config.data, filters])
|
|
57
|
+
}, [config.dynamicFootnotes, config.data, filters, markupVariables, enableMarkupVariables, data])
|
|
27
58
|
|
|
28
|
-
// get static footnotes from the config.footnotes
|
|
29
|
-
const staticFootnotes =
|
|
59
|
+
// get static footnotes from the config.footnotes and process their text
|
|
60
|
+
const staticFootnotes: Footnote[] = useMemo(() => {
|
|
61
|
+
return (config.staticFootnotes || []).map(footnote => ({
|
|
62
|
+
...footnote,
|
|
63
|
+
text: processFootnoteText(footnote.text)
|
|
64
|
+
}))
|
|
65
|
+
}, [config.staticFootnotes, markupVariables, enableMarkupVariables, data, filters])
|
|
30
66
|
|
|
31
67
|
return <Footnotes footnotes={[...apiFootnotes, ...staticFootnotes]} />
|
|
32
68
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* HeaderThemeSelector component styles */
|
|
2
|
+
|
|
3
|
+
.header {
|
|
4
|
+
margin-bottom: 1rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.header .edit-label {
|
|
8
|
+
display: block;
|
|
9
|
+
margin-bottom: 0.5rem;
|
|
10
|
+
font-weight: 500;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.header .color-palette {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
list-style: none;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.header .color-palette button {
|
|
23
|
+
width: 30px;
|
|
24
|
+
height: 30px;
|
|
25
|
+
border-radius: 50%;
|
|
26
|
+
border: 2px solid transparent;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: all 0.2s ease;
|
|
29
|
+
outline: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.header .color-palette button:hover {
|
|
33
|
+
transform: scale(1.1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.header .color-palette button.selected {
|
|
37
|
+
border-color: #000;
|
|
38
|
+
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.header .color-palette button:focus {
|
|
42
|
+
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
|
43
|
+
}
|
|
@@ -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
|
|
@@ -70,7 +70,7 @@ const LegendGradient = ({
|
|
|
70
70
|
const lastTick = index === labels.length - 1
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
|
-
<Group top={MARGIN}>
|
|
73
|
+
<Group key={`tick-${index}`} top={MARGIN}>
|
|
74
74
|
{!lastTick && !isLinearBlocks && <line x1={xPositionX} x2={xPositionX} y1={30} y2={boxHeight} stroke='black' />}
|
|
75
75
|
<Text
|
|
76
76
|
angle={-tickRotation}
|
|
@@ -123,9 +123,8 @@ const LegendGradient = ({
|
|
|
123
123
|
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
124
124
|
const xPosition = index * segmentWidth + MARGIN + getTickSeparatorsAdjustment(index)
|
|
125
125
|
return (
|
|
126
|
-
<Group>
|
|
126
|
+
<Group key={`color-block-${index}`}>
|
|
127
127
|
<rect
|
|
128
|
-
key={index}
|
|
129
128
|
x={xPosition}
|
|
130
129
|
y={MARGIN}
|
|
131
130
|
width={segmentWidth}
|
|
@@ -142,10 +141,9 @@ const LegendGradient = ({
|
|
|
142
141
|
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
143
142
|
const xPosition = separatorAfter * segmentWidth + MARGIN + getTickSeparatorsAdjustment(separatorAfter - 1)
|
|
144
143
|
return (
|
|
145
|
-
<Group>
|
|
144
|
+
<Group key={`separator-${index}`}>
|
|
146
145
|
{/* Separators block */}
|
|
147
146
|
<rect
|
|
148
|
-
key={index}
|
|
149
147
|
x={xPosition}
|
|
150
148
|
y={MARGIN / 2}
|
|
151
149
|
width={separatorSize}
|
|
@@ -157,7 +155,6 @@ const LegendGradient = ({
|
|
|
157
155
|
|
|
158
156
|
{/* Dotted dividing line */}
|
|
159
157
|
<line
|
|
160
|
-
key={index}
|
|
161
158
|
x1={xPosition + separatorSize / 2}
|
|
162
159
|
x2={xPosition + separatorSize / 2}
|
|
163
160
|
y1={-3}
|
|
@@ -1,24 +1,142 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
+
interface PatternInfo {
|
|
4
|
+
pattern: string
|
|
5
|
+
patternId: string
|
|
6
|
+
size?: string
|
|
7
|
+
color?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
interface LegendShapeProps {
|
|
4
11
|
fill: string
|
|
5
12
|
borderColor?: string
|
|
6
13
|
display?: 'inline-block' | 'block' | 'inline'
|
|
7
14
|
shape?: 'circle' | 'square'
|
|
15
|
+
patternInfo?: PatternInfo
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
const LegendShape: React.FC<LegendShapeProps> = props => {
|
|
11
|
-
const { fill, borderColor, display = 'inline-block', shape = 'circle' } = props
|
|
19
|
+
const { fill, borderColor, display = 'inline-block', shape = 'circle', patternInfo } = props
|
|
12
20
|
const dimensions = { width: '1em', height: '1em' }
|
|
13
21
|
const isCircleOrSquare = ['circle', 'square'].includes(shape)
|
|
22
|
+
|
|
23
|
+
// If pattern is provided, use SVG with pattern fill
|
|
24
|
+
if (patternInfo) {
|
|
25
|
+
const sizes = {
|
|
26
|
+
small: '8',
|
|
27
|
+
medium: '10',
|
|
28
|
+
large: '12'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const patternSize = sizes[patternInfo.size as keyof typeof sizes] || '10'
|
|
32
|
+
// Use the exact pattern color from config, with a reliable fallback
|
|
33
|
+
const patternColor = patternInfo.color || '#212529'
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<span className={`legend-item ${isCircleOrSquare ? 'me-2' : ''}`} style={{ display, verticalAlign: 'middle', width: dimensions.width, height: dimensions.height }}>
|
|
37
|
+
<svg width="100%" height="100%" viewBox="0 0 16 16" className="legend-shape-svg">
|
|
38
|
+
{/* Pattern definitions */}
|
|
39
|
+
<defs>
|
|
40
|
+
{patternInfo.pattern === 'waves' && (
|
|
41
|
+
<pattern
|
|
42
|
+
id={patternInfo.patternId}
|
|
43
|
+
patternUnits="userSpaceOnUse"
|
|
44
|
+
width={patternSize}
|
|
45
|
+
height={patternSize}
|
|
46
|
+
>
|
|
47
|
+
<path
|
|
48
|
+
d={`M0,${parseInt(patternSize) / 2} Q${parseInt(patternSize) / 4},0 ${parseInt(patternSize) / 2},${parseInt(patternSize) / 2} T${patternSize},${parseInt(patternSize) / 2}`}
|
|
49
|
+
stroke={patternColor}
|
|
50
|
+
strokeWidth="0.25"
|
|
51
|
+
fill="none"
|
|
52
|
+
/>
|
|
53
|
+
</pattern>
|
|
54
|
+
)}
|
|
55
|
+
{patternInfo.pattern === 'circles' && (
|
|
56
|
+
<pattern
|
|
57
|
+
id={patternInfo.patternId}
|
|
58
|
+
patternUnits="userSpaceOnUse"
|
|
59
|
+
width={patternSize}
|
|
60
|
+
height={patternSize}
|
|
61
|
+
>
|
|
62
|
+
<circle
|
|
63
|
+
cx={parseInt(patternSize) / 2}
|
|
64
|
+
cy={parseInt(patternSize) / 2}
|
|
65
|
+
r="1.25"
|
|
66
|
+
fill={patternColor}
|
|
67
|
+
/>
|
|
68
|
+
</pattern>
|
|
69
|
+
)}
|
|
70
|
+
{patternInfo.pattern === 'lines' && (
|
|
71
|
+
<pattern
|
|
72
|
+
id={patternInfo.patternId}
|
|
73
|
+
patternUnits="userSpaceOnUse"
|
|
74
|
+
width={patternSize}
|
|
75
|
+
height={patternSize}
|
|
76
|
+
>
|
|
77
|
+
<line
|
|
78
|
+
x1="0"
|
|
79
|
+
y1="0"
|
|
80
|
+
x2={patternSize}
|
|
81
|
+
y2={patternSize}
|
|
82
|
+
stroke={patternColor}
|
|
83
|
+
strokeWidth="0.75"
|
|
84
|
+
/>
|
|
85
|
+
</pattern>
|
|
86
|
+
)}
|
|
87
|
+
</defs>
|
|
88
|
+
|
|
89
|
+
{shape === 'circle' ? (
|
|
90
|
+
<circle
|
|
91
|
+
fill={fill}
|
|
92
|
+
r={7.5}
|
|
93
|
+
cx={8}
|
|
94
|
+
cy={8}
|
|
95
|
+
stroke={borderColor || 'rgba(0,0,0,.3)'}
|
|
96
|
+
strokeWidth={1}
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<rect
|
|
100
|
+
fill={fill}
|
|
101
|
+
width={15}
|
|
102
|
+
height={15}
|
|
103
|
+
x={0.5}
|
|
104
|
+
y={0.5}
|
|
105
|
+
stroke={borderColor || 'rgba(0,0,0,.3)'}
|
|
106
|
+
strokeWidth={1}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
{shape === 'circle' ? (
|
|
110
|
+
<circle
|
|
111
|
+
fill={`url(#${patternInfo.patternId})`}
|
|
112
|
+
r={7.5}
|
|
113
|
+
cx={8}
|
|
114
|
+
cy={8}
|
|
115
|
+
stroke='none'
|
|
116
|
+
/>
|
|
117
|
+
) : (
|
|
118
|
+
<rect
|
|
119
|
+
fill={`url(#${patternInfo.patternId})`}
|
|
120
|
+
width={15}
|
|
121
|
+
height={15}
|
|
122
|
+
x={0.5}
|
|
123
|
+
y={0.5}
|
|
124
|
+
stroke='none'
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</svg>
|
|
128
|
+
</span>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Default solid color shape
|
|
14
133
|
const styles = {
|
|
15
134
|
borderRadius: shape === 'circle' ? '50%' : '0px',
|
|
16
|
-
verticalAlign: 'middle',
|
|
17
135
|
display: display,
|
|
18
136
|
height: dimensions.height,
|
|
19
137
|
width: dimensions.width,
|
|
20
138
|
border: borderColor ? `${borderColor} 1px solid` : 'rgba(0,0,0,.3) 1px solid',
|
|
21
|
-
backgroundColor: fill
|
|
139
|
+
backgroundColor: fill,
|
|
22
140
|
}
|
|
23
141
|
|
|
24
142
|
return <span className={`legend-item ${isCircleOrSquare ? 'me-2' : ''}`} style={styles} />
|
|
@@ -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
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
// import html2pdf from 'html2pdf.js'
|
|
3
3
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
4
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
4
5
|
|
|
5
6
|
const buttonText = {
|
|
6
7
|
pdf: 'Download PDF',
|
|
@@ -38,63 +39,99 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
38
39
|
// Identify Selector
|
|
39
40
|
const baseSvg = document.querySelector(`[data-download-id=${elementToCapture}]`)
|
|
40
41
|
|
|
42
|
+
// Extract title from different state structures
|
|
43
|
+
const getTitle = state => {
|
|
44
|
+
if (state?.dashboard?.title) return state.dashboard.title
|
|
45
|
+
if (state?.general?.title) return state.general.title
|
|
46
|
+
if (state?.title) return state.title
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate timestamp once for consistency
|
|
51
|
+
const date = new Date()
|
|
52
|
+
const day = date.getDate()
|
|
53
|
+
const month = date.getMonth() + 1
|
|
54
|
+
const year = date.getFullYear()
|
|
55
|
+
const timestamp = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
|
|
56
|
+
|
|
41
57
|
// Handles different state title locations between components
|
|
42
58
|
// Apparently some packages use state.title where others use state.general.title
|
|
43
59
|
const handleFileName = state => {
|
|
44
60
|
// dashboard titles
|
|
45
61
|
if (state?.dashboard?.title)
|
|
46
62
|
return (
|
|
47
|
-
state.dashboard.title.replace(/\s+/g, '-').toLowerCase()
|
|
48
|
-
'-' +
|
|
49
|
-
date.getDate() +
|
|
50
|
-
date.getMonth() +
|
|
51
|
-
date.getFullYear()
|
|
63
|
+
`${state.dashboard.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
52
64
|
)
|
|
53
65
|
|
|
54
66
|
// map titles
|
|
55
67
|
if (state?.general?.title)
|
|
56
68
|
return (
|
|
57
|
-
state.general.title.replace(/\s+/g, '-').toLowerCase()
|
|
58
|
-
'-' +
|
|
59
|
-
date.getDate() +
|
|
60
|
-
date.getMonth() +
|
|
61
|
-
date.getFullYear()
|
|
69
|
+
`${state.general.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
62
70
|
)
|
|
63
71
|
|
|
64
72
|
// chart titles
|
|
65
73
|
if (state?.title)
|
|
66
74
|
return (
|
|
67
|
-
state.title.replace(/\s+/g, '-').toLowerCase()
|
|
75
|
+
`${state.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
68
76
|
)
|
|
69
77
|
|
|
70
78
|
return 'no-title'
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
// Construct filename with timestamp
|
|
74
|
-
const date = new Date()
|
|
75
81
|
const filename = handleFileName(state)
|
|
76
82
|
|
|
77
83
|
switch (type) {
|
|
78
84
|
case 'image':
|
|
79
85
|
const container = document.createElement('div')
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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`
|
|
83
91
|
}
|
|
84
|
-
|
|
92
|
+
|
|
93
|
+
container.appendChild(baseSvg.cloneNode(true));
|
|
85
94
|
|
|
86
95
|
const downloadImage = async () => {
|
|
87
96
|
document.body.appendChild(container) // Append container to the DOM
|
|
97
|
+
|
|
98
|
+
// Fix select elements to show their current selected values before screenshot
|
|
99
|
+
const selectElements = container.querySelectorAll('select')
|
|
100
|
+
const originalSelects = baseSvg.querySelectorAll('select')
|
|
101
|
+
|
|
102
|
+
selectElements.forEach((select, index) => {
|
|
103
|
+
const originalSelect = originalSelects[index]
|
|
104
|
+
if (originalSelect && originalSelect.value) {
|
|
105
|
+
select.value = originalSelect.value
|
|
106
|
+
// Also update the visual display for browsers that don't update immediately
|
|
107
|
+
const selectedOption = select.querySelector(`option[value="${originalSelect.value}"]`) as HTMLOptionElement
|
|
108
|
+
if (selectedOption) {
|
|
109
|
+
selectedOption.selected = true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
88
114
|
import(/* webpackChunkName: "html2canvas" */ 'html2canvas').then(mod => {
|
|
89
115
|
mod
|
|
90
116
|
.default(container, {
|
|
91
117
|
ignoreElements: el =>
|
|
92
118
|
el.className?.indexOf &&
|
|
93
|
-
el.className.search(/download-buttons|download-links|data-table-container/) !== -1
|
|
119
|
+
el.className.search(/download-buttons|download-links|data-table-container/) !== -1,
|
|
120
|
+
useCORS: true,
|
|
121
|
+
scale: 2, // Better quality
|
|
122
|
+
allowTaint: true,
|
|
94
123
|
})
|
|
95
124
|
.then(canvas => {
|
|
125
|
+
document.body.removeChild(container) // Clean up container
|
|
96
126
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
97
|
-
publishAnalyticsEvent(
|
|
127
|
+
publishAnalyticsEvent({
|
|
128
|
+
vizType: state.type,
|
|
129
|
+
vizSubType: getVizSubType(state),
|
|
130
|
+
eventType: `image_download`,
|
|
131
|
+
eventAction: 'click',
|
|
132
|
+
eventLabel: interactionLabel,
|
|
133
|
+
vizTitle: getTitle(state)
|
|
134
|
+
})
|
|
98
135
|
})
|
|
99
136
|
})
|
|
100
137
|
}
|
|
@@ -151,7 +188,14 @@ const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
|
|
|
151
188
|
title={buttonText.link}
|
|
152
189
|
target='_blank'
|
|
153
190
|
onClick={() => {
|
|
154
|
-
publishAnalyticsEvent(
|
|
191
|
+
publishAnalyticsEvent({
|
|
192
|
+
vizType: config.type,
|
|
193
|
+
vizSubType: getVizSubType(config),
|
|
194
|
+
eventType: 'clicked_data_link_to_view',
|
|
195
|
+
eventAction: 'click',
|
|
196
|
+
eventLabel: interactionLabel,
|
|
197
|
+
vizTitle: getVizTitle(config)
|
|
198
|
+
})
|
|
155
199
|
}}
|
|
156
200
|
>
|
|
157
201
|
{buttonText.link}
|
|
@@ -166,7 +210,14 @@ const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
|
|
|
166
210
|
title='Link to view full data set'
|
|
167
211
|
target='_blank'
|
|
168
212
|
onClick={() => {
|
|
169
|
-
publishAnalyticsEvent(
|
|
213
|
+
publishAnalyticsEvent({
|
|
214
|
+
vizType: config.type,
|
|
215
|
+
vizSubType: getVizSubType(config),
|
|
216
|
+
eventType: 'data_viewed',
|
|
217
|
+
eventAction: 'click',
|
|
218
|
+
eventLabel: interactionLabel,
|
|
219
|
+
vizTitle: getVizTitle(config)
|
|
220
|
+
})
|
|
170
221
|
}}
|
|
171
222
|
>
|
|
172
223
|
{buttonText.link}
|