@cdc/core 4.25.10 → 4.26.1

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 (134) hide show
  1. package/_stories/Gallery.Charts.stories.tsx +307 -0
  2. package/_stories/Gallery.DataBite.stories.tsx +72 -0
  3. package/_stories/Gallery.Maps.stories.tsx +230 -0
  4. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  5. package/_stories/PageART.stories.tsx +192 -0
  6. package/_stories/PageBRFSS.stories.tsx +289 -0
  7. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  8. package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
  9. package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
  10. package/_stories/PageMaternalMortality.stories.tsx +192 -0
  11. package/_stories/PageOralHealth.stories.tsx +196 -0
  12. package/_stories/PageRespiratory.stories.tsx +332 -0
  13. package/_stories/PageSmokingTobacco.stories.tsx +195 -0
  14. package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
  15. package/_stories/PageWastewater.stories.tsx +463 -0
  16. package/_stories/StoryRenderingTests.stories.tsx +164 -0
  17. package/assets/icon-magnifying-glass.svg +5 -0
  18. package/assets/icon-warming-stripes.svg +13 -0
  19. package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
  20. package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
  21. package/components/ComboBox/ComboBox.tsx +345 -0
  22. package/components/ComboBox/combobox.styles.css +185 -0
  23. package/components/ComboBox/index.ts +1 -0
  24. package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
  25. package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
  26. package/components/CustomColorsEditor/index.ts +1 -0
  27. package/components/DataTable/DataTable.tsx +132 -58
  28. package/components/DataTable/DataTableStandAlone.tsx +8 -3
  29. package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
  30. package/components/DataTable/data-table.css +217 -210
  31. package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
  32. package/components/DataTable/helpers/standardizeState.js +2 -2
  33. package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
  34. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  35. package/components/EditorPanel/DataTableEditor.tsx +54 -28
  36. package/components/EditorPanel/EditorPanel.styles.css +439 -0
  37. package/components/EditorPanel/EditorPanel.tsx +144 -0
  38. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  39. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  40. package/components/EditorPanel/FootnotesEditor.tsx +44 -37
  41. package/components/EditorPanel/Inputs.tsx +44 -8
  42. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
  43. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
  44. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
  45. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  46. package/components/Filters/Filters.tsx +57 -10
  47. package/components/Filters/components/Dropdown.tsx +6 -1
  48. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  49. package/components/Filters/helpers/handleSorting.ts +1 -1
  50. package/components/Footnotes/Footnotes.tsx +35 -25
  51. package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
  52. package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
  53. package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
  54. package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
  55. package/components/HeaderThemeSelector/index.ts +2 -0
  56. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
  57. package/components/Layout/components/Visualization/index.tsx +16 -1
  58. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  59. package/components/Layout/styles/editor.scss +2 -1
  60. package/components/Legend/Legend.Gradient.tsx +1 -1
  61. package/components/Loader/Loader.tsx +1 -1
  62. package/components/MediaControls.tsx +63 -34
  63. package/components/PaletteConversionModal.tsx +7 -4
  64. package/components/PaletteSelector/PaletteSelector.css +49 -6
  65. package/components/Table/components/Cell.tsx +23 -2
  66. package/components/Table/components/Row.tsx +5 -3
  67. package/components/_stories/Filters.stories.tsx +20 -1
  68. package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
  69. package/components/_stories/Footnotes.stories.tsx +768 -3
  70. package/components/_stories/Inputs.stories.tsx +2 -2
  71. package/components/_stories/styles.scss +0 -1
  72. package/components/ui/Accordion.jsx +1 -1
  73. package/components/ui/Icon.tsx +3 -1
  74. package/components/ui/Title/index.tsx +30 -2
  75. package/components/ui/Title/title.styles.css +42 -0
  76. package/components/ui/accordion.styles.css +57 -0
  77. package/data/chartColorPalettes.ts +1 -1
  78. package/dist/cove-main.css +75 -6
  79. package/dist/cove-main.css.map +1 -1
  80. package/generateViteConfig.js +8 -1
  81. package/helpers/addValuesToFilters.ts +11 -1
  82. package/helpers/constants.ts +37 -0
  83. package/helpers/cove/number.ts +33 -12
  84. package/helpers/coveUpdateWorker.ts +20 -11
  85. package/helpers/embedCodeGenerator.ts +109 -0
  86. package/helpers/fetchRemoteData.ts +3 -15
  87. package/helpers/getUniqueValues.ts +19 -0
  88. package/helpers/hashObj.ts +25 -0
  89. package/helpers/isRightAlignedTableValue.js +5 -0
  90. package/helpers/markupProcessor.ts +27 -12
  91. package/helpers/mergeCustomOrderValues.ts +37 -0
  92. package/helpers/metrics/helpers.ts +1 -0
  93. package/helpers/parseCsvWithQuotes.ts +65 -0
  94. package/helpers/pivotData.ts +2 -2
  95. package/helpers/prepareScreenshot.ts +268 -0
  96. package/helpers/queryStringUtils.ts +29 -0
  97. package/helpers/testing.ts +17 -4
  98. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  99. package/helpers/tests/queryStringUtils.test.ts +381 -0
  100. package/helpers/tests/testStandaloneBuild.ts +23 -5
  101. package/helpers/useDataVizClasses.ts +0 -1
  102. package/helpers/ver/4.25.11.ts +13 -0
  103. package/helpers/ver/4.26.1.ts +80 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useDataColumns.ts +63 -0
  106. package/hooks/useFilterManagement.ts +94 -0
  107. package/hooks/useLegendSeparators.ts +26 -0
  108. package/hooks/useListManagement.ts +192 -0
  109. package/package.json +6 -4
  110. package/styles/_button-section.scss +0 -3
  111. package/styles/_common-components.css +73 -0
  112. package/styles/_global.scss +25 -5
  113. package/styles/base.scss +0 -50
  114. package/styles/cove-main.scss +3 -1
  115. package/styles/filters.scss +10 -3
  116. package/styles/v2/base/index.scss +0 -1
  117. package/styles/v2/components/editor.scss +14 -6
  118. package/styles/v2/utils/_breakpoints.scss +1 -1
  119. package/styles/v2/utils/index.scss +0 -1
  120. package/styles/waiting.scss +1 -1
  121. package/types/Axis.ts +1 -0
  122. package/types/ForecastingSeriesKey.ts +1 -0
  123. package/types/MarkupInclude.ts +5 -3
  124. package/types/MarkupVariable.ts +1 -1
  125. package/types/Series.ts +3 -0
  126. package/types/Table.ts +1 -0
  127. package/types/Visualization.ts +1 -0
  128. package/types/VizFilter.ts +2 -0
  129. package/LICENSE +0 -201
  130. package/styles/_mixins.scss +0 -13
  131. package/styles/_typography.scss +0 -0
  132. package/styles/v2/base/_typography.scss +0 -0
  133. package/styles/v2/components/guidance-block.scss +0 -74
  134. package/styles/v2/utils/_functions.scss +0 -0
