@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.
- package/_stories/Gallery.Charts.stories.tsx +307 -0
- package/_stories/Gallery.DataBite.stories.tsx +72 -0
- package/_stories/Gallery.Maps.stories.tsx +230 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +192 -0
- package/_stories/PageBRFSS.stories.tsx +289 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +202 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +196 -0
- package/_stories/PageMaternalMortality.stories.tsx +192 -0
- package/_stories/PageOralHealth.stories.tsx +196 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +195 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +196 -0
- package/_stories/PageWastewater.stories.tsx +463 -0
- package/_stories/StoryRenderingTests.stories.tsx +164 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +7 -1
- package/components/AdvancedEditor/EmbedEditor.tsx +281 -0
- package/components/ComboBox/ComboBox.tsx +345 -0
- package/components/ComboBox/combobox.styles.css +185 -0
- package/components/ComboBox/index.ts +1 -0
- package/components/CustomColorsEditor/CustomColorsEditor.css +299 -0
- package/components/CustomColorsEditor/CustomColorsEditor.tsx +209 -0
- package/components/CustomColorsEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/DataTableStandAlone.tsx +8 -3
- package/components/DataTable/components/DataTableEditorPanel.tsx +12 -2
- package/components/DataTable/data-table.css +217 -210
- package/components/DataTable/helpers/mapCellMatrix.tsx +28 -9
- package/components/DataTable/helpers/standardizeState.js +2 -2
- package/components/DataTable/helpers/tests/standardizeState.test.js +54 -0
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +54 -28
- package/components/EditorPanel/EditorPanel.styles.css +439 -0
- package/components/EditorPanel/EditorPanel.tsx +144 -0
- package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
- package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
- package/components/EditorPanel/FootnotesEditor.tsx +44 -37
- package/components/EditorPanel/Inputs.tsx +44 -8
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +35 -62
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +246 -175
- package/components/EditorPanel/components/MarkupVariablesEditor.tsx +61 -22
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +57 -10
- package/components/Filters/components/Dropdown.tsx +6 -1
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Footnotes/Footnotes.tsx +35 -25
- package/components/Footnotes/FootnotesStandAlone.tsx +42 -6
- package/components/HeaderThemeSelector/HeaderThemeSelector.css +43 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.stories.tsx +74 -0
- package/components/HeaderThemeSelector/HeaderThemeSelector.tsx +61 -0
- package/components/HeaderThemeSelector/index.ts +2 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +82 -0
- package/components/Layout/components/Visualization/index.tsx +16 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Layout/styles/editor.scss +2 -1
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/Loader/Loader.tsx +1 -1
- package/components/MediaControls.tsx +63 -34
- package/components/PaletteConversionModal.tsx +7 -4
- package/components/PaletteSelector/PaletteSelector.css +49 -6
- package/components/Table/components/Cell.tsx +23 -2
- package/components/Table/components/Row.tsx +5 -3
- package/components/_stories/Filters.stories.tsx +20 -1
- package/components/_stories/Footnotes.CSV.stories.tsx +247 -0
- package/components/_stories/Footnotes.stories.tsx +768 -3
- package/components/_stories/Inputs.stories.tsx +2 -2
- package/components/_stories/styles.scss +0 -1
- package/components/ui/Accordion.jsx +1 -1
- package/components/ui/Icon.tsx +3 -1
- package/components/ui/Title/index.tsx +30 -2
- package/components/ui/Title/title.styles.css +42 -0
- package/components/ui/accordion.styles.css +57 -0
- package/data/chartColorPalettes.ts +1 -1
- package/dist/cove-main.css +75 -6
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +8 -1
- package/helpers/addValuesToFilters.ts +11 -1
- package/helpers/constants.ts +37 -0
- package/helpers/cove/number.ts +33 -12
- package/helpers/coveUpdateWorker.ts +20 -11
- package/helpers/embedCodeGenerator.ts +109 -0
- package/helpers/fetchRemoteData.ts +3 -15
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/markupProcessor.ts +27 -12
- package/helpers/mergeCustomOrderValues.ts +37 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/parseCsvWithQuotes.ts +65 -0
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +268 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +17 -4
- package/helpers/tests/prepareScreenshot.test.ts +414 -0
- package/helpers/tests/queryStringUtils.test.ts +381 -0
- package/helpers/tests/testStandaloneBuild.ts +23 -5
- package/helpers/useDataVizClasses.ts +0 -1
- package/helpers/ver/4.25.11.ts +13 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/viewports.ts +2 -0
- package/hooks/useDataColumns.ts +63 -0
- package/hooks/useFilterManagement.ts +94 -0
- package/hooks/useLegendSeparators.ts +26 -0
- package/hooks/useListManagement.ts +192 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +0 -3
- package/styles/_common-components.css +73 -0
- package/styles/_global.scss +25 -5
- package/styles/base.scss +0 -50
- package/styles/cove-main.scss +3 -1
- package/styles/filters.scss +10 -3
- package/styles/v2/base/index.scss +0 -1
- package/styles/v2/components/editor.scss +14 -6
- package/styles/v2/utils/_breakpoints.scss +1 -1
- package/styles/v2/utils/index.scss +0 -1
- package/styles/waiting.scss +1 -1
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/MarkupInclude.ts +5 -3
- package/types/MarkupVariable.ts +1 -1
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +1 -0
- package/types/VizFilter.ts +2 -0
- package/LICENSE +0 -201
- package/styles/_mixins.scss +0 -13
- package/styles/_typography.scss +0 -0
- package/styles/v2/base/_typography.scss +0 -0
- package/styles/v2/components/guidance-block.scss +0 -74
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
326
|
-
? getColumnValues
|
|
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 '
|
|
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(
|
|
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
|
-
|
|
148
|
+
// Determine reset value based on filter configuration
|
|
149
|
+
let resetValue
|
|
145
150
|
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
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
|
-
<
|
|
345
|
+
<button className='btn btn-link' disabled={initialFiltersActive} onClick={handleFiltersReset}>
|
|
299
346
|
Clear Filters
|
|
300
|
-
</
|
|
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
|
|
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'
|
|
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 '
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 => ({
|
|
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 =
|
|
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
|
+
}
|