@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.
Files changed (128) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +63 -11
  3. package/components/DataTable/DataTableStandAlone.tsx +4 -1
  4. package/components/DataTable/components/ChartHeader.tsx +58 -9
  5. package/components/DataTable/components/ExpandCollapse.tsx +21 -1
  6. package/components/DataTable/components/MapHeader.tsx +35 -7
  7. package/components/DataTable/data-table.css +6 -0
  8. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  9. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  10. package/components/DownloadButton.tsx +42 -13
  11. package/components/EditorPanel/DataTableEditor.tsx +10 -1
  12. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  13. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  14. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  15. package/components/ErrorBoundary.jsx +3 -1
  16. package/components/Filters/Filters.tsx +35 -11
  17. package/components/Filters/components/Tabs.tsx +1 -0
  18. package/components/Footnotes/FootnotesStandAlone.tsx +2 -1
  19. package/components/Legend/Legend.Gradient.tsx +3 -6
  20. package/components/LegendShape.tsx +121 -3
  21. package/components/{MediaControls.jsx → MediaControls.tsx} +80 -16
  22. package/components/PaletteConversionModal.tsx +87 -0
  23. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  24. package/components/PaletteSelector/PaletteSelector.css +51 -0
  25. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  26. package/components/PaletteSelector/index.ts +2 -0
  27. package/components/RichTooltip/RichTooltip.tsx +1 -0
  28. package/components/Table/Table.tsx +3 -1
  29. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  30. package/components/_stories/DataTable.stories.tsx +1 -1
  31. package/components/_stories/Filters.stories.tsx +1 -1
  32. package/components/_stories/Footnotes.stories.tsx +1 -1
  33. package/components/_stories/Inputs.stories.tsx +1 -1
  34. package/components/_stories/MultiSelect.stories.tsx +3 -3
  35. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  36. package/components/_stories/Table.stories.tsx +1 -1
  37. package/components/elements/_stories/Button.stories.tsx +1 -1
  38. package/components/elements/_stories/Card.stories.tsx +1 -1
  39. package/components/inputs/InputToggle.tsx +2 -0
  40. package/components/managers/DataDesigner.tsx +10 -9
  41. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  42. package/components/ui/Tooltip.tsx +2 -1
  43. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  44. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  45. package/components/ui/_stories/Colors.stories.tsx +330 -0
  46. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  47. package/components/ui/_stories/Title.stories.tsx +1 -1
  48. package/contexts/EditorContext.ts +18 -0
  49. package/contexts/editor.actions.ts +28 -0
  50. package/contexts/editor.reducer.ts +94 -0
  51. package/data/chartColorPalettes.ts +118 -0
  52. package/data/colorPalettes.ts +9 -0
  53. package/data/mapColorPalettes.ts +45 -0
  54. package/data/sharedPalettes.ts +50 -0
  55. package/dist/cove-main.css +14 -13
  56. package/dist/cove-main.css.map +1 -1
  57. package/generateViteConfig.js +80 -0
  58. package/helpers/addValuesToFilters.ts +2 -3
  59. package/helpers/cloneConfig.ts +31 -0
  60. package/helpers/configDataHelpers.ts +128 -0
  61. package/helpers/configHelpers.ts +27 -0
  62. package/helpers/constants.ts +5 -2
  63. package/helpers/cove/number.ts +6 -2
  64. package/helpers/coveUpdateWorker.ts +15 -3
  65. package/helpers/events.ts +32 -0
  66. package/helpers/filterColorPalettes.ts +152 -0
  67. package/helpers/generateColorsArray.ts +13 -0
  68. package/helpers/getColorPaletteVersion.ts +33 -0
  69. package/helpers/getPaletteAccessor.ts +18 -0
  70. package/helpers/markupProcessor.ts +205 -0
  71. package/helpers/metrics/helpers.ts +75 -0
  72. package/helpers/metrics/types.ts +82 -0
  73. package/helpers/metrics/utils.ts +34 -0
  74. package/helpers/palettes/colorDistributions.ts +56 -0
  75. package/helpers/palettes/migratePaletteName.ts +150 -0
  76. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  77. package/helpers/palettes/utils.ts +267 -0
  78. package/helpers/queryStringUtils.ts +13 -0
  79. package/helpers/testing.ts +345 -0
  80. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  81. package/helpers/tests/generateColorsArray.test.ts +24 -0
  82. package/helpers/tests/markupProcessor.test.ts +538 -0
  83. package/helpers/tests/testStandaloneBuild.ts +44 -0
  84. package/helpers/useMarkupVariables.ts +31 -0
  85. package/helpers/vegaConfig.ts +0 -1
  86. package/helpers/ver/4.24.10.ts +2 -1
  87. package/helpers/ver/4.24.11.ts +2 -1
  88. package/helpers/ver/4.24.3.ts +2 -1
  89. package/helpers/ver/4.24.4.ts +2 -1
  90. package/helpers/ver/4.24.5.ts +2 -1
  91. package/helpers/ver/4.24.7.ts +2 -1
  92. package/helpers/ver/4.24.9.ts +2 -1
  93. package/helpers/ver/4.25.1.ts +2 -1
  94. package/helpers/ver/4.25.10.ts +36 -0
  95. package/helpers/ver/4.25.3.ts +2 -1
  96. package/helpers/ver/4.25.4.ts +2 -1
  97. package/helpers/ver/4.25.6.ts +2 -1
  98. package/helpers/ver/4.25.7.ts +2 -1
  99. package/helpers/ver/4.25.8.ts +62 -0
  100. package/helpers/ver/4.25.9.ts +293 -0
  101. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  102. package/helpers/ver/tests/4.25.8.test.ts +86 -0
  103. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useColorPalette.ts +79 -0
  106. package/package.json +12 -4
  107. package/styles/_button-section.scss +0 -2
  108. package/styles/_global.scss +7 -5
  109. package/styles/base.scss +8 -5
  110. package/styles/v2/components/button.scss +4 -3
  111. package/styles/v2/components/editor.scss +2 -1
  112. package/styles/v2/layout/_data-table.scss +3 -2
  113. package/styles/v2/themes/_color-definitions.scss +18 -17
  114. package/testBuild.js +0 -0
  115. package/testing-setup.js +32 -0
  116. package/types/ForecastingSeriesKey.ts +0 -1
  117. package/types/MarkupInclude.ts +6 -1
  118. package/types/MarkupVariable.ts +19 -0
  119. package/types/Series.ts +4 -0
  120. package/types/Table.ts +1 -0
  121. package/types/VizFilter.ts +1 -0
  122. package/vitest.config.ts +16 -0
  123. package/components/ui/_stories/Colors.stories.mdx +0 -220
  124. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  125. package/data/colorPalettes.js +0 -171
  126. package/helpers/events.js +0 -14
  127. package/helpers/formatConfigBeforeSave.ts +0 -135
  128. 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.warn(error, errorInfo)
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
- getUniqueValues?: Function
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
- getUniqueValues
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 handleReset = e => {
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
- {BUTTON_TEXT.apply}
296
+ Apply
273
297
  </Button>
274
- <Button secondary disabled={initialFiltersActive} onClick={handleReset}>
275
- {BUTTON_TEXT.resetText}
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
- const configData = filterVizData(filters, config.data)
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(baseSvg, {
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
- // Handles different state theme locations between components
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 href={dataConfig.dataFileName} title={buttonText.link} target='_blank'>
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 href={dataConfig.dataUrl} title='Link to view full data set' target='_blank'>
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