@@ -1,15 +1,21 @@
1
- import React, { useState, useMemo } from 'react'
1
+ import React, { useState, useMemo, useCallback } from 'react'
2
2
  import { MarkupVariable, MarkupCondition } from '../../../types/MarkupVariable'
3
3
  import Button from '../../elements/Button'
4
4
  import { TextField, Select, CheckBox } from '../Inputs'
5
5
  import Icon from '../../ui/Icon'
6
6
  import Accordion from '../../ui/Accordion'
7
+ import { Datasets } from '../../../types/DataSet'
8
+ import _ from 'lodash'
7
9
 
8
10
  type MarkupVariablesEditorProps = {
9
11
  /** Array of markup variable configurations */
10
12
  markupVariables: MarkupVariable[]
11
- /** Dataset to extract column names and values from */
12
- data: any[]
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 }
13
19
  /** Callback when variables are added, updated, or removed */
14
20
  onChange: (variables: MarkupVariable[]) => void
15
21
  /** Whether markup variables feature is enabled */
@@ -27,6 +33,8 @@ export type { MarkupVariablesEditorProps }
27
33
  const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
28
34
  markupVariables = [],
29
35
  data = [],
36
+ datasets,
37
+ config,
30
38
  onChange,
31
39
  enableMarkupVariables = false,
