@cdc/core 4.25.8 → 4.25.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  2. package/components/AdvancedEditor/AdvancedEditor.tsx +32 -9
  3. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  4. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  5. package/components/CustomColorsEditor/index.ts +1 -0
  6. package/components/DataTable/DataTable.tsx +56 -38
  7. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  8. package/components/DataTable/components/ChartHeader.tsx +44 -14
  9. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  10. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  11. package/components/DataTable/components/MapHeader.tsx +24 -13
  12. package/components/DataTable/data-table.css +12 -0
  13. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  14. package/components/DataTable/helpers/mapCellMatrix.tsx +33 -4
  15. package/components/DataTable/helpers/standardizeState.js +2 -2
  16. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  17. package/components/DownloadButton.tsx +40 -14
  18. package/components/EditorPanel/DataTableEditor.tsx +3 -3
  19. package/components/EditorPanel/EditorPanel.styles.css +423 -0
  20. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  21. package/components/EditorPanel/Inputs.tsx +12 -2
  22. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  23. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +12 -2
  24. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  25. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +450 -0
  26. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  27. package/components/ErrorBoundary.jsx +3 -1
  28. package/components/Filters/Filters.tsx +52 -24
  29. package/components/Filters/components/Dropdown.tsx +6 -1
  30. package/components/Filters/components/Tabs.tsx +1 -0
  31. package/components/Footnotes/Footnotes.tsx +35 -25
  32. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  33. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  34. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  35. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  36. package/components/HeaderThemeSelector/index.ts +2 -0
  37. package/components/Layout/styles/editor.scss +2 -1
  38. package/components/Legend/Legend.Gradient.tsx +3 -6
  39. package/components/LegendShape.tsx +121 -3
  40. package/components/Loader/Loader.tsx +1 -1
  41. package/components/MediaControls.tsx +72 -21
  42. package/components/PaletteConversionModal.tsx +90 -0
  43. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  44. package/components/PaletteSelector/PaletteSelector.css +94 -0
  45. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  46. package/components/PaletteSelector/index.ts +2 -0
  47. package/components/RichTooltip/RichTooltip.tsx +1 -0
  48. package/components/Table/Table.tsx +3 -1
  49. package/components/Table/components/Cell.tsx +23 -2
  50. package/components/Table/components/Row.tsx +5 -3
  51. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  52. package/components/_stories/DataTable.stories.tsx +1 -1
  53. package/components/_stories/Filters.stories.tsx +21 -2
  54. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  55. package/components/_stories/Footnotes.stories.tsx +769 -4
  56. package/components/_stories/Inputs.stories.tsx +3 -3
  57. package/components/_stories/MultiSelect.stories.tsx +3 -3
  58. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  59. package/components/_stories/Table.stories.tsx +1 -1
  60. package/components/_stories/styles.scss +0 -1
  61. package/components/elements/_stories/Button.stories.tsx +1 -1
  62. package/components/elements/_stories/Card.stories.tsx +1 -1
  63. package/components/inputs/InputToggle.tsx +2 -0
  64. package/components/managers/DataDesigner.tsx +10 -9
  65. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  66. package/components/ui/Accordion.jsx +1 -1
  67. package/components/ui/Tooltip.tsx +2 -1
  68. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  69. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  70. package/components/ui/_stories/Colors.stories.tsx +330 -0
  71. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  72. package/components/ui/_stories/Title.stories.tsx +1 -1
  73. package/components/ui/accordion.styles.css +57 -0
  74. package/contexts/EditorContext.ts +18 -0
  75. package/contexts/editor.actions.ts +28 -0
  76. package/contexts/editor.reducer.ts +94 -0
  77. package/data/chartColorPalettes.ts +118 -0
  78. package/data/colorPalettes.ts +9 -0
  79. package/data/mapColorPalettes.ts +45 -0
  80. package/data/sharedPalettes.ts +50 -0
  81. package/dist/cove-main.css +63 -14
  82. package/dist/cove-main.css.map +1 -1
  83. package/generateViteConfig.js +80 -0
  84. package/helpers/addValuesToFilters.ts +7 -3
  85. package/helpers/cloneConfig.ts +31 -0
  86. package/helpers/configDataHelpers.ts +128 -0
  87. package/helpers/configHelpers.ts +27 -0
  88. package/helpers/constants.ts +42 -2
  89. package/helpers/cove/number.ts +33 -12
  90. package/helpers/coveUpdateWorker.ts +15 -3
  91. package/helpers/fetchRemoteData.ts +3 -15
  92. package/helpers/filterColorPalettes.ts +152 -0
  93. package/helpers/generateColorsArray.ts +13 -0
  94. package/helpers/getColorPaletteVersion.ts +33 -0
  95. package/helpers/getPaletteAccessor.ts +18 -0
  96. package/helpers/markupProcessor.ts +220 -0
  97. package/helpers/mergeCustomOrderValues.ts +37 -0
  98. package/helpers/metrics/helpers.ts +42 -19
  99. package/helpers/metrics/types.ts +48 -9
  100. package/helpers/metrics/utils.ts +34 -0
  101. package/helpers/palettes/colorDistributions.ts +56 -0
  102. package/helpers/palettes/migratePaletteName.ts +150 -0
  103. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  104. package/helpers/palettes/utils.ts +267 -0
  105. package/helpers/parseCsvWithQuotes.ts +65 -0
  106. package/helpers/queryStringUtils.ts +13 -0
  107. package/helpers/testing.ts +358 -0
  108. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  109. package/helpers/tests/generateColorsArray.test.ts +24 -0
  110. package/helpers/tests/markupProcessor.test.ts +538 -0
  111. package/helpers/tests/testStandaloneBuild.ts +44 -0
  112. package/helpers/useMarkupVariables.ts +31 -0
  113. package/helpers/vegaConfig.ts +0 -1
  114. package/helpers/ver/4.24.10.ts +2 -1
  115. package/helpers/ver/4.24.11.ts +2 -1
  116. package/helpers/ver/4.24.3.ts +2 -1
  117. package/helpers/ver/4.24.4.ts +2 -1
  118. package/helpers/ver/4.24.5.ts +2 -1
  119. package/helpers/ver/4.24.7.ts +2 -1
  120. package/helpers/ver/4.24.9.ts +2 -1
  121. package/helpers/ver/4.25.1.ts +2 -1
  122. package/helpers/ver/4.25.10.ts +36 -0
  123. package/helpers/ver/4.25.11.ts +13 -0
  124. package/helpers/ver/4.25.3.ts +2 -1
  125. package/helpers/ver/4.25.4.ts +2 -1
  126. package/helpers/ver/4.25.6.ts +2 -1
  127. package/helpers/ver/4.25.7.ts +2 -1
  128. package/helpers/ver/4.25.8.ts +2 -1
  129. package/helpers/ver/4.25.9.ts +293 -0
  130. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  131. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  132. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  133. package/helpers/viewports.ts +2 -0
  134. package/hooks/useColorPalette.ts +79 -0
  135. package/package.json +13 -4
  136. package/styles/_common-components.css +73 -0
  137. package/styles/_global.scss +32 -10
  138. package/styles/base.scss +8 -55
  139. package/styles/cove-main.scss +3 -1
  140. package/styles/filters.scss +10 -3
  141. package/styles/v2/base/index.scss +0 -1
  142. package/styles/v2/components/button.scss +4 -3
  143. package/styles/v2/components/editor.scss +16 -7
  144. package/styles/v2/layout/_data-table.scss +3 -2
  145. package/styles/v2/themes/_color-definitions.scss +18 -17
  146. package/styles/v2/utils/_breakpoints.scss +1 -1
  147. package/styles/v2/utils/index.scss +0 -1
  148. package/styles/waiting.scss +1 -1
  149. package/testing-setup.js +32 -0
  150. package/types/MarkupInclude.ts +8 -2
  151. package/types/MarkupVariable.ts +19 -0
  152. package/types/VizFilter.ts +2 -0
  153. package/vitest.config.ts +16 -0
  154. package/components/ui/_stories/Colors.stories.mdx +0 -220
  155. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  156. package/data/colorPalettes.js +0 -171
  157. package/helpers/formatConfigBeforeSave.ts +0 -135
  158. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
  159. package/styles/_mixins.scss +0 -13
  160. package/styles/v2/base/_typography.scss +0 -0
  161. package/styles/v2/components/guidance-block.scss +0 -74
  162. package/styles/v2/utils/_functions.scss +0 -0
  163. /package/{styles/_typography.scss → testBuild.js} +0 -0
