@cdc/core 4.25.8 → 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 +56 -38
- package/components/DataTable/components/ChartHeader.tsx +44 -14
- package/components/DataTable/components/ExpandCollapse.tsx +10 -1
- package/components/DataTable/components/MapHeader.tsx +24 -13
- 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 +40 -14
- 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 +27 -20
- package/components/Filters/components/Tabs.tsx +1 -0
- package/components/Legend/Legend.Gradient.tsx +3 -6
- package/components/LegendShape.tsx +121 -3
- package/components/MediaControls.tsx +51 -3
- 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 -11
- 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/coveUpdateWorker.ts +13 -3
- 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 +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/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 +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/hooks/useColorPalette.ts +79 -0
- package/package.json +12 -4
- 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/MarkupInclude.ts +6 -1
- package/types/MarkupVariable.ts +19 -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/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() {
|
|
@@ -20,6 +20,7 @@ import { applyQueuedActive } from './helpers/applyQueuedActive'
|
|
|
20
20
|
import Tabs from './components/Tabs'
|
|
21
21
|
import Dropdown from './components/Dropdown'
|
|
22
22
|
import { publishAnalyticsEvent } from '../../helpers/metrics/helpers'
|
|
23
|
+
import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
|
|
23
24
|
|
|
24
25
|
export const VIZ_FILTER_STYLE = {
|
|
25
26
|
dropdown: 'dropdown',
|
|
@@ -87,12 +88,15 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
87
88
|
const newFilters = getChangedFilters([...filters], index, value, filterBehavior)
|
|
88
89
|
setFilters(newFilters)
|
|
89
90
|
|
|
90
|
-
publishAnalyticsEvent(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`${
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
})
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
const handleApplyButton = newFilters => {
|
|
@@ -113,17 +117,19 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
113
117
|
|
|
114
118
|
setFilters(newFilters)
|
|
115
119
|
|
|
116
|
-
publishAnalyticsEvent(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
})
|
|
122
128
|
|
|
123
129
|
setShowApplyButton(false)
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
const
|
|
132
|
+
const handleFiltersReset = e => {
|
|
127
133
|
let newFilters = [...filters]
|
|
128
134
|
e.preventDefault()
|
|
129
135
|
|
|
@@ -148,12 +154,13 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
setFilters(newFilters)
|
|
151
|
-
publishAnalyticsEvent(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
})
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
const mobileFilterStyle = useMemo(() => {
|
|
@@ -288,7 +295,7 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
288
295
|
>
|
|
289
296
|
Apply
|
|
290
297
|
</Button>
|
|
291
|
-
<Button secondary disabled={initialFiltersActive} onClick={
|
|
298
|
+
<Button secondary disabled={initialFiltersActive} onClick={handleFiltersReset}>
|
|
292
299
|
Clear Filters
|
|
293
300
|
</Button>
|
|
294
301
|
</div>
|
|
@@ -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 => {
|
|
@@ -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,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,6 +39,14 @@ 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
|
+
|
|
41
50
|
// Handles different state title locations between components
|
|
42
51
|
// Apparently some packages use state.title where others use state.general.title
|
|
43
52
|
const handleFileName = state => {
|
|
@@ -85,6 +94,23 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
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, {
|
|
@@ -93,8 +119,16 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
93
119
|
el.className.search(/download-buttons|download-links|data-table-container/) !== -1
|
|
94
120
|
})
|
|
95
121
|
.then(canvas => {
|
|
122
|
+
document.body.removeChild(container) // Clean up container
|
|
96
123
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
97
|
-
publishAnalyticsEvent(
|
|
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
|
+
})
|
|
98
132
|
})
|
|
99
133
|
})
|
|
100
134
|
}
|
|
@@ -151,7 +185,14 @@ const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
|
|
|
151
185
|
title={buttonText.link}
|
|
152
186
|
target='_blank'
|
|
153
187
|
onClick={() => {
|
|
154
|
-
publishAnalyticsEvent(
|
|
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
|
+
})
|
|
155
196
|
}}
|
|
156
197
|
>
|
|
157
198
|
{buttonText.link}
|
|
@@ -166,7 +207,14 @@ const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
|
|
|
166
207
|
title='Link to view full data set'
|
|
167
208
|
target='_blank'
|
|
168
209
|
onClick={() => {
|
|
169
|
-
publishAnalyticsEvent(
|
|
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
|
+
})
|
|
170
218
|
}}
|
|
171
219
|
>
|
|
172
220
|
{buttonText.link}
|
|
@@ -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
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import _ from 'lodash'
|
|
3
|
+
import { isCoveDeveloperMode } from '../../helpers/queryStringUtils'
|
|
4
|
+
import {
|
|
5
|
+
hasPaletteBackup,
|
|
6
|
+
getOriginalPaletteName,
|
|
7
|
+
rollbackPaletteToOriginal,
|
|
8
|
+
hasTwoColorPaletteBackup,
|
|
9
|
+
getOriginalTwoColorPaletteName,
|
|
10
|
+
rollbackTwoColorPaletteToOriginal
|
|
11
|
+
} from '../../helpers/palettes/utils'
|
|
12
|
+
import './PaletteSelector.css'
|
|
13
|
+
|
|
14
|
+
interface DeveloperPaletteRollbackProps {
|
|
15
|
+
config: any
|
|
16
|
+
updateConfig: (newConfig: any) => void
|
|
17
|
+
className?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DeveloperPaletteRollback: React.FC<DeveloperPaletteRollbackProps> = ({
|
|
21
|
+
config,
|
|
22
|
+
updateConfig,
|
|
23
|
+
className = ''
|
|
24
|
+
}) => {
|
|
25
|
+
// Only show if developer mode is enabled
|
|
26
|
+
if (!isCoveDeveloperMode()) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for both regular and two-color palette backups
|
|
31
|
+
const hasRegularBackup = hasPaletteBackup(config)
|
|
32
|
+
const hasTwoColorBackup = hasTwoColorPaletteBackup(config)
|
|
33
|
+
|
|
34
|
+
// Only show if there's backup data available
|
|
35
|
+
if (!hasRegularBackup && !hasTwoColorBackup) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const originalPaletteName = getOriginalPaletteName(config)
|
|
40
|
+
const originalTwoColorPaletteName = getOriginalTwoColorPaletteName(config)
|
|
41
|
+
const currentPaletteName = config?.general?.palette?.name || config?.palette || config?.color
|
|
42
|
+
const currentTwoColorPaletteName = config?.twoColor?.palette
|
|
43
|
+
|
|
44
|
+
const handleRollback = () => {
|
|
45
|
+
// Determine which type of rollback to perform and show appropriate confirmation
|
|
46
|
+
let confirmMessage = ''
|
|
47
|
+
let fromName = ''
|
|
48
|
+
let toName = ''
|
|
49
|
+
|
|
50
|
+
if (hasTwoColorBackup) {
|
|
51
|
+
// Two-color palette rollback
|
|
52
|
+
fromName = currentTwoColorPaletteName
|
|
53
|
+
toName = originalTwoColorPaletteName
|
|
54
|
+
confirmMessage = `Are you sure you want to rollback the palette from "${fromName}" to "${toName}"?\n\nThis will restore the original v1 palette configuration.`
|
|
55
|
+
} else if (hasRegularBackup) {
|
|
56
|
+
// Regular palette rollback
|
|
57
|
+
fromName = currentPaletteName
|
|
58
|
+
toName = originalPaletteName
|
|
59
|
+
confirmMessage = `Are you sure you want to rollback the palette from "${fromName}" to "${toName}"?\n\nThis will restore the original palette configuration and remove the migrated structure.`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const confirmRollback = window.confirm(confirmMessage)
|
|
63
|
+
|
|
64
|
+
if (confirmRollback) {
|
|
65
|
+
const configCopy = _.cloneDeep(config)
|
|
66
|
+
console.log('Config before rollback:', JSON.stringify(configCopy, null, 2))
|
|
67
|
+
|
|
68
|
+
let success = false
|
|
69
|
+
|
|
70
|
+
if (hasTwoColorBackup) {
|
|
71
|
+
success = rollbackTwoColorPaletteToOriginal(configCopy)
|
|
72
|
+
console.log('Two-color rollback success:', success)
|
|
73
|
+
} else if (hasRegularBackup) {
|
|
74
|
+
const rolledBackConfig = rollbackPaletteToOriginal(configCopy)
|
|
75
|
+
success = !!rolledBackConfig
|
|
76
|
+
console.log('Regular rollback success:', success)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('Config after rollback:', JSON.stringify(configCopy, null, 2))
|
|
80
|
+
|
|
81
|
+
if (success) {
|
|
82
|
+
updateConfig(configCopy)
|
|
83
|
+
} else {
|
|
84
|
+
alert('Rollback failed: No backup data available')
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Determine display text based on available backups
|
|
90
|
+
const displayPaletteName = hasTwoColorBackup ? originalTwoColorPaletteName : originalPaletteName
|
|
91
|
+
const infoText = hasTwoColorBackup
|
|
92
|
+
? `Developer Mode: Two-color palette migrated from "${originalTwoColorPaletteName}"`
|
|
93
|
+
: `Developer Mode: Migrated from "${originalPaletteName}"`
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={`developer-palette-rollback ${className}`}>
|
|
97
|
+
<div className="rollback-info">
|
|
98
|
+
<small className="text-muted">
|
|
99
|
+
{infoText}
|
|
100
|
+
</small>
|
|
101
|
+
</div>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className="btn btn-sm btn-outline-warning"
|
|
105
|
+
onClick={handleRollback}
|
|
106
|
+
title={`Rollback to original palette: ${displayPaletteName}`}
|
|
107
|
+
>
|
|
108
|
+
Rollback Palette
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default DeveloperPaletteRollback
|