@cdc/core 4.25.7 → 4.25.10
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/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
- package/components/DataTable/DataTable.tsx +63 -11
- package/components/DataTable/DataTableStandAlone.tsx +4 -1
- package/components/DataTable/components/ChartHeader.tsx +58 -9
- package/components/DataTable/components/ExpandCollapse.tsx +21 -1
- package/components/DataTable/components/MapHeader.tsx +35 -7
- package/components/DataTable/data-table.css +6 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
- package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
- package/components/DownloadButton.tsx +42 -13
- package/components/EditorPanel/DataTableEditor.tsx +10 -1
- package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
- package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
- package/components/ErrorBoundary.jsx +3 -1
- package/components/Filters/Filters.tsx +35 -11
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Footnotes/FootnotesStandAlone.tsx +2 -1
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/{MediaControls.jsx → MediaControls.tsx} +80 -16
- package/components/PaletteConversionModal.tsx +87 -0
- package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
- package/components/PaletteSelector/PaletteSelector.css +51 -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/_stories/BlurStrokeTest.stories.tsx +1 -1
- package/components/_stories/DataTable.stories.tsx +1 -1
- package/components/_stories/Filters.stories.tsx +1 -1
- package/components/_stories/Footnotes.stories.tsx +1 -1
- package/components/_stories/Inputs.stories.tsx +1 -1
- 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/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/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/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 +14 -13
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +80 -0
- package/helpers/addValuesToFilters.ts +2 -3
- package/helpers/cloneConfig.ts +31 -0
- package/helpers/configDataHelpers.ts +128 -0
- package/helpers/configHelpers.ts +27 -0
- package/helpers/constants.ts +5 -2
- package/helpers/cove/number.ts +6 -2
- package/helpers/coveUpdateWorker.ts +15 -3
- package/helpers/events.ts +32 -0
- 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 +205 -0
- package/helpers/metrics/helpers.ts +75 -0
- package/helpers/metrics/types.ts +82 -0
- 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/queryStringUtils.ts +13 -0
- package/helpers/testing.ts +345 -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.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 +62 -0
- 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 +86 -0
- 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 +12 -4
- package/styles/_button-section.scss +0 -2
- package/styles/_global.scss +7 -5
- package/styles/base.scss +8 -5
- package/styles/v2/components/button.scss +4 -3
- package/styles/v2/components/editor.scss +2 -1
- package/styles/v2/layout/_data-table.scss +3 -2
- package/styles/v2/themes/_color-definitions.scss +18 -17
- package/testBuild.js +0 -0
- package/testing-setup.js +32 -0
- package/types/ForecastingSeriesKey.ts +0 -1
- package/types/MarkupInclude.ts +6 -1
- package/types/MarkupVariable.ts +19 -0
- package/types/Series.ts +4 -0
- package/types/Table.ts +1 -0
- package/types/VizFilter.ts +1 -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/events.js +0 -14
- package/helpers/formatConfigBeforeSave.ts +0 -135
- package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import MarkupVariablesEditor from './MarkupVariablesEditor'
|
|
3
|
+
import Accordion from '../../ui/Accordion'
|
|
4
|
+
import { MarkupVariable } from '../../../types/MarkupVariable'
|
|
5
|
+
|
|
6
|
+
type PanelMarkupProps = {
|
|
7
|
+
/** Display name for the panel */
|
|
8
|
+
name: string
|
|
9
|
+
/** Array of markup variable configurations */
|
|
10
|
+
markupVariables: MarkupVariable[]
|
|
11
|
+
/** Dataset to extract column names and values from */
|
|
12
|
+
data: any[]
|
|
13
|
+
/** Whether markup variables feature is enabled */
|
|
14
|
+
enableMarkupVariables: boolean
|
|
15
|
+
/** Callback when variables are added, updated, or removed */
|
|
16
|
+
onMarkupVariablesChange: (variables: MarkupVariable[]) => void
|
|
17
|
+
/** Callback when enable/disable toggle changes */
|
|
18
|
+
onToggleEnable: (enabled: boolean) => void
|
|
19
|
+
/** Optional: wrap in accordion. Default true */
|
|
20
|
+
withAccordion?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shared panel for markup variables editor across all visualization packages.
|
|
25
|
+
* Wraps MarkupVariablesEditor with optional accordion functionality.
|
|
26
|
+
*/
|
|
27
|
+
const PanelMarkup: React.FC<PanelMarkupProps> = ({
|
|
28
|
+
name,
|
|
29
|
+
markupVariables,
|
|
30
|
+
data,
|
|
31
|
+
enableMarkupVariables,
|
|
32
|
+
onMarkupVariablesChange,
|
|
33
|
+
onToggleEnable,
|
|
34
|
+
withAccordion = true
|
|
35
|
+
}) => {
|
|
36
|
+
const content = (
|
|
37
|
+
<MarkupVariablesEditor
|
|
38
|
+
markupVariables={markupVariables || []}
|
|
39
|
+
data={data}
|
|
40
|
+
onChange={onMarkupVariablesChange}
|
|
41
|
+
enableMarkupVariables={enableMarkupVariables || false}
|
|
42
|
+
onToggleEnable={onToggleEnable}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (!withAccordion) {
|
|
47
|
+
return content
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Accordion key={name}>
|
|
52
|
+
<Accordion.Section title={name} key={name}>
|
|
53
|
+
{content}
|
|
54
|
+
</Accordion.Section>
|
|
55
|
+
</Accordion>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default PanelMarkup
|
|
@@ -13,7 +13,9 @@ class ErrorBoundary extends React.Component {
|
|
|
13
13
|
|
|
14
14
|
componentDidCatch(error, errorInfo) {
|
|
15
15
|
// You can also log the error to an error reporting service
|
|
16
|
-
console.
|
|
16
|
+
console.error('ErrorBoundary caught an error:', error)
|
|
17
|
+
console.error('Error info:', errorInfo)
|
|
18
|
+
console.error('Error stack:', error.stack)
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
render() {
|
|
@@ -14,10 +14,13 @@ import { getNestedOptions } from './helpers/getNestedOptions'
|
|
|
14
14
|
import { getWrappingStatuses } from './helpers/filterWrapping'
|
|
15
15
|
import { handleSorting } from './helpers/handleSorting'
|
|
16
16
|
import { getChangedFilters } from './helpers/getChangedFilters'
|
|
17
|
+
import { getUniqueValues } from '@cdc/map/src/helpers'
|
|
17
18
|
import { getQueryParams, updateQueryString } from '../../helpers/queryStringUtils'
|
|
18
19
|
import { applyQueuedActive } from './helpers/applyQueuedActive'
|
|
19
20
|
import Tabs from './components/Tabs'
|
|
20
21
|
import Dropdown from './components/Dropdown'
|
|
22
|
+
import { publishAnalyticsEvent } from '../../helpers/metrics/helpers'
|
|
23
|
+
import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
|
|
21
24
|
|
|
22
25
|
export const VIZ_FILTER_STYLE = {
|
|
23
26
|
dropdown: 'dropdown',
|
|
@@ -33,18 +36,13 @@ export type VizFilterStyle = (typeof VIZ_FILTER_STYLE)[keyof typeof VIZ_FILTER_S
|
|
|
33
36
|
|
|
34
37
|
export const filterStyleOptions = Object.values(VIZ_FILTER_STYLE)
|
|
35
38
|
|
|
36
|
-
const BUTTON_TEXT = {
|
|
37
|
-
apply: 'Apply',
|
|
38
|
-
resetText: 'Clear Filters'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
39
|
type FilterProps = {
|
|
42
40
|
dimensions?: DimensionsType
|
|
43
41
|
config: Visualization
|
|
44
42
|
setFilters: Function
|
|
45
43
|
standaloneMap?: boolean
|
|
46
44
|
excludedData?: Object[]
|
|
47
|
-
|
|
45
|
+
interactionLabel?: string
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
const Filters: React.FC<FilterProps> = ({
|
|
@@ -53,7 +51,7 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
53
51
|
standaloneMap,
|
|
54
52
|
setFilters,
|
|
55
53
|
excludedData,
|
|
56
|
-
|
|
54
|
+
interactionLabel = ''
|
|
57
55
|
}) => {
|
|
58
56
|
const { filters, general, theme, filterBehavior } = visualizationConfig
|
|
59
57
|
const [showApplyButton, setShowApplyButton] = useState(false)
|
|
@@ -89,6 +87,16 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
89
87
|
|
|
90
88
|
const newFilters = getChangedFilters([...filters], index, value, filterBehavior)
|
|
91
89
|
setFilters(newFilters)
|
|
90
|
+
|
|
91
|
+
publishAnalyticsEvent({
|
|
92
|
+
vizType: visualizationConfig.type as any,
|
|
93
|
+
vizSubType: getVizSubType(visualizationConfig),
|
|
94
|
+
eventType: `${visualizationConfig.type}_filter_changed` as any,
|
|
95
|
+
eventAction: 'change',
|
|
96
|
+
eventLabel: interactionLabel,
|
|
97
|
+
vizTitle: getVizTitle(visualizationConfig),
|
|
98
|
+
specifics: `key: ${String(newFilters?.[index]?.columnName).toLowerCase()}, value: ${String(newFilters?.[index]?.active).toLowerCase()}`
|
|
99
|
+
})
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
const handleApplyButton = newFilters => {
|
|
@@ -109,10 +117,19 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
109
117
|
|
|
110
118
|
setFilters(newFilters)
|
|
111
119
|
|
|
120
|
+
publishAnalyticsEvent({
|
|
121
|
+
vizType: visualizationConfig.type as any,
|
|
122
|
+
eventType: `${visualizationConfig.type}_filter_applied` as any,
|
|
123
|
+
eventAction: 'click',
|
|
124
|
+
eventLabel: interactionLabel,
|
|
125
|
+
vizTitle: getVizTitle(visualizationConfig),
|
|
126
|
+
specifics: newFilters.map(f => f.active).join(',')
|
|
127
|
+
})
|
|
128
|
+
|
|
112
129
|
setShowApplyButton(false)
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
const
|
|
132
|
+
const handleFiltersReset = e => {
|
|
116
133
|
let newFilters = [...filters]
|
|
117
134
|
e.preventDefault()
|
|
118
135
|
|
|
@@ -137,6 +154,13 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
setFilters(newFilters)
|
|
157
|
+
publishAnalyticsEvent({
|
|
158
|
+
vizType: visualizationConfig.type as any,
|
|
159
|
+
eventType: `${visualizationConfig.type}_filter_reset` as any,
|
|
160
|
+
eventAction: 'click',
|
|
161
|
+
eventLabel: interactionLabel,
|
|
162
|
+
vizTitle: visualizationConfig?.title
|
|
163
|
+
})
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
const mobileFilterStyle = useMemo(() => {
|
|
@@ -269,10 +293,10 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
269
293
|
disabled={!showApplyButton}
|
|
270
294
|
className={[general?.headerColor ? general.headerColor : theme, 'apply', 'me-2'].join(' ')}
|
|
271
295
|
>
|
|
272
|
-
|
|
296
|
+
Apply
|
|
273
297
|
</Button>
|
|
274
|
-
<Button secondary disabled={initialFiltersActive} onClick={
|
|
275
|
-
|
|
298
|
+
<Button secondary disabled={initialFiltersActive} onClick={handleFiltersReset}>
|
|
299
|
+
Clear Filters
|
|
276
300
|
</Button>
|
|
277
301
|
</div>
|
|
278
302
|
) : (
|
|
@@ -41,6 +41,7 @@ const Tabs: React.FC<TabsProps> = ({ filter, index: outerIndex, changeFilterActi
|
|
|
41
41
|
const Tabs = filter.values.map((value, index) => {
|
|
42
42
|
return (
|
|
43
43
|
<button
|
|
44
|
+
key={`${value}-${outerIndex}-${index}-${id}`}
|
|
44
45
|
id={`${value}-${outerIndex}-${index}-${id}`}
|
|
45
46
|
className={getClassList(value)}
|
|
46
47
|
onClick={e => {
|
|
@@ -14,7 +14,8 @@ const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) =>
|
|
|
14
14
|
if (!config) return null
|
|
15
15
|
// get the api footnotes from the config
|
|
16
16
|
const apiFootnotes = useMemo(() => {
|
|
17
|
-
|
|
17
|
+
// If filters exist and should filter footnotes, apply them, otherwise use data as-is
|
|
18
|
+
const configData = filters && filters.length > 0 ? filterVizData(filters, config.data) : config.data
|
|
18
19
|
if (configData && config.dataKey && config.dynamicFootnotes) {
|
|
19
20
|
const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
|
|
20
21
|
const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
|
|
@@ -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} />
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
// import html2pdf from 'html2pdf.js'
|
|
3
|
+
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
4
|
+
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
3
5
|
|
|
4
6
|
const buttonText = {
|
|
5
7
|
pdf: 'Download PDF',
|
|
@@ -33,10 +35,18 @@ const saveImageAs = (uri, filename) => {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
const generateMedia = (state, type, elementToCapture) => {
|
|
38
|
+
const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
37
39
|
// Identify Selector
|
|
38
40
|
const baseSvg = document.querySelector(`[data-download-id=${elementToCapture}]`)
|
|
39
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
|
+
|
|
40
50
|
// Handles different state title locations between components
|
|
41
51
|
// Apparently some packages use state.title where others use state.general.title
|
|
42
52
|
const handleFileName = state => {
|
|
@@ -75,20 +85,55 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
75
85
|
|
|
76
86
|
switch (type) {
|
|
77
87
|
case 'image':
|
|
88
|
+
const container = document.createElement('div')
|
|
89
|
+
// On screenshots without a title (like some charts), add padding around the chart svg
|
|
90
|
+
if (!state.showTitle) {
|
|
91
|
+
container.style.padding = '35px'
|
|
92
|
+
}
|
|
93
|
+
container.appendChild(baseSvg.cloneNode(true)) // Clone baseSvg to avoid modifying the original
|
|
94
|
+
|
|
78
95
|
const downloadImage = async () => {
|
|
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
|
+
|
|
79
114
|
import(/* webpackChunkName: "html2canvas" */ 'html2canvas').then(mod => {
|
|
80
115
|
mod
|
|
81
|
-
.default(
|
|
116
|
+
.default(container, {
|
|
82
117
|
ignoreElements: el =>
|
|
83
118
|
el.className?.indexOf &&
|
|
84
119
|
el.className.search(/download-buttons|download-links|data-table-container/) !== -1
|
|
85
120
|
})
|
|
86
121
|
.then(canvas => {
|
|
122
|
+
document.body.removeChild(container) // Clean up container
|
|
87
123
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
124
|
+
publishAnalyticsEvent({
|
|
125
|
+
vizType: state.type,
|
|
126
|
+
vizSubType: getVizSubType(state),
|
|
127
|
+
eventType: `image_download`,
|
|
128
|
+
eventAction: 'click',
|
|
129
|
+
eventLabel: interactionLabel,
|
|
130
|
+
vizTitle: getTitle(state)
|
|
131
|
+
})
|
|
88
132
|
})
|
|
89
133
|
})
|
|
90
134
|
}
|
|
91
135
|
downloadImage()
|
|
136
|
+
|
|
92
137
|
return
|
|
93
138
|
case 'pdf':
|
|
94
139
|
// let opt = {
|
|
@@ -115,22 +160,13 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
115
160
|
}
|
|
116
161
|
}
|
|
117
162
|
|
|
118
|
-
|
|
119
|
-
// Apparently some packages use state.headerColor where others use state.theme
|
|
120
|
-
const handleTheme = state => {
|
|
121
|
-
if (state?.headerColor) return state.headerColor // ie. maps
|
|
122
|
-
if (state?.theme) return state.theme // ie. charts
|
|
123
|
-
return 'theme-notFound'
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Download CSV
|
|
127
|
-
const Button = ({ state, text, type, title, elementToCapture }) => {
|
|
163
|
+
const Button = ({ state, text, type, title, elementToCapture, interactionLabel = '' }) => {
|
|
128
164
|
const buttonClasses = ['btn', 'btn-primary']
|
|
129
165
|
return (
|
|
130
166
|
<button
|
|
131
167
|
className={buttonClasses.join(' ')}
|
|
132
168
|
title={title}
|
|
133
|
-
onClick={() => generateMedia(state, type, elementToCapture)}
|
|
169
|
+
onClick={() => generateMedia(state, type, elementToCapture, interactionLabel)}
|
|
134
170
|
style={{ lineHeight: '1.4em' }}
|
|
135
171
|
>
|
|
136
172
|
{buttonText[type]}
|
|
@@ -139,12 +175,26 @@ const Button = ({ state, text, type, title, elementToCapture }) => {
|
|
|
139
175
|
}
|
|
140
176
|
|
|
141
177
|
// Link to CSV/JSON data
|
|
142
|
-
const Link = ({ config, dashboardDataConfig }) => {
|
|
178
|
+
const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
|
|
143
179
|
let dataConfig = dashboardDataConfig || config
|
|
144
180
|
// Handles Maps & Charts
|
|
145
181
|
if (dataConfig.dataFileSourceType === 'url' && dataConfig.dataFileName && config.table.showDownloadUrl) {
|
|
146
182
|
return (
|
|
147
|
-
<a
|
|
183
|
+
<a
|
|
184
|
+
href={dataConfig.dataFileName}
|
|
185
|
+
title={buttonText.link}
|
|
186
|
+
target='_blank'
|
|
187
|
+
onClick={() => {
|
|
188
|
+
publishAnalyticsEvent({
|
|
189
|
+
vizType: config.type,
|
|
190
|
+
vizSubType: getVizSubType(config),
|
|
191
|
+
eventType: 'clicked_data_link_to_view',
|
|
192
|
+
eventAction: 'click',
|
|
193
|
+
eventLabel: interactionLabel,
|
|
194
|
+
vizTitle: getVizTitle(config)
|
|
195
|
+
})
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
148
198
|
{buttonText.link}
|
|
149
199
|
</a>
|
|
150
200
|
)
|
|
@@ -152,7 +202,21 @@ const Link = ({ config, dashboardDataConfig }) => {
|
|
|
152
202
|
|
|
153
203
|
// Handles Dashboards
|
|
154
204
|
return config?.table?.showDownloadUrl && dataConfig.dataUrl ? (
|
|
155
|
-
<a
|
|
205
|
+
<a
|
|
206
|
+
href={dataConfig.dataUrl}
|
|
207
|
+
title='Link to view full data set'
|
|
208
|
+
target='_blank'
|
|
209
|
+
onClick={() => {
|
|
210
|
+
publishAnalyticsEvent({
|
|
211
|
+
vizType: config.type,
|
|
212
|
+
vizSubType: getVizSubType(config),
|
|
213
|
+
eventType: 'data_viewed',
|
|
214
|
+
eventAction: 'click',
|
|
215
|
+
eventLabel: interactionLabel,
|
|
216
|
+
vizTitle: getVizTitle(config)
|
|
217
|
+
})
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
156
220
|
{buttonText.link}
|
|
157
221
|
</a>
|
|
158
222
|
) : null
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
3
|
+
|
|
4
|
+
interface PaletteConversionModalProps {
|
|
5
|
+
onConfirm: () => void
|
|
6
|
+
onCancel: () => void
|
|
7
|
+
onReturnToV1: () => void
|
|
8
|
+
paletteName?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PaletteConversionModal: React.FC<PaletteConversionModalProps> = ({
|
|
12
|
+
onConfirm,
|
|
13
|
+
onCancel,
|
|
14
|
+
onReturnToV1,
|
|
15
|
+
paletteName
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className='modal-overlay'
|
|
20
|
+
style={{
|
|
21
|
+
position: 'fixed',
|
|
22
|
+
top: 0,
|
|
23
|
+
left: 0,
|
|
24
|
+
right: 0,
|
|
25
|
+
bottom: 0,
|
|
26
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
zIndex: 9999
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
className='modal-content'
|
|
35
|
+
style={{
|
|
36
|
+
backgroundColor: 'white',
|
|
37
|
+
borderRadius: '8px',
|
|
38
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
39
|
+
maxWidth: '500px',
|
|
40
|
+
margin: '20px'
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className='modal-header'
|
|
45
|
+
style={{
|
|
46
|
+
padding: '20px 20px 0 20px',
|
|
47
|
+
borderBottom: '1px solid #e0e0e0'
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<h3 style={{ margin: '0 0 20px 0' }}>Color Palette Conversion</h3>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className='modal-body' style={{ padding: '20px' }}>
|
|
54
|
+
<p>
|
|
55
|
+
<strong>
|
|
56
|
+
Your visualization uses an outdated color palette and will be updated to a new, improved palette.
|
|
57
|
+
</strong>
|
|
58
|
+
</p>
|
|
59
|
+
<br />
|
|
60
|
+
<p>
|
|
61
|
+
These new palettes provide improved accessibility and consistency across visualizations. If your previous
|
|
62
|
+
colors are important for approvals, do not save your visualizations with the new palette.
|
|
63
|
+
</p>
|
|
64
|
+
<br />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div
|
|
68
|
+
className='modal-footer'
|
|
69
|
+
style={{
|
|
70
|
+
padding: '20px',
|
|
71
|
+
borderTop: '1px solid #e0e0e0',
|
|
72
|
+
display: 'flex',
|
|
73
|
+
gap: '10px',
|
|
74
|
+
justifyContent: 'flex-end'
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<Button secondary onClick={onReturnToV1} style={{ marginRight: 'auto' }}>
|
|
78
|
+
Cancel
|
|
79
|
+
</Button>
|
|
80
|
+
<Button onClick={onConfirm}>Convert to New Palette</Button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default PaletteConversionModal
|