@@ -0,0 +1,450 @@
1
+ import React, { useState, useMemo, useCallback } from 'react'
2
+ import { MarkupVariable, MarkupCondition } from '../../../types/MarkupVariable'
3
+ import Button from '../../elements/Button'
4
+ import { TextField, Select, CheckBox } from '../Inputs'
5
+ import Icon from '../../ui/Icon'
6
+ import Accordion from '../../ui/Accordion'
7
+ import { Datasets } from '../../../types/DataSet'
8
+ import _ from 'lodash'
9
+
10
+ type MarkupVariablesEditorProps = {
11
+ /** Array of markup variable configurations */
12
+ markupVariables: MarkupVariable[]
13
+ /** Dataset to extract column names and values from (for backward compatibility) */
14
+ data?: any[]
15
+ /** Available datasets for multi-dataset support */
16
+ datasets?: Datasets
17
+ /** Configuration object containing dataKey for dataset assignment */
18
+ config?: { dataKey?: string }
19
+ /** Callback when variables are added, updated, or removed */
20
+ onChange: (variables: MarkupVariable[]) => void
21
+ /** Whether markup variables feature is enabled */
22
+ enableMarkupVariables?: boolean
23
+ /** Callback when enable/disable toggle changes */
24
+ onToggleEnable?: (enabled: boolean) => void
25
+ }
26
+
27
+ export type { MarkupVariablesEditorProps }
28
+
29
+ /**
30
+ * Editor for creating and managing markup variables with {{variable-name}} syntax.
31
+ * Supports conditional filters, number formatting, and auto-generated tags.
32
+ */
33
+ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
34
+ markupVariables = [],
35
+ data = [],
36
+ datasets,
37
+ config,
38
+ onChange,
39
+ enableMarkupVariables = false,
40
+ onToggleEnable
41
+ }) => {
42
+ const [editingIndex, setEditingIndex] = useState<number | null>(null)
43
+ const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
44
+
45
+ // Ensure we always have a valid array (memoized with deep equality to prevent unnecessary re-renders)
46
+ const safeMarkupVariables = useMemo(() => markupVariables || [], [JSON.stringify(markupVariables)])
47
+
48
+ // Get the target dataset with fallback logic (memoized for performance)
49
+ const getTargetData = useCallback((): any[] => {
50
+ // First try to use the data prop
51
+ if (data && data.length > 0) {
52
+ return data
53
+ }
54
+
55
+ // Fallback to assigned dataset using config.dataKey
56
+ if (datasets && config?.dataKey) {
57
+ const assignedDataset = datasets[config.dataKey]
58
+ if (assignedDataset?.data?.length > 0) {
59
+ return assignedDataset.data
60
+ }
61
+ }
62
+
63
+ return []
64
+ }, [data, datasets, config?.dataKey])
65
+
66
+ // Get columns from the available data (memoized for performance)
67
+ const getAvailableColumns = useMemo((): string[] => {
68
+ const targetData = getTargetData()
69
+ return targetData.length > 0 ? Object.keys(targetData[0]) : []
70
+ }, [getTargetData])
71
+
72
+ // Get column values for a specific column (memoized for performance)
73
+ const getColumnValues = useCallback((columnName: string): string[] => {
74
+ const targetData = getTargetData()
75
+ if (targetData.length === 0) return []
76
+
77
+ const uniqueValues = new Set<string>()
78
+ targetData.forEach(row => {
79
+ if (row[columnName] !== undefined && row[columnName] !== null) {
80
+ uniqueValues.add(String(row[columnName]))
81
+ }
82
+ })
83
+ return Array.from(uniqueValues).sort()
84
+ }, [data, datasets, config?.dataKey])
85
+
86
+ // Validate a variable and return array of error messages
87
+ const validateVariable = React.useCallback((variable: MarkupVariable): string[] => {
88
+ const errors: string[] = []
89
+ if (!variable.name || variable.name.trim() === '') {
90
+ errors.push('Variable name is required')
91
+ }
92
+ if (!variable.tag || variable.tag.trim() === '') {
93
+ errors.push('Variable tag is required')
94
+ }
95
+ if (!variable.columnName || variable.columnName.trim() === '') {
96
+ errors.push('Data column is required')
97
+ }
98
+ // Validate conditions
99
+ variable.conditions?.forEach((condition, index) => {
100
+ if (!condition.columnName) {
101
+ errors.push(`Condition ${index + 1}: Column is required`)
102
+ }
103
+ if (!condition.value) {
104
+ errors.push(`Condition ${index + 1}: Value is required`)
105
+ }
106
+ })
107
+ return errors
108
+ }, [])
109
+
110
+ // Validate all variables on mount and when variables change
111
+ React.useEffect(() => {
112
+ const errors: Record<number, string[]> = {}
113
+ safeMarkupVariables.forEach((variable, index) => {
114
+ const variableErrors = validateVariable(variable)
115
+ if (variableErrors.length > 0) {
116
+ errors[index] = variableErrors
117
+ }
118
+ })
119
+
120
+ // Only update if errors have actually changed (use deep equality)
121
+ setValidationErrors(prev => {
122
+ const errorsChanged = !_.isEqual(prev, errors)
123
+ return errorsChanged ? errors : prev
124
+ })
125
+ }, [safeMarkupVariables, validateVariable]) // Re-validate when variables change
126
+
127
+
128
+ const addVariable = () => {
129
+ const newVariable: MarkupVariable = {
130
+ name: '',
131
+ tag: '',
132
+ columnName: '',
133
+ conditions: [],
134
+ addCommas: false,
135
+ hideOnNull: false
136
+ }
137
+ const newVariables = [...safeMarkupVariables, newVariable]
138
+ onChange(newVariables)
139
+ const newIndex = safeMarkupVariables.length
140
+ setEditingIndex(newIndex)
141
+
142
+ // Immediately show validation errors for the new empty variable
143
+ const errors = validateVariable(newVariable)
144
+ setValidationErrors(prev => ({
145
+ ...prev,
146
+ [newIndex]: errors
147
+ }))
148
+ }
149
+
150
+ const updateVariable = (index: number, updates: Partial<MarkupVariable>) => {
151
+ const updated = safeMarkupVariables.map((variable, i) =>
152
+ i === index ? { ...variable, conditions: variable.conditions || [], ...updates } : variable
153
+ )
154
+ onChange(updated)
155
+
156
+ // Validate and update errors for this variable
157
+ const errors = validateVariable(updated[index])
158
+ setValidationErrors(prev => ({
159
+ ...prev,
160
+ [index]: errors
161
+ }))
162
+ }
163
+
164
+ const removeVariable = (index: number) => {
165
+ const updated = safeMarkupVariables.filter((_, i) => i !== index)
166
+ onChange(updated)
167
+ if (editingIndex === index) {
168
+ setEditingIndex(null)
169
+ }
170
+ }
171
+
172
+ const generateTag = (name: string) => {
173
+ if (!name) return ''
174
+ // Convert name to tag format: "My Variable" -> "{{my-variable}}"
175
+ return `{{${name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}}}`
176
+ }
177
+
178
+
179
+
180
+ const addCondition = (variableIndex: number) => {
181
+ const variable = safeMarkupVariables[variableIndex]
182
+ const newCondition: MarkupCondition = {
183
+ columnName: '',
184
+ isOrIsNotEqualTo: 'is',
185
+ value: ''
186
+ }
187
+ const updatedConditions = [...(variable.conditions || []), newCondition]
188
+ updateVariable(variableIndex, { conditions: updatedConditions })
189
+ }
190
+
191
+ const updateCondition = (variableIndex: number, conditionIndex: number, updates: Partial<MarkupCondition>) => {
192
+ const variable = safeMarkupVariables[variableIndex]
193
+ const conditions = variable.conditions || []
194
+ const updatedConditions = conditions.map((condition, i) =>
195
+ i === conditionIndex ? { ...condition, ...updates } : condition
196
+ )
197
+ updateVariable(variableIndex, { conditions: updatedConditions })
198
+ }
199
+
200
+ const removeCondition = (variableIndex: number, conditionIndex: number) => {
201
+ const variable = safeMarkupVariables[variableIndex]
202
+ const updatedConditions = variable.conditions.filter((_, i) => i !== conditionIndex)
203
+ updateVariable(variableIndex, { conditions: updatedConditions })
204
+ }
205
+
206
+ return (
207
+ <div className='markup-variables-editor'>
208
+ <div className='mb-3'>
209
+ <CheckBox
210
+ value={enableMarkupVariables}
211
+ fieldName='enableMarkupVariables'
212
+ label='Enable Markup Variables'
213
+ updateField={(_section, _subsection, _fieldName, value) => {
214
+ if (onToggleEnable) {
215
+ onToggleEnable(value)
216
+ }
217
+ }}
218
+ />
219
+ </div>
220
+
221
+ {enableMarkupVariables && (
222
+ <>
223
+ <div className='mb-3'>
224
+ <p className='text-sm text-gray-600'>
225
+ Use variables in your content with <code>{'{{variable-name}}'}</code> syntax.
226
+ Variables will be replaced with values from your data.
227
+ </p>
228
+ </div>
229
+
230
+ {safeMarkupVariables.length > 0 && (
231
+ <div className='variables-list mb-3'>
232
+ {safeMarkupVariables.map((variable, index) => variable ? (
233
+ <div key={index} className='variable-item p-3 border rounded mb-2' style={{ backgroundColor: '#fff' }}>
234
+ <div className='d-flex justify-content-between align-items-start mb-2'>
235
+ <div style={{ flex: 1 }}>
236
+ <div style={{ fontSize: '16px', fontWeight: 600, marginBottom: '4px' }}>
237
+ {variable.name || 'Unnamed Variable'}
238
+ </div>
239
+ <div style={{ fontSize: '13px', color: '#6c757d', fontFamily: 'monospace', marginBottom: '4px' }}>
240
+ {variable.tag}
241
+ </div>
242
+ <div style={{ fontSize: '13px', color: '#6c757d' }}>
243
+ Column: <strong>{variable.columnName || 'Not selected'}</strong>
244
+ {variable.conditions && variable.conditions.length > 0 && (
245
+ <span> • {variable.conditions.length} condition{variable.conditions.length !== 1 ? 's' : ''}</span>
246
+ )}
247
+ </div>
248
+ {validationErrors[index] && validationErrors[index].length > 0 && (
249
+ <div style={{ fontSize: '12px', color: '#dc3545', marginTop: '8px' }}>
250
+ <strong>⚠ Validation Errors:</strong>
251
+ <ul style={{ margin: '4px 0 0 0', paddingLeft: '20px' }}>
252
+ {validationErrors[index].map((error, errorIndex) => (
253
+ <li key={errorIndex}>{error}</li>
254
+ ))}
255
+ </ul>
256
+ </div>
257
+ )}
258
+ </div>
259
+ <div className='d-flex gap-2'>
260
+ <Button
261
+ className='btn-sm'
262
+ onClick={() => setEditingIndex(editingIndex === index ? null : index)}
263
+ >
264
+ {editingIndex === index ? 'Close' : 'Edit'}
265
+ </Button>
266
+ </div>
267
+ </div>
268
+
269
+ {editingIndex === index && (
270
+ <div className='mt-3 pt-3 border-t'>
271
+ <Accordion>
272
+ <Accordion.Section title='Basic Settings'>
273
+ <div className='mb-3'>
274
+ <TextField
275
+ value={variable.name}
276
+ fieldName='name'
277
+ label='Variable Name'
278
+ placeholder='e.g., "State Name"'
279
+ updateField={(section, subsection, fieldName, value) => {
280
+ updateVariable(index, {
281
+ name: value,
282
+ tag: generateTag(value)
283
+ })
284
+ }}
285
+ />
286
+ </div>
287
+ <div className='mb-3'>
288
+ <label>
289
+ <span className='edit-label column-heading'>Tag (auto-generated)</span>
290
+ <input
291
+ type='text'
292
+ value={variable.tag}
293
+ placeholder='{{variable-name}}'
294
+ readOnly
295
+ style={{ backgroundColor: '#e9ecef', cursor: 'not-allowed' }}
296
+ />
297
+ </label>
298
+ </div>
299
+
300
+ <div className='mb-3'>
301
+ <Select
302
+ value={variable.columnName}
303
+ fieldName='columnName'
304
+ label='Data Column'
305
+ options={[
306
+ { value: '', label: 'Select Column...' },
307
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
308
+ ]}
309
+ updateField={(_section, _subsection, _fieldName, value) => {
310
+ updateVariable(index, { columnName: value })
311
+ }}
312
+ />
313
+ </div>
314
+ </Accordion.Section>
315
+
316
+ <Accordion.Section title='Conditions'>
317
+ <div className='text-sm text-gray-500 mb-2'>
318
+ Add conditions to filter when this variable should display data
319
+ </div>
320
+
321
+ {variable.conditions && variable.conditions.length > 0 && (
322
+ <div className='conditions-list mb-2'>
323
+ {variable.conditions.map((condition, conditionIndex) => (
324
+ <div key={`condition-${index}-${conditionIndex}`} className='condition-item p-2 border rounded mb-2' style={{ backgroundColor: '#f8f9fa' }}>
325
+ <div className='mb-2'>
326
+ <Select
327
+ value={condition.columnName || ''}
328
+ fieldName={`condition-column-${index}-${conditionIndex}`}
329
+ label='Column'
330
+ options={[
331
+ { value: '', label: 'Select Column...' },
332
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
333
+ ]}
334
+ updateField={(_section, _subsection, _fieldName, newColumnName) => {
335
+ // Reset value when column changes
336
+ updateCondition(index, conditionIndex, {
337
+ columnName: newColumnName,
338
+ value: ''
339
+ })
340
+ }}
341
+ />
342
+ </div>
343
+ <div className='mb-2'>
344
+ <Select
345
+ value={condition.isOrIsNotEqualTo || 'is'}
346
+ fieldName={`condition-operator-${index}-${conditionIndex}`}
347
+ label='Operator'
348
+ options={[
349
+ { value: 'is', label: 'is' },
350
+ { value: 'is not', label: 'is not' }
351
+ ]}
352
+ updateField={(_section, _subsection, _fieldName, value) => {
353
+ updateCondition(index, conditionIndex, { isOrIsNotEqualTo: value as 'is' | 'is not' })
354
+ }}
355
+ />
356
+ </div>
357
+ <div className='mb-2'>
358
+ <Select
359
+ value={condition.value || ''}
360
+ fieldName={`condition-value-${index}-${conditionIndex}`}
361
+ label='Value'
362
+ options={[
363
+ { value: '', label: 'Select Value...' },
364
+ ...(condition.columnName
365
+ ? getColumnValues(condition.columnName).map(val => ({
366
+ value: String(val),
367
+ label: String(val)
368
+ }))
369
+ : [])
370
+ ]}
371
+ updateField={(_section, _subsection, _fieldName, value) => {
372
+ updateCondition(index, conditionIndex, { value })
373
+ }}
374
+ />
375
+ </div>
376
+ <Button
377
+ className='btn-sm btn-danger'
378
+ onClick={() => removeCondition(index, conditionIndex)}
379
+ >
380
+ Remove Condition
381
+ </Button>
382
+ </div>
383
+ ))}
384
+ </div>
385
+ )}
386
+
387
+ <Button
388
+ className='btn-sm'
389
+ onClick={() => addCondition(index)}
390
+ >
391
+ <Icon display='plus' size={14} className='mr-1' />
392
+ Add Condition
393
+ </Button>
394
+ </Accordion.Section>
395
+
396
+ <Accordion.Section title='Formatting Options'>
397
+ <div className='mb-3'>
398
+ <CheckBox
399
+ value={variable.addCommas || false}
400
+ fieldName='addCommas'
401
+ label='Format numbers with commas'
402
+ updateField={(_section, _subsection, _fieldName, value) =>
403
+ updateVariable(index, { addCommas: value })
404
+ }
405
+ />
406
+ </div>
407
+
408
+ <div className='mb-3'>
409
+ <CheckBox
410
+ value={variable.hideOnNull || false}
411
+ fieldName='hideOnNull'
412
+ label='Hide section when value is null'
413
+ updateField={(_section, _subsection, _fieldName, value) =>
414
+ updateVariable(index, { hideOnNull: value })
415
+ }
416
+ />
417
+ </div>
418
+ </Accordion.Section>
419
+ </Accordion>
420
+
421
+ <div className='mt-3 pt-3 border-t' style={{ textAlign: 'center' }}>
422
+ <Button
423
+ className='btn-sm btn-danger'
424
+ onClick={() => {
425
+ if (window.confirm(`Are you sure you want to delete the variable "${variable.name || 'Unnamed Variable'}"?`)) {
426
+ removeVariable(index)
427
+ }
428
+ }}
429
+ >
430
+ Delete Variable
431
+ </Button>
432
+ </div>
433
+ </div>
434
+ )}
435
+ </div>
436
+ ) : <></>)}
437
+ </div>
438
+ )}
439
+
440
+ <Button className='btn-primary' onClick={addVariable}>
441
+ <Icon display='plus' size={16} className='mr-2' />
442
+ Add Variable
443
+ </Button>
444
+ </>
445
+ )}
446
+ </div>
447
+ )
448
+ }
449
+
450
+ export default MarkupVariablesEditor
@@ -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
 