32
40
  onToggleEnable
@@ -34,9 +42,46 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
34
42
  const [editingIndex, setEditingIndex] = useState<number | null>(null)
35
43
  const [validationErrors, setValidationErrors] = useState<Record<number, string[]>>({})
36
44
 
37
- // Ensure we always have a valid array
38
- const safeMarkupVariables = markupVariables || []
39
- const availableColumns = data.length > 0 ? Object.keys(data[0]) : []
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])
40
85
 
41
86
  // Validate a variable and return array of error messages
42
87
  const validateVariable = React.useCallback((variable: MarkupVariable): string[] => {
@@ -71,7 +116,12 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
71
116
  errors[index] = variableErrors
72
117
  }
73
118
  })
74
- setValidationErrors(errors)
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
+ })
75
125
  }, [safeMarkupVariables, validateVariable]) // Re-validate when variables change
76
126
 
77
127
 
@@ -125,18 +175,7 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
125
175
  return `{{${name.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')}}}`
126
176
  }
127
177
 
128
- // Get unique values for a given column for condition dropdowns
129
- const getColumnValues = useMemo(() => {
130
- if (!data || data.length === 0) return {}
131
178
 
132
- const columnValues: Record<string, (string | number)[]> = {}
133
- availableColumns.forEach(column => {
134
- const uniqueValues = Array.from(new Set(data.map(row => row[column])))
135
- .filter(val => val !== null && val !== undefined && val !== '')
136
- columnValues[column] = uniqueValues
137
- })
138
- return columnValues
139
- }, [data, availableColumns])
140
179
 
