@cdc/core 4.25.11 → 4.26.2
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/.claude/agents/qa-test-developer.md +126 -0
- package/CLAUDE.local.md +67 -0
- package/_stories/Gallery.Charts.stories.tsx +300 -0
- package/_stories/Gallery.DataBite.stories.tsx +79 -0
- package/_stories/Gallery.Maps.stories.tsx +239 -0
- package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
- package/_stories/PageART.stories.tsx +193 -0
- package/_stories/PageBRFSS.stories.tsx +294 -0
- package/_stories/PageCancerRegistries.stories.tsx +199 -0
- package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
- package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
- package/_stories/PageMaternalMortality.stories.tsx +193 -0
- package/_stories/PageOralHealth.stories.tsx +201 -0
- package/_stories/PageRespiratory.stories.tsx +332 -0
- package/_stories/PageSmokingTobacco.stories.tsx +200 -0
- package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
- package/_stories/PageWastewater.stories.tsx +477 -0
- package/_stories/VegaImport.stories.tsx +401 -0
- package/_stories/vega-fixtures/bars-with-line.json +444 -0
- package/_stories/vega-fixtures/bars.json +58 -0
- package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
- package/_stories/vega-fixtures/combo.json +68 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
- package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
- package/_stories/vega-fixtures/horizontal-bar.json +427 -0
- package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
- package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
- package/_stories/vega-fixtures/lines.json +227 -0
- package/_stories/vega-fixtures/measles-bars.json +348 -0
- package/_stories/vega-fixtures/measles-map.json +11101 -0
- package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
- package/_stories/vega-fixtures/multi-dataset.json +255 -0
- package/_stories/vega-fixtures/no-data.json +14 -0
- package/_stories/vega-fixtures/pie-chart.json +94 -0
- package/_stories/vega-fixtures/repeat-spec.json +47 -0
- package/_stories/vega-fixtures/stacked-area.json +222 -0
- package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
- package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
- package/_stories/vega-fixtures/stacked-bars.json +212 -0
- package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
- package/_stories/vega-fixtures/warning-combo.json +59 -0
- package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
- package/assets/icon-chart-area.svg +1 -0
- package/assets/icon-chart-radar.svg +23 -0
- package/assets/icon-magnifying-glass.svg +5 -0
- package/assets/icon-warming-stripes.svg +13 -0
- package/assets/logo2.svg +31 -0
- package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
- package/components/AdvancedEditor/EmbedEditor.tsx +513 -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.tsx +3 -10
- package/components/DataTable/DataTable.tsx +132 -58
- package/components/DataTable/data-table.css +216 -215
- package/components/DataTable/helpers/getSeriesName.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
- package/components/EditorPanel/ColumnsEditor.tsx +37 -19
- package/components/EditorPanel/DataTableEditor.tsx +51 -25
- package/components/EditorPanel/EditorPanel.styles.css +16 -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/Inputs.tsx +33 -7
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
- package/components/EditorPanel/sections/VisualSection.tsx +169 -0
- package/components/Filters/Filters.tsx +31 -5
- package/components/Filters/helpers/getNestedOptions.ts +2 -1
- package/components/Filters/helpers/handleSorting.ts +1 -1
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
- package/components/Layout/components/Visualization/index.tsx +27 -1
- package/components/Layout/components/Visualization/visualizations.scss +7 -0
- package/components/Legend/Legend.Gradient.tsx +1 -1
- package/components/MediaControls.tsx +53 -28
- package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
- package/components/_stories/DataTable.stories.tsx +1 -0
- 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/data/colorPalettes.ts +18 -5
- package/data/mapColorPalettes.ts +10 -0
- package/devTemplate/dev.js +235 -0
- package/devTemplate/index.html +30 -0
- package/devTemplate/preview.html +1503 -0
- package/devTemplate/sidebar.css +151 -0
- package/dist/cove-main.css +2803 -4448
- package/dist/cove-main.css.map +1 -1
- package/generateViteConfig.js +118 -2
- package/helpers/DataTransform.ts +1 -5
- package/helpers/addValuesToFilters.ts +6 -1
- package/helpers/cove/date.ts +33 -1
- package/helpers/cove/string.ts +29 -0
- package/helpers/coveUpdateWorker.ts +21 -12
- package/helpers/embed/embedCodeGenerator.ts +80 -0
- package/helpers/embed/embedHelper.js +158 -0
- package/helpers/embed/filterUtils.ts +121 -0
- package/helpers/embed/index.ts +21 -0
- package/helpers/embed/urlValidation.ts +119 -0
- package/helpers/filterVizData.ts +6 -1
- package/helpers/getFileExtension.ts +0 -6
- package/helpers/getUniqueValues.ts +19 -0
- package/helpers/hashObj.ts +25 -0
- package/helpers/isRightAlignedTableValue.js +5 -0
- package/helpers/metrics/helpers.ts +1 -0
- package/helpers/metrics/types.ts +3 -0
- package/helpers/palettes/colorDistributions.ts +1 -1
- package/helpers/palettes/utils.ts +12 -12
- package/helpers/parseCsvWithQuotes.ts +15 -14
- package/helpers/pivotData.ts +2 -2
- package/helpers/prepareScreenshot.ts +288 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/testing.ts +44 -0
- package/helpers/tests/DataTransform.test.ts +125 -0
- package/helpers/tests/date.test.ts +64 -0
- 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/vegaConfig.ts +1 -1
- package/helpers/vegaConfigImport.ts +160 -0
- package/helpers/ver/4.26.1.ts +80 -0
- package/helpers/ver/4.26.2.ts +84 -0
- package/helpers/ver/tests/4.26.1.test.ts +105 -0
- package/helpers/ver/tests/4.26.2.test.ts +298 -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 +29 -33
- package/styles/_button-section.scss +0 -3
- package/styles/v2/components/editor.scss +9 -9
- package/styles/v2/utils/_grid.scss +8 -3
- package/types/Annotation.ts +10 -11
- package/types/Axis.ts +1 -0
- package/types/ForecastingSeriesKey.ts +1 -0
- package/types/General.ts +2 -0
- package/types/MarkupInclude.ts +1 -0
- package/types/Palette.ts +21 -0
- package/types/Series.ts +3 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +7 -0
- package/types/VizFilter.ts +1 -0
- package/LICENSE +0 -201
- package/_stories/StoryRenderingTests.stories.tsx +0 -164
|
@@ -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
|
|
|
@@ -202,6 +206,9 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
202
206
|
|
|
203
207
|
if (visualizationConfig?.filters?.length === 0) return <></>
|
|
204
208
|
|
|
209
|
+
const hasVisibleFilters = filters?.some(filter => filter.showDropdown !== false)
|
|
210
|
+
if (!hasVisibleFilters) return <></>
|
|
211
|
+
|
|
205
212
|
const getClasses = () => {
|
|
206
213
|
const { visualizationType, legend } = visualizationConfig || {}
|
|
207
214
|
const baseClass = 'filters-section'
|
|
@@ -216,6 +223,12 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
216
223
|
return (singleFilter.queuedActive || [singleFilter.active, singleFilter.subGrouping?.active]) as [string, string]
|
|
217
224
|
}
|
|
218
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
|
+
|
|
219
232
|
return (
|
|
220
233
|
<section className={getClasses().join(' ')}>
|
|
221
234
|
{visualizationConfig.filterIntro && (
|
|
@@ -235,7 +248,9 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
235
248
|
'form-group',
|
|
236
249
|
mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`
|
|
237
250
|
]
|
|
238
|
-
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
|
+
)
|
|
239
254
|
const { isDropdown } = wrappingFilters[columnName] || {}
|
|
240
255
|
const showDefaultDropdown =
|
|
241
256
|
((filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt) || isDropdown
|
|
@@ -302,6 +317,17 @@ const Filters: React.FC<FilterProps> = ({
|
|
|
302
317
|
handleSelectedItems={value => changeFilterActive(outerIndex, value)}
|
|
303
318
|
/>
|
|
304
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
|
+
)}
|
|
305
331
|
</div>
|
|
306
332
|
)
|
|
307
333
|
})}
|
|
@@ -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
|
}
|
|
@@ -186,6 +186,88 @@
|
|
|
186
186
|
overflow: hidden;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
.editor-field-item {
|
|
190
|
+
position: relative;
|
|
191
|
+
padding: 5px;
|
|
192
|
+
background-color: #fff;
|
|
193
|
+
border: 1px solid #ccc;
|
|
194
|
+
margin-bottom: 10px;
|
|
195
|
+
|
|
196
|
+
&:last-child {
|
|
197
|
+
padding-bottom: 5px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&__header {
|
|
201
|
+
width: 100%;
|
|
202
|
+
background-color: #f5f5f5;
|
|
203
|
+
border: 1px solid #ccc;
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
padding: 5px;
|
|
207
|
+
padding-left: 5px !important;
|
|
208
|
+
|
|
209
|
+
.cove-icon {
|
|
210
|
+
flex-shrink: 0;
|
|
211
|
+
padding-right: 5px;
|
|
212
|
+
margin-right: 10px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.btn {
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
&__name {
|
|
221
|
+
margin-left: 0.5rem;
|
|
222
|
+
user-select: none;
|
|
223
|
+
flex: 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
&__content {
|
|
227
|
+
padding: 10px;
|
|
228
|
+
background-color: #fff;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
&__remove-wrapper {
|
|
232
|
+
display: flex;
|
|
233
|
+
justify-content: flex-end;
|
|
234
|
+
margin-bottom: 10px;
|
|
235
|
+
|
|
236
|
+
.btn {
|
|
237
|
+
border: 1px solid red;
|
|
238
|
+
border-radius: 10px;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.draggable-field-list {
|
|
244
|
+
list-style: none;
|
|
245
|
+
padding: 0;
|
|
246
|
+
margin: 0;
|
|
247
|
+
|
|
248
|
+
.currently-dragging {
|
|
249
|
+
opacity: 0.8;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.editor-field-item {
|
|
253
|
+
cursor: grab;
|
|
254
|
+
|
|
255
|
+
&:active {
|
|
256
|
+
cursor: grabbing;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
&__header .cove-icon {
|
|
260
|
+
cursor: grab;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.filters-list {
|
|
266
|
+
list-style: none;
|
|
267
|
+
padding: 0;
|
|
268
|
+
margin: 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
189
271
|
.accordion__heading {
|
|
190
272
|
background: var(--lightestGray);
|
|
191
273
|
}
|
|
@@ -544,9 +626,9 @@
|
|
|
544
626
|
|
|
545
627
|
.sort-list {
|
|
546
628
|
list-style: none;
|
|
629
|
+
padding: 0;
|
|
547
630
|
|
|
548
631
|
> li {
|
|
549
|
-
margin-right: 0.3em;
|
|
550
632
|
margin-bottom: 0.3em;
|
|
551
633
|
}
|
|
552
634
|
}
|
|
@@ -559,8 +641,8 @@
|
|
|
559
641
|
background: #f1f1f1;
|
|
560
642
|
padding: 0.4em 0.6em;
|
|
561
643
|
font-size: 0.8em;
|
|
562
|
-
margin-bottom: 0.3em;
|
|
563
644
|
cursor: move;
|
|
645
|
+
width: 100%;
|
|
564
646
|
}
|
|
565
647
|
|
|
566
648
|
.info {
|
|
@@ -11,7 +11,14 @@ import { MapConfig } from '@cdc/map/src/types/MapConfig'
|
|
|
11
11
|
|
|
12
12
|
type VisualizationWrapper = {
|
|
13
13
|
children: React.ReactNode
|
|
14
|
-
config:
|
|
14
|
+
config:
|
|
15
|
+
| ChartConfig
|
|
16
|
+
| DataBiteConfig
|
|
17
|
+
| WaffleChartConfig
|
|
18
|
+
| MarkupIncludeConfig
|
|
19
|
+
| DashboardFilters
|
|
20
|
+
| MapConfig
|
|
21
|
+
| DataTableConfig
|
|
15
22
|
currentViewport?: string
|
|
16
23
|
imageId?: string
|
|
17
24
|
isEditor: boolean
|
|
@@ -64,6 +71,10 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
|
|
|
64
71
|
if (config?.runtime?.editorErrorMessage.length !== 0) classes.push('type-map--has-error')
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
if (config.type === 'table') {
|
|
75
|
+
classes.push('type-data-table')
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
if (config.type === 'data-bite') {
|
|
68
79
|
classes.push('cdc-open-viz-module', 'type-data-bite', currentViewport, config.theme, `font-${config.fontSize}`)
|
|
69
80
|
if (isEditor) {
|
|
@@ -89,6 +100,21 @@ const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, r
|
|
|
89
100
|
classes.push('is-editor')
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
// Add TP5 style classes
|
|
104
|
+
if (config.visualizationType === 'TP5 Waffle') {
|
|
105
|
+
classes.push('waffle__style--tp5')
|
|
106
|
+
if (config.visual?.whiteBackground) {
|
|
107
|
+
classes.push('white-background-style')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (config.visualizationType === 'TP5 Gauge') {
|
|
112
|
+
classes.push('gauge__style--tp5')
|
|
113
|
+
if (config.visual?.whiteBackground) {
|
|
114
|
+
classes.push('white-background-style')
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
92
118
|
classes.push('cove-component', 'waffle-chart')
|
|
93
119
|
}
|
|
94
120
|
return classes
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
left: 0;
|
|
35
35
|
width: 100% !important;
|
|
36
36
|
grid-area: content;
|
|
37
|
+
padding: 1rem;
|
|
38
|
+
|
|
39
|
+
// Prevent double padding on nested .cove-component__content divs
|
|
40
|
+
// (e.g., in markup-include, waffle-chart, filtered-text)
|
|
41
|
+
.cove-component__content {
|
|
42
|
+
padding: 0;
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
}
|
|
@@ -4,7 +4,7 @@ import { type MapConfig } from '@cdc/map/src/types/MapConfig'
|
|
|
4
4
|
import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
5
5
|
import { getTextWidth } from '../../helpers/getTextWidth'
|
|
6
6
|
import { DimensionsType } from '../../types/Dimensions'
|
|
7
|
-
import useLegendSeparators from '
|
|
7
|
+
import useLegendSeparators from '../../hooks/useLegendSeparators'
|
|
8
8
|
|
|
9
9
|
const MARGIN = 1
|
|
10
10
|
const BORDER_SIZE = 1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
// import html2pdf from 'html2pdf.js'
|
|
3
2
|
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
4
3
|
import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
|
|
4
|
+
import { prepareScreenshotContainer } from '@cdc/core/helpers/prepareScreenshot'
|
|
5
5
|
|
|
6
6
|
const buttonText = {
|
|
7
7
|
pdf: 'Download PDF',
|
|
@@ -35,7 +35,7 @@ const saveImageAs = (uri, filename) => {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
38
|
+
const generateMedia = (state, type, elementToCapture, interactionLabel, includeContextInDownload = false) => {
|
|
39
39
|
// Identify Selector
|
|
40
40
|
const baseSvg = document.querySelector(`[data-download-id=${elementToCapture}]`)
|
|
41
41
|
|
|
@@ -58,22 +58,13 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
58
58
|
// Apparently some packages use state.title where others use state.general.title
|
|
59
59
|
const handleFileName = state => {
|
|
60
60
|
// dashboard titles
|
|
61
|
-
if (state?.dashboard?.title)
|
|
62
|
-
return (
|
|
63
|
-
`${state.dashboard.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
64
|
-
)
|
|
61
|
+
if (state?.dashboard?.title) return `${state.dashboard.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
65
62
|
|
|
66
63
|
// map titles
|
|
67
|
-
if (state?.general?.title)
|
|
68
|
-
return (
|
|
69
|
-
`${state.general.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
70
|
-
)
|
|
64
|
+
if (state?.general?.title) return `${state.general.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
71
65
|
|
|
72
66
|
// chart titles
|
|
73
|
-
if (state?.title)
|
|
74
|
-
return (
|
|
75
|
-
`${state.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
76
|
-
)
|
|
67
|
+
if (state?.title) return `${state.title.replace(/\s+/g, '-').toLowerCase()}-${timestamp}`
|
|
77
68
|
|
|
78
69
|
return 'no-title'
|
|
79
70
|
}
|
|
@@ -82,18 +73,13 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
82
73
|
|
|
83
74
|
switch (type) {
|
|
84
75
|
case 'image':
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Simple configurable padding (main fix for spacing issues)
|
|
88
|
-
const downloadPadding = state.downloadImagePadding !== undefined ? state.downloadImagePadding : (!state.showTitle ? 35 : 0)
|
|
89
|
-
if (downloadPadding > 0) {
|
|
90
|
-
container.style.padding = `${downloadPadding}px`
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
container.appendChild(baseSvg.cloneNode(true));
|
|
76
|
+
// Prepare screenshot container with all cloning, styling, and transformations
|
|
77
|
+
const container = prepareScreenshotContainer(baseSvg, includeContextInDownload, elementToCapture)
|
|
94
78
|
|
|
95
79
|
const downloadImage = async () => {
|
|
96
|
-
|
|
80
|
+
// Append to main element if exists, otherwise body
|
|
81
|
+
const targetElement = document.querySelector('main') || document.body
|
|
82
|
+
targetElement.appendChild(container)
|
|
97
83
|
|
|
98
84
|
// Fix select elements to show their current selected values before screenshot
|
|
99
85
|
const selectElements = container.querySelectorAll('select')
|
|
@@ -119,10 +105,10 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
119
105
|
el.className.search(/download-buttons|download-links|data-table-container/) !== -1,
|
|
120
106
|
useCORS: true,
|
|
121
107
|
scale: 2, // Better quality
|
|
122
|
-
allowTaint: true
|
|
108
|
+
allowTaint: true
|
|
123
109
|
})
|
|
124
110
|
.then(canvas => {
|
|
125
|
-
|
|
111
|
+
targetElement.removeChild(container) // Clean up container from wherever we appended it
|
|
126
112
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
127
113
|
publishAnalyticsEvent({
|
|
128
114
|
vizType: state.type,
|
|
@@ -163,13 +149,23 @@ const generateMedia = (state, type, elementToCapture, interactionLabel) => {
|
|
|
163
149
|
}
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
|
|
152
|
+
// Button component for Dashboard downloads (renders as actual button)
|
|
153
|
+
const Button = ({
|
|
154
|
+
state,
|
|
155
|
+
text,
|
|
156
|
+
type,
|
|
157
|
+
title,
|
|
158
|
+
elementToCapture,
|
|
159
|
+
interactionLabel = '',
|
|
160
|
+
includeContextInDownload = false
|
|
161
|
+
}) => {
|
|
167
162
|
const buttonClasses = ['btn', 'btn-primary']
|
|
163
|
+
|
|
168
164
|
return (
|
|
169
165
|
<button
|
|
170
166
|
className={buttonClasses.join(' ')}
|
|
171
167
|
title={title}
|
|
172
|
-
onClick={() => generateMedia(state, type, elementToCapture, interactionLabel)}
|
|
168
|
+
onClick={() => generateMedia(state, type, elementToCapture, interactionLabel, includeContextInDownload)}
|
|
173
169
|
style={{ lineHeight: '1.4em' }}
|
|
174
170
|
>
|
|
175
171
|
{buttonText[type]}
|
|
@@ -177,6 +173,34 @@ const Button = ({ state, text, type, title, elementToCapture, interactionLabel =
|
|
|
177
173
|
)
|
|
178
174
|
}
|
|
179
175
|
|
|
176
|
+
// DownloadLink component for Chart/Map downloads (renders as text link)
|
|
177
|
+
const DownloadLink = ({
|
|
178
|
+
state,
|
|
179
|
+
type,
|
|
180
|
+
title,
|
|
181
|
+
elementToCapture,
|
|
182
|
+
interactionLabel = '',
|
|
183
|
+
includeContextInDownload = false
|
|
184
|
+
}) => {
|
|
185
|
+
const vizType = state?.type === 'map' ? 'Map' : 'Chart'
|
|
186
|
+
const format = type === 'pdf' ? 'PDF' : 'PNG'
|
|
187
|
+
const linkText = `Download ${vizType} (${format})`
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<a
|
|
191
|
+
role='button'
|
|
192
|
+
onClick={() => generateMedia(state, type, elementToCapture, interactionLabel, includeContextInDownload)}
|
|
193
|
+
aria-label={title}
|
|
194
|
+
title={title}
|
|
195
|
+
className={`no-border`}
|
|
196
|
+
style={{ cursor: 'pointer' }}
|
|
197
|
+
data-html2canvas-ignore
|
|
198
|
+
>
|
|
199
|
+
{linkText}
|
|
200
|
+
</a>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
180
204
|
// Link to CSV/JSON data
|
|
181
205
|
const Link = ({ config, dashboardDataConfig, interactionLabel }) => {
|
|
182
206
|
let dataConfig = dashboardDataConfig || config
|
|
@@ -238,6 +262,7 @@ const MediaControls = () => null
|
|
|
238
262
|
MediaControls.Section = Section
|
|
239
263
|
MediaControls.Link = Link
|
|
240
264
|
MediaControls.Button = Button
|
|
265
|
+
MediaControls.DownloadLink = DownloadLink
|
|
241
266
|
MediaControls.generateMedia = generateMedia
|
|
242
267
|
|
|
243
268
|
export default MediaControls
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import CustomColorsEditor from '../CustomColorsEditor/CustomColorsEditor'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof CustomColorsEditor> = {
|
|
6
|
+
title: 'Components/Atoms/CustomColorsEditor',
|
|
7
|
+
component: CustomColorsEditor
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
type Story = StoryObj<typeof CustomColorsEditor>
|
|
12
|
+
|
|
13
|
+
const fiftyColors = [
|
|
14
|
+
'#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
|
|
15
|
+
'#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
|
|
16
|
+
'#469990', '#dcbeff', '#9a6324', '#fffac8', '#800000',
|
|
17
|
+
'#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9',
|
|
18
|
+
'#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231',
|
|
19
|
+
'#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4',
|
|
20
|
+
'#469990', '#dcbeff', '#9a6324', '#fffac8', '#800000',
|
|
21
|
+
'#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9',
|
|
22
|
+
'#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00',
|
|
23
|
+
'#ffff33', '#a65628', '#f781bf', '#999999', '#66c2a5'
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const Wrapper = ({ initialColors }: { initialColors: string[] }) => {
|
|
27
|
+
const [colors, setColors] = useState(initialColors)
|
|
28
|
+
return <CustomColorsEditor colors={colors} onChange={setColors} label='Custom Color Order' />
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
render: () => <Wrapper initialColors={['#3366cc', '#dc3912', '#ff9900']} />
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const FiftyColors: Story = {
|
|
36
|
+
render: () => <Wrapper initialColors={fiftyColors} />
|
|
37
|
+
}
|
package/components/ui/Icon.tsx
CHANGED
|
@@ -36,6 +36,7 @@ import iconTable from '../../assets/icon-table.svg'
|
|
|
36
36
|
import iconSankey from '../../assets/icon-sankey.svg'
|
|
37
37
|
import iconRotateLeft from '../../assets/icon-rotate-left.svg'
|
|
38
38
|
import iconCommand from '../../assets/icon-command.svg'
|
|
39
|
+
import iconMagnifyingGlass from '../../assets/icon-magnifying-glass.svg'
|
|
39
40
|
|
|
40
41
|
import '../../styles/v2/components/icon.scss'
|
|
41
42
|
|
|
@@ -75,7 +76,8 @@ const iconHash = {
|
|
|
75
76
|
table: iconTable,
|
|
76
77
|
sankey: iconSankey,
|
|
77
78
|
rotateLeft: iconRotateLeft,
|
|
78
|
-
command: iconCommand
|
|
79
|
+
command: iconCommand,
|
|
80
|
+
magnifyingGlass: iconMagnifyingGlass
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export type IconType = keyof typeof iconHash
|