@@ -135,10 +141,31 @@ const Filters: React.FC<FilterProps> = ({
135
141
  filter.values = getUniqueValues(visualizationConfig.data, filter.columnName)
136
142
  }
137
143
 
138
- newFilters[i].active = handleSorting(filter).values[0]
144
+ // Determine reset value based on filter configuration
145
+ let resetValue
146
+
147
+ // If filter has a resetLabel, reset to empty state (shows "- Select -")
148
+ if (filter.resetLabel) {
149
+ resetValue = filter.resetLabel
150
+ }
151
+ // If filter has a defaultValue, use that
152
+ else if (filter.defaultValue) {
153
+ resetValue = filter.defaultValue
154
+ }
155
+ // Otherwise, use first value in sorted values array
156
+ else if (filter.values && filter.values.length > 0) {
157
+ resetValue = handleSorting(filter).values[0]
158
+ }
159
+
160
+ // Handle multi-select filters
161
+ if (filter.filterStyle === 'multi-select') {
162
+ newFilters[i].active = resetValue ? [resetValue] : []
163
+ } else {
164
+ newFilters[i].active = resetValue
165
+ }
139
166
 
140
- if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
141
- queryParams[filter.setByQueryParameter] = filter.active
167
+ if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== newFilters[i].active) {
168
+ queryParams[filter.setByQueryParameter] = newFilters[i].active
142
169
  needsQueryUpdate = true
143
170
  }