141
180
  const addCondition = (variableIndex: number) => {
142
181
  const variable = safeMarkupVariables[variableIndex]
@@ -265,7 +304,7 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
265
304
  label='Data Column'
266
305
  options={[
267
306
  { value: '', label: 'Select Column...' },
268
- ...availableColumns.map(col => ({ value: col, label: col }))
307
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
269
308
  ]}
270
309
  updateField={(_section, _subsection, _fieldName, value) => {
271
310
  updateVariable(index, { columnName: value })
@@ -290,7 +329,7 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
290
329
  label='Column'
291
330
  options={[
292
331
  { value: '', label: 'Select Column...' },
293
- ...availableColumns.map(col => ({ value: col, label: col }))
332
+ ...getAvailableColumns.map(col => ({ value: col, label: col }))
294
333
  ]}
295
334
  updateField={(_section, _subsection, _fieldName, newColumnName) => {
296
335
  // Reset value when column changes
@@ -322,8 +361,8 @@ const MarkupVariablesEditor: React.FC<MarkupVariablesEditorProps> = ({
322
361
  label='Value'
323
362
  options={[
324
363
  { value: '', label: 'Select Value...' },
325
- ...(condition.columnName && getColumnValues[condition.columnName]
326
- ? getColumnValues[condition.columnName].map(val => ({
364
+ ...(condition.columnName
365
+ ? getColumnValues(condition.columnName).map(val => ({
327
366
  value: String(val),
328
367
  label: String(val)
329
368
  }))
@@ -0,0 +1,169 @@
1
+ import { ReactNode } from 'react'
2
+ import { CheckBox } from '../Inputs'
3
+ import { HeaderThemeSelector } from '../../HeaderThemeSelector'
4
+ import { UpdateFieldFunc } from '../../../types/UpdateFieldFunc'
5
+
6
+ export interface VisualSectionConfig {
7
+ visual?: {
8
+ border?: boolean
9
+ borderColorTheme?: boolean
10
+ accent?: boolean
11
+ background?: boolean
12
+ hideBackgroundColor?: boolean
13
+ }
14
+ theme?: string
15
+ }
16
+
17
+ export interface VisualSectionProps<TConfig extends VisualSectionConfig = VisualSectionConfig> {
18
+ /** The visualization config object */
19
+ config: TConfig
20
+
21
+ /** Update function for individual fields */
22
+ updateField: UpdateFieldFunc<TConfig>
23
+
24
+ /** Update function for the entire config (used by HeaderThemeSelector) */
25
+ updateConfig: (config: TConfig) => void
26
+
27
+ /** Optional content to render before the standard checkboxes */
28
+ beforeCheckboxes?: ReactNode
29
+
30
+ /** Optional content to render after the standard checkboxes */
31
+ afterCheckboxes?: ReactNode
32
+
33
+ /** Position of HeaderThemeSelector. Defaults to 'before' */
34
+ themeSelectorPosition?: 'before' | 'after' | 'none'
35
+
36
+ /** Whether to show the border checkbox. Defaults to true */
37
+ showBorder?: boolean
38
+
39
+ /** Whether to show the borderColorTheme checkbox. Defaults to true */
40
+ showBorderColorTheme?: boolean
41
+
42
+ /** Whether to show the accent checkbox. Defaults to true */
43
+ showAccent?: boolean
44
+
45
+ /** Whether to show the background checkbox. Defaults to true */
46
+ showBackground?: boolean
47
+
48
+ /** Whether to show the hideBackgroundColor checkbox. Defaults to true */
49
+ showHideBackgroundColor?: boolean
50
+ }
51
+
52
+ /**
53
+ * Reusable Visual section component for EditorPanels
54
+ *
55
+ * Provides common visual configuration options including:
56
+ * - Theme selection
57
+ * - Border controls
58
+ * - Background and accent styling
59
+ *
60
+ * Note: Must be wrapped in an Accordion.Section when used
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * <Accordion.Section title='Visual'>
65
+ * <VisualSection
66
+ * config={config}
67
+ * updateField={updateField}
68
+ * updateConfig={updateConfig}
69
+ * beforeCheckboxes={
70
+ * <Select
71
+ * value={config.fontSize}
72
+ * fieldName='fontSize'
73
+ * label='Font Size'
74
+ * updateField={updateField}
75
+ * options={['small', 'medium', 'large']}
76
+ * />
77
+ * }
78
+ * />
79
+ * </Accordion.Section>
80
+ * ```
81
+ */
82
+ export const VisualSection = <TConfig extends VisualSectionConfig = VisualSectionConfig>({
83
+ config,
84
+ updateField,
85
+ updateConfig,
86
+ beforeCheckboxes,
87
+ afterCheckboxes,
88
+ themeSelectorPosition = 'before',
89
+ showBorder = true,
90
+ showBorderColorTheme = true,
91
+ showAccent = true,
92
+ showBackground = true,
93
+ showHideBackgroundColor = true
94
+ }: VisualSectionProps<TConfig>) => {
95
+ const visual = config.visual || {}
96
+ const theme = config.theme
97
+
98
+ const renderThemeSelector = () => {
99
+ if (themeSelectorPosition === 'none') return null
100
+
101
+ return (
102
+ <HeaderThemeSelector
103
+ selectedTheme={theme}
104
+ onThemeSelect={theme => updateConfig({ ...config, theme } as TConfig)}
105
+ label='Theme'
106
+ />
107
+ )
108
+ }
109
+
110
+ const renderCheckboxes = () => (
111
+ <div className='checkbox-group'>
112
+ {showBorder && (
113
+ <CheckBox
114
+ value={visual.border}
115
+ section='visual'
116
+ fieldName='border'
117
+ label='Display Border'
118
+ updateField={updateField}
119
+ />
120
+ )}
121
+ {showBorderColorTheme && (
122
+ <CheckBox
123
+ value={visual.borderColorTheme}
124
+ section='visual'
125
+ fieldName='borderColorTheme'
126
+ label='Use Border Color Theme'
127
+ updateField={updateField}
128
+ />
129
+ )}
130
+ {showAccent && (
131
+ <CheckBox
132
+ value={visual.accent}
133
+ section='visual'
134
+ fieldName='accent'
135
+ label='Use Accent Style'
136
+ updateField={updateField}
137
+ />
138
+ )}
139
+ {showBackground && (
140
+ <CheckBox
141
+ value={visual.background}
142
+ section='visual'
143
+ fieldName='background'
144
+ label='Use Theme Background Color'
145
+ updateField={updateField}
146
+ />
147
+ )}
148
+ {showHideBackgroundColor && (
149
+ <CheckBox
150
+ value={visual.hideBackgroundColor}
151
+ section='visual'
152
+ fieldName='hideBackgroundColor'
153
+ label='Hide Background Color'
154
+ updateField={updateField}
155
+ />
156
+ )}
157
+ </div>
158
+ )
159
+
160
+ return (
161
+ <>
162
+ {beforeCheckboxes}
163
+ {themeSelectorPosition === 'before' && renderThemeSelector()}
164
+ {renderCheckboxes()}
165
+ {themeSelectorPosition === 'after' && renderThemeSelector()}
166
+ {afterCheckboxes}
167
+ </>
168
+ )
169
+ }
@@ -5,6 +5,7 @@ import parse from 'html-react-parser'
5
5
  // CDC
6
6
  import Button from '../elements/Button'
7
7
  import MultiSelect from '../MultiSelect'
8
+ import ComboBox from '../ComboBox'
8
9
  import { Visualization } from '../../types/Visualization'
9
10
  import { MultiSelectFilter, VizFilter } from '../../types/VizFilter'
10
11
  import { addValuesToFilters } from '../../helpers/addValuesToFilters'
@@ -14,7 +15,7 @@ import { getNestedOptions } from './helpers/getNestedOptions'
14
15
  import { getWrappingStatuses } from './helpers/filterWrapping'
15
16
  import { handleSorting } from './helpers/handleSorting'
16
17
  import { getChangedFilters } from './helpers/getChangedFilters'
17
- import { getUniqueValues } from '@cdc/map/src/helpers'
18
+ import { getUniqueValues } from '../../helpers/getUniqueValues'
18
19
  import { getQueryParams, updateQueryString } from '../../helpers/queryStringUtils'
19
20
  import { applyQueuedActive } from './helpers/applyQueuedActive'
20
21
  import Tabs from './components/Tabs'
@@ -23,13 +24,14 @@ import { publishAnalyticsEvent } from '../../helpers/metrics/helpers'
23
24
  import { getVizSubType, getVizTitle } from '@cdc/core/helpers/metrics/utils'
24
25
 
25
26
  export const VIZ_FILTER_STYLE = {
27
+ combobox: 'combobox',
26
28
  dropdown: 'dropdown',
29
+ multiSelect: 'multi-select',
27
30
  nestedDropdown: 'nested-dropdown',
28
31
  pill: 'pill',
29
32
  tab: 'tab',
30
33
  tabSimple: 'tab-simple',
31
- tabBar: 'tab bar',
32
- multiSelect: 'multi-select'
34
+ tabBar: 'tab bar'
33
35
  } as const
34
36
 
35
37
  export type VizFilterStyle = (typeof VIZ_FILTER_STYLE)[keyof typeof VIZ_FILTER_STYLE]
@@ -95,7 +97,9 @@ const Filters: React.FC<FilterProps> = ({
95
97
  eventAction: 'change',
96
98
  eventLabel: interactionLabel,
97
99
  vizTitle: getVizTitle(visualizationConfig),
98
- specifics: `key: ${String(newFilters?.[index]?.columnName).toLowerCase()}, value: ${String(newFilters?.[index]?.active).toLowerCase()}`
100
+ specifics: `key: ${String(newFilters?.[index]?.columnName).toLowerCase()}, value: ${String(
101
+ newFilters?.[index]?.active
102
+ ).toLowerCase()}`
99
103
  })
100
104
  }
101
105
 
@@ -141,10 +145,31 @@ const Filters: React.FC<FilterProps> = ({
141
145
  filter.values = getUniqueValues(visualizationConfig.data, filter.columnName)
142
146
  }
143
147
 
144
- newFilters[i].active = handleSorting(filter).values[0]
148
+ // Determine reset value based on filter configuration
149
+ let resetValue
145
150
 
146
- if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
147
- queryParams[filter.setByQueryParameter] = filter.active
151
+ // If filter has a resetLabel, reset to empty state (shows "- Select -")
152
+ if (filter.resetLabel) {
153
+ resetValue = filter.resetLabel
154
+ }
155
+ // If filter has a defaultValue, use that
156
+ else if (filter.defaultValue) {
157
+ resetValue = filter.defaultValue
158
+ }
159
+ // Otherwise, use first value in sorted values array
160
+ else if (filter.values && filter.values.length > 0) {
161
+ resetValue = handleSorting(filter).values[0]
162
+ }
163
+
164
+ // Handle multi-select filters
165
+ if (filter.filterStyle === 'multi-select') {
166
+ newFilters[i].active = resetValue ? [resetValue] : []
167
+ } else {
168
+ newFilters[i].active = resetValue
169
+ }
170
+
171
+ if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== newFilters[i].active) {
172
+ queryParams[filter.setByQueryParameter] = newFilters[i].active
148
173
  needsQueryUpdate = true
149
174
  }
150
175
  })
@@ -181,6 +206,9 @@ const Filters: React.FC<FilterProps> = ({
181
206
 
182
207
  if (visualizationConfig?.filters?.length === 0) return <></>
183
208
 
209
+ const hasVisibleFilters = filters?.some(filter => filter.showDropdown !== false)
210
+ if (!hasVisibleFilters) return <></>
211
+
184
212
  const getClasses = () => {
185
213
  const { visualizationType, legend } = visualizationConfig || {}
186
214
  const baseClass = 'filters-section'
@@ -195,6 +223,12 @@ const Filters: React.FC<FilterProps> = ({
195
223
  return (singleFilter.queuedActive || [singleFilter.active, singleFilter.subGrouping?.active]) as [string, string]
196
224
  }
197
225
 
226
+ // Don't render filter section if all filters are hidden
227
+ const allFiltersHidden = vizFiltersWithValues.every(filter => filter.showDropdown === false)
228
+ if (allFiltersHidden) {
229
+ return null
230
+ }
231
+
198
232
  return (
199
233
  <section className={getClasses().join(' ')}>
200
234
  {visualizationConfig.filterIntro && (
@@ -214,7 +248,9 @@ const Filters: React.FC<FilterProps> = ({
214
248
  'form-group',
215
249
  mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`
216
250
  ]
217
- const mobileExempt = ['nested-dropdown', 'multi-select', VIZ_FILTER_STYLE.tabSimple].includes(filterStyle)
251
+ const mobileExempt = ['nested-dropdown', 'multi-select', 'combobox', VIZ_FILTER_STYLE.tabSimple].includes(
252
+ filterStyle
253
+ )
218
254
  const { isDropdown } = wrappingFilters[columnName] || {}
219
255
  const showDefaultDropdown =
220
256
  ((filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt) || isDropdown
@@ -281,6 +317,17 @@ const Filters: React.FC<FilterProps> = ({
281
317
  handleSelectedItems={value => changeFilterActive(outerIndex, value)}
282
318
  />
283
319
  )}
320
+ {filterStyle === 'combobox' && (
321
+ <ComboBox
322
+ options={singleFilter.values.map(v => ({ value: v, label: v }))}
323
+ fieldName={outerIndex}
324
+ updateField={(_section, _subSection, fieldName, value) => {
325
+ changeFilterActive(fieldName, value)
326
+ }}
327
+ selected={(singleFilter.queuedActive || singleFilter.active) as string}
328
+ label={label}
329
+ />
330
+ )}
284
331
  </div>
285
332
  )
286
333
  })}
@@ -295,9 +342,9 @@ const Filters: React.FC<FilterProps> = ({
295
342
  >
296
343
  Apply
297
344
  </Button>
298
- <Button secondary disabled={initialFiltersActive} onClick={handleFiltersReset}>
345
+ <button className='btn btn-link' disabled={initialFiltersActive} onClick={handleFiltersReset}>
299
346
  Clear Filters
300
- </Button>
347
+ </button>
301
348
  </div>
302
349
  ) : (
303
350
  <></>
@@ -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}>
@@ -8,9 +8,10 @@ type GetOptionsMemoParams = {
8
8
  }
9
9
 
10
10
  export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOptionsMemoParams): NestedOptions => {
11
+ if (!values?.length && !orderedValues?.length) return []
11
12
  // keep custom ordered value order
12
13
  const filteredValues = orderedValues?.length
13
- ? orderedValues.filter(orderedValue => values.includes(orderedValue))
14
+ ? orderedValues.filter(orderedValue => values?.includes(orderedValue))
14
15
  : values
15
16
  const options: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
16
17
  if (!subGrouping) return [[value], []]
@@ -2,7 +2,7 @@ import _ from 'lodash'
2
2
 
3
3
  export const handleSorting = singleFilter => {
4
4
  const singleFilterValues = _.cloneDeep(singleFilter.values)
5
- if (singleFilter.order === 'cust' && singleFilter.filterStyle !== 'nested-dropdown') {
5
+ if (singleFilter.order === 'cust') {
6
6
  singleFilter.values = singleFilter.orderedValues?.length ? singleFilter.orderedValues : singleFilterValues
7
7
  return singleFilter
8
8
  }
@@ -1,25 +1,35 @@
1
- import { Footnote } from '../../types/Footnotes'
2
- import './footnotes.css'
3
-
4
- type FootnotesProps = {
5
- footnotes: Footnote[]
6
- }
7
-
8
- const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
9
- return (
10
- <footer className='col-12 m-3 mt-1 mb-0'>
11
- <ul className='cove-footnotes'>
12
- {footnotes.map((note, i) => {
13
- return (
14
- <li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
15
- {note.symbol && <span className='me-1'>{note.symbol}</span>}
16
- {note.text}
17
- </li>
18
- )
19
- })}
20
- </ul>
21
- </footer>
22
- )
23
- }
24
-
25
- export default Footnotes
1
+ import { Footnote } from '../../types/Footnotes'
2
+ import parse from 'html-react-parser'
3
+ import './footnotes.css'
4
+
5
+ type FootnotesProps = {
6
+ footnotes: Footnote[]
7
+ }
8
+
9
+ const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
10
+ // Convert newlines to <br> tags and parse HTML
11
+ const processFootnoteText = (text: string) => {
12
+ if (!text) return ''
13
+ // Convert newline characters to <br> tags
14
+ const textWithBreaks = text.replace(/\n/g, '<br>')
15
+ // Parse HTML (html-react-parser handles sanitization)
16
+ return parse(textWithBreaks)
17
+ }
18
+
19
+ return (
20
+ <footer className='col-12 m-3 mt-1 mb-0'>
21
+ <ul className='cove-footnotes'>
22
+ {footnotes.map((note, i) => {
23
+ return (
24
+ <li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
25
+ {note.symbol && <span className='me-1'>{note.symbol}</span>}
26
+ {processFootnoteText(note.text)}
27
+ </li>
28
+ )
29
+ })}
30
+ </ul>
31
+ </footer>
32
+ )
33
+ }
34
+
35
+ export default Footnotes
@@ -1,17 +1,45 @@
1
1
  import Footnotes from './Footnotes'
2
- import FootnotesConfig from '../../types/Footnotes'
2
+ import FootnotesConfig, { Footnote } from '../../types/Footnotes'
3
3
  import _ from 'lodash'
4
4
  import { useMemo } from 'react'
5
5
  import { filterVizData } from '../../helpers/filterVizData'
6
6
  import { VizFilter } from '../../types/VizFilter'
7
+ import { MarkupVariable } from '../../types/MarkupVariable'
8
+ import { processMarkupVariables } from '../../helpers/markupProcessor'
7
9
 
8
10
  type StandAloneProps = {
9
11
  config: FootnotesConfig
10
12
  filters?: VizFilter[]
13
+ markupVariables?: MarkupVariable[]
14
+ enableMarkupVariables?: boolean
15
+ data?: Object[]
11
16
  }
12
17
 
13
- const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) => {
18
+ const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters, markupVariables = [], enableMarkupVariables = false, data = [] }) => {
14
19
  if (!config) return null
20
+
21
+ // Helper function to process markup variables in footnote text
22
+ const processFootnoteText = (text: string): string => {
23
+ if (!enableMarkupVariables || !markupVariables || markupVariables.length === 0) {
24
+ return text
25
+ }
26
+
27
+ // Use data from props if available, otherwise use config.data
28
+ const footnoteData = data.length > 0 ? data : config.data || []
29
+
30
+ const { processedContent } = processMarkupVariables(
31
+ text,
32
+ footnoteData,
33
+ markupVariables,
34
+ {
35
+ filters,
36
+ isEditor: false
37
+ }
38
+ )
39
+
40
+ return processedContent
41
+ }
42
+
15
43
  // get the api footnotes from the config
16
44
  const apiFootnotes = useMemo(() => {
17
45
  // If filters exist and should filter footnotes, apply them, otherwise use data as-is
@@ -20,13 +48,21 @@ const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) =>
20
48
  const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
21
49
  const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
22
50
  _data.sort((a, b) => a[orderColumn] - b[orderColumn])
23
- return _data.map(row => ({ symbol: row[symbolColumn], text: row[textColumn] }))
51
+ return _data.map(row => ({
52
+ symbol: row[symbolColumn],
53
+ text: processFootnoteText(row[textColumn])
54
+ }))
24
55
  }
25
56
  return []
26
- }, [config.dynamicFootnotes, config.data, filters])
57
+ }, [config.dynamicFootnotes, config.data, filters, markupVariables, enableMarkupVariables, data])
27
58
 
28
- // get static footnotes from the config.footnotes
29
- const staticFootnotes = config.staticFootnotes || []
59
+ // get static footnotes from the config.footnotes and process their text
60
+ const staticFootnotes: Footnote[] = useMemo(() => {
61
+ return (config.staticFootnotes || []).map(footnote => ({
62
+ ...footnote,
63
+ text: processFootnoteText(footnote.text)
64
+ }))
65
+ }, [config.staticFootnotes, markupVariables, enableMarkupVariables, data, filters])
30
66
 
31
67
  return <Footnotes footnotes={[...apiFootnotes, ...staticFootnotes]} />
32
68
  }
@@ -0,0 +1,43 @@
1
+ /* HeaderThemeSelector component styles */
2
+
3
+ .header {
4
+ margin-bottom: 1rem;
5
+ }
6
+
7
+ .header .edit-label {
8
+ display: block;
9
+ margin-bottom: 0.5rem;
10
+ font-weight: 500;
11
+ }
12
+
13
+ .header .color-palette {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ gap: 0.5rem;
17
+ list-style: none;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+
22
+ .header .color-palette button {
23
+ width: 30px;
24
+ height: 30px;
25
+ border-radius: 50%;
26
+ border: 2px solid transparent;
27
+ cursor: pointer;
28
+ transition: all 0.2s ease;
29
+ outline: none;
30
+ }
31
+
32
+ .header .color-palette button:hover {
33
+ transform: scale(1.1);
34
+ }
35
+
36
+ .header .color-palette button.selected {
37
+ border-color: #000;
38
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
39
+ }
40
+
41
+ .header .color-palette button:focus {
42
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
43
+ }