@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.
Files changed (117) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +56 -38
  3. package/components/DataTable/components/ChartHeader.tsx +44 -14
  4. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  5. package/components/DataTable/components/MapHeader.tsx +24 -13
  6. package/components/DataTable/data-table.css +6 -0
  7. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  8. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  9. package/components/DownloadButton.tsx +40 -14
  10. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  11. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  12. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  13. package/components/ErrorBoundary.jsx +3 -1
  14. package/components/Filters/Filters.tsx +27 -20
  15. package/components/Filters/components/Tabs.tsx +1 -0
  16. package/components/Legend/Legend.Gradient.tsx +3 -6
  17. package/components/LegendShape.tsx +121 -3
  18. package/components/MediaControls.tsx +51 -3
  19. package/components/PaletteConversionModal.tsx +87 -0
  20. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  21. package/components/PaletteSelector/PaletteSelector.css +51 -0
  22. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  23. package/components/PaletteSelector/index.ts +2 -0
  24. package/components/RichTooltip/RichTooltip.tsx +1 -0
  25. package/components/Table/Table.tsx +3 -1
  26. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  27. package/components/_stories/DataTable.stories.tsx +1 -1
  28. package/components/_stories/Filters.stories.tsx +1 -1
  29. package/components/_stories/Footnotes.stories.tsx +1 -1
  30. package/components/_stories/Inputs.stories.tsx +1 -1
  31. package/components/_stories/MultiSelect.stories.tsx +3 -3
  32. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  33. package/components/_stories/Table.stories.tsx +1 -1
  34. package/components/elements/_stories/Button.stories.tsx +1 -1
  35. package/components/elements/_stories/Card.stories.tsx +1 -1
  36. package/components/inputs/InputToggle.tsx +2 -0
  37. package/components/managers/DataDesigner.tsx +10 -9
  38. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  39. package/components/ui/Tooltip.tsx +2 -1
  40. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  41. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  42. package/components/ui/_stories/Colors.stories.tsx +330 -0
  43. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  44. package/components/ui/_stories/Title.stories.tsx +1 -1
  45. package/contexts/EditorContext.ts +18 -0
  46. package/contexts/editor.actions.ts +28 -0
  47. package/contexts/editor.reducer.ts +94 -0
  48. package/data/chartColorPalettes.ts +118 -0
  49. package/data/colorPalettes.ts +9 -0
  50. package/data/mapColorPalettes.ts +45 -0
  51. package/data/sharedPalettes.ts +50 -0
  52. package/dist/cove-main.css +14 -11
  53. package/dist/cove-main.css.map +1 -1
  54. package/generateViteConfig.js +80 -0
  55. package/helpers/addValuesToFilters.ts +2 -3
  56. package/helpers/cloneConfig.ts +31 -0
  57. package/helpers/configDataHelpers.ts +128 -0
  58. package/helpers/configHelpers.ts +27 -0
  59. package/helpers/constants.ts +5 -2
  60. package/helpers/coveUpdateWorker.ts +13 -3
  61. package/helpers/filterColorPalettes.ts +152 -0
  62. package/helpers/generateColorsArray.ts +13 -0
  63. package/helpers/getColorPaletteVersion.ts +33 -0
  64. package/helpers/getPaletteAccessor.ts +18 -0
  65. package/helpers/markupProcessor.ts +205 -0
  66. package/helpers/metrics/helpers.ts +42 -19
  67. package/helpers/metrics/types.ts +48 -9
  68. package/helpers/metrics/utils.ts +34 -0
  69. package/helpers/palettes/colorDistributions.ts +56 -0
  70. package/helpers/palettes/migratePaletteName.ts +150 -0
  71. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  72. package/helpers/palettes/utils.ts +267 -0
  73. package/helpers/queryStringUtils.ts +13 -0
  74. package/helpers/testing.ts +345 -0
  75. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  76. package/helpers/tests/generateColorsArray.test.ts +24 -0
  77. package/helpers/tests/markupProcessor.test.ts +538 -0
  78. package/helpers/tests/testStandaloneBuild.ts +44 -0
  79. package/helpers/useMarkupVariables.ts +31 -0
  80. package/helpers/vegaConfig.ts +0 -1
  81. package/helpers/ver/4.24.10.ts +2 -1
  82. package/helpers/ver/4.24.11.ts +2 -1
  83. package/helpers/ver/4.24.3.ts +2 -1
  84. package/helpers/ver/4.24.4.ts +2 -1
  85. package/helpers/ver/4.24.5.ts +2 -1
  86. package/helpers/ver/4.24.7.ts +2 -1
  87. package/helpers/ver/4.24.9.ts +2 -1
  88. package/helpers/ver/4.25.1.ts +2 -1
  89. package/helpers/ver/4.25.10.ts +36 -0
  90. package/helpers/ver/4.25.3.ts +2 -1
  91. package/helpers/ver/4.25.4.ts +2 -1
  92. package/helpers/ver/4.25.6.ts +2 -1
  93. package/helpers/ver/4.25.7.ts +2 -1
  94. package/helpers/ver/4.25.8.ts +2 -1
  95. package/helpers/ver/4.25.9.ts +293 -0
  96. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  97. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  98. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  99. package/hooks/useColorPalette.ts +79 -0
  100. package/package.json +12 -4
  101. package/styles/_global.scss +7 -5
  102. package/styles/base.scss +8 -5
  103. package/styles/v2/components/button.scss +4 -3
  104. package/styles/v2/components/editor.scss +2 -1
  105. package/styles/v2/layout/_data-table.scss +3 -2
  106. package/styles/v2/themes/_color-definitions.scss +18 -17
  107. package/testBuild.js +0 -0
  108. package/testing-setup.js +32 -0
  109. package/types/MarkupInclude.ts +6 -1
  110. package/types/MarkupVariable.ts +19 -0
  111. package/types/VizFilter.ts +1 -0
  112. package/vitest.config.ts +16 -0
  113. package/components/ui/_stories/Colors.stories.mdx +0 -220
  114. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  115. package/data/colorPalettes.js +0 -171
  116. package/helpers/formatConfigBeforeSave.ts +0 -135
  117. 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() {
@@ -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
- `${visualizationConfig.type}_filter_changed`,
92
- 'click',
93
- `${interactionLabel}|key_${newFilters?.[index]?.columnName}|value_${newFilters?.[index]?.active}`,
94
- visualizationConfig.type
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
- `${visualizationConfig.type}_filter_applied`,
118
- 'click',
119
- `${interactionLabel}|${newFilters.map(f => f.active)}`,
120
- visualizationConfig.type
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 handleReset = e => {
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
- `${visualizationConfig.type}_filter_reset`,
153
- 'click',
154
- `${interactionLabel}`,
155
- visualizationConfig.type
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={handleReset}>
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(`${state.type}_image_downloaded`, 'click', interactionLabel, `${state.type}`)
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('data_viewed', 'click', `${unknown}`)
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('data_viewed', 'click', `${interactionLabel}`)
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