144
171
  })
@@ -148,12 +175,13 @@ const Filters: React.FC<FilterProps> = ({
148
175
  }
149
176
 
150
177
  setFilters(newFilters)
151
- publishAnalyticsEvent(
152
- `${visualizationConfig.type}_filter_reset`,
153
- 'click',
154
- `${interactionLabel}`,
155
- visualizationConfig.type
156
- )
178
+ publishAnalyticsEvent({
179
+ vizType: visualizationConfig.type as any,
180
+ eventType: `${visualizationConfig.type}_filter_reset` as any,
181
+ eventAction: 'click',
182
+ eventLabel: interactionLabel,
183
+ vizTitle: visualizationConfig?.title
184
+ })
157
185
  }
158
186
 
159
187
  const mobileFilterStyle = useMemo(() => {
@@ -288,9 +316,9 @@ const Filters: React.FC<FilterProps> = ({
288
316
  >
289
317
  Apply
290
318
  </Button>
291
- <Button secondary disabled={initialFiltersActive} onClick={handleReset}>
319
+ <button className='btn btn-link' disabled={initialFiltersActive} onClick={handleFiltersReset}>
292
320
  Clear Filters
293
- </Button>
321
+ </button>
294
322
  </div>
295
323
  ) : (
296
324
  <></>
@@ -10,7 +10,7 @@ type DropdownProps = {
10
10
  }
11
11
 
12
12
  const Dropdown: React.FC<DropdownProps> = ({ index: outerIndex, label, filter, changeFilterActive }) => {
13
- const { active, queuedActive } = filter
13
+ const { active, queuedActive, resetLabel } = filter
14
14
 
15
15
  return (
16
16
  <select
@@ -25,6 +25,11 @@ const Dropdown: React.FC<DropdownProps> = ({ index: outerIndex, label, filter, c
25
25
  changeFilterActive(outerIndex, e.target.value)
26
26
  }}
27
27
  >
28
+ {resetLabel && (
29
+ <option key='reset-option' value={resetLabel} aria-label={resetLabel}>
30
+ {resetLabel}
31
+ </option>
32
+ )}
28
33
  {filter.values?.map((value, index) => {
29
34
  return (
30
35
  <option key={index} value={value} aria-label={value}>
@@ -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 => {