@cdc/dashboard 4.24.7 → 4.24.9
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/dist/cdcdashboard.js +128394 -122305
- package/examples/single-state-dashboard-filters.json +421 -0
- package/examples/state-level.json +90136 -0
- package/examples/state-points.json +10474 -0
- package/examples/test-file.json +147 -0
- package/examples/testing.json +94456 -0
- package/index.html +18 -6
- package/package.json +9 -9
- package/src/CdcDashboardComponent.tsx +154 -90
- package/src/DashboardContext.tsx +7 -1
- package/src/_stories/Dashboard.stories.tsx +124 -10
- package/src/_stories/_mock/api-filter-map.json +1 -1
- package/src/_stories/_mock/bump-chart.json +3554 -0
- package/src/_stories/_mock/methodology.json +412 -0
- package/src/_stories/_mock/methodologyAPI.ts +90 -0
- package/src/_stories/_mock/multi-viz.json +1 -1
- package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
- package/src/components/DashboardFilters/DashboardFilters.tsx +39 -17
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +2 -2
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +141 -31
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +66 -18
- package/src/components/Header/Header.tsx +0 -5
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +20 -8
- package/src/components/Row.tsx +1 -1
- package/src/components/VisualizationRow.tsx +98 -17
- package/src/components/Widget.tsx +1 -0
- package/src/helpers/FilterBehavior.ts +4 -0
- package/src/helpers/addValuesToDashboardFilters.ts +49 -0
- package/src/helpers/apiFilterHelpers.ts +69 -18
- package/src/helpers/changeFilterActive.ts +16 -7
- package/src/helpers/getFilteredData.ts +4 -4
- package/src/helpers/iconHash.tsx +2 -0
- package/src/helpers/loadAPIFilters.ts +74 -0
- package/src/helpers/reloadURLHelpers.ts +13 -3
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
- package/src/helpers/tests/getFilteredData.test.ts +86 -0
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
- package/src/types/SharedFilter.ts +2 -1
|
@@ -27,10 +27,21 @@ type VisualizationWrapperProps = {
|
|
|
27
27
|
row: ConfigRow
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({
|
|
30
|
+
const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({
|
|
31
|
+
allExpanded,
|
|
32
|
+
currentViewport,
|
|
33
|
+
groupName,
|
|
34
|
+
row,
|
|
35
|
+
children
|
|
36
|
+
}) => {
|
|
31
37
|
return row.expandCollapseAllButtons ? (
|
|
32
38
|
<div className='collapsable-multiviz-container'>
|
|
33
|
-
<CollapsibleVisualizationRow
|
|
39
|
+
<CollapsibleVisualizationRow
|
|
40
|
+
allExpanded={allExpanded}
|
|
41
|
+
fontSize={'26px'}
|
|
42
|
+
groupName={groupName}
|
|
43
|
+
currentViewport={currentViewport}
|
|
44
|
+
>
|
|
34
45
|
{children}
|
|
35
46
|
</CollapsibleVisualizationRow>
|
|
36
47
|
</div>
|
|
@@ -54,7 +65,17 @@ type VizRowProps = {
|
|
|
54
65
|
currentViewport: ViewPort
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
const VisualizationRow: React.FC<VizRowProps> = ({
|
|
68
|
+
const VisualizationRow: React.FC<VizRowProps> = ({
|
|
69
|
+
allExpanded,
|
|
70
|
+
filteredDataOverride,
|
|
71
|
+
groupName,
|
|
72
|
+
row,
|
|
73
|
+
rowIndex: index,
|
|
74
|
+
setSharedFilter,
|
|
75
|
+
updateChildConfig,
|
|
76
|
+
apiFilterDropdowns,
|
|
77
|
+
currentViewport
|
|
78
|
+
}) => {
|
|
58
79
|
const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
|
|
59
80
|
const [show, setShow] = React.useState(row.columns.map((col, i) => i === 0))
|
|
60
81
|
const setToggled = (colIndex: number) => {
|
|
@@ -81,7 +102,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
81
102
|
}, [config, row, rawData, dashboardFilteredData])
|
|
82
103
|
|
|
83
104
|
const applyButtonNotClicked = (vizConfig: DashboardFilters): boolean => {
|
|
84
|
-
const dashboardFilters = Object.values(config.visualizations).filter(
|
|
105
|
+
const dashboardFilters = Object.values(config.visualizations).filter(
|
|
106
|
+
v => v.type === 'dashboardFilters'
|
|
107
|
+
) as DashboardFilters[]
|
|
85
108
|
const applyFilters = dashboardFilters.filter(v => !v.autoLoad).flatMap(v => v.sharedFilterIndexes)
|
|
86
109
|
if (hasDashboardApplyBehavior(config.visualizations) && vizConfig.autoLoad) {
|
|
87
110
|
return applyFilters.some(index => {
|
|
@@ -94,11 +117,16 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
94
117
|
return false
|
|
95
118
|
}
|
|
96
119
|
return (
|
|
97
|
-
<div
|
|
98
|
-
{row
|
|
120
|
+
<div
|
|
121
|
+
className={`row mb-5 ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`}
|
|
122
|
+
key={`row__${index}`}
|
|
123
|
+
>
|
|
124
|
+
{row.toggle && (
|
|
125
|
+
<Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />
|
|
126
|
+
)}
|
|
99
127
|
{row.columns.map((col, colIndex) => {
|
|
100
128
|
if (col.width) {
|
|
101
|
-
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col
|
|
129
|
+
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col col-${col.width}`}></div>
|
|
102
130
|
|
|
103
131
|
const visualizationConfig = getVizConfig(col.widget, index, config, rawData, dashboardFilteredData)
|
|
104
132
|
if (filteredDataOverride) {
|
|
@@ -108,22 +136,37 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
108
136
|
}
|
|
109
137
|
}
|
|
110
138
|
|
|
111
|
-
const setsSharedFilter =
|
|
112
|
-
|
|
139
|
+
const setsSharedFilter =
|
|
140
|
+
config.dashboard.sharedFilters &&
|
|
141
|
+
config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
|
|
142
|
+
const setSharedFilterValue = setsSharedFilter
|
|
143
|
+
? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active
|
|
144
|
+
: undefined
|
|
113
145
|
const tableLink = (
|
|
114
146
|
<a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
|
|
115
147
|
{visualizationConfig.dataKey} (Go to Table)
|
|
116
148
|
</a>
|
|
117
149
|
)
|
|
118
|
-
const hideFilter =
|
|
150
|
+
const hideFilter =
|
|
151
|
+
inNoDataState &&
|
|
152
|
+
visualizationConfig.type === 'dashboardFilters' &&
|
|
153
|
+
applyButtonNotClicked(visualizationConfig)
|
|
119
154
|
|
|
120
155
|
const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
|
|
121
156
|
|
|
122
157
|
const body = <></>
|
|
123
158
|
|
|
124
159
|
return (
|
|
125
|
-
<div
|
|
126
|
-
|
|
160
|
+
<div
|
|
161
|
+
key={`vis__${index}__${colIndex}`}
|
|
162
|
+
className={`p-1 col-12 col-md-${col.width} ${!shouldShow ? 'd-none' : ''}`}
|
|
163
|
+
>
|
|
164
|
+
<VisualizationWrapper
|
|
165
|
+
allExpanded={allExpanded}
|
|
166
|
+
currentViewport={currentViewport}
|
|
167
|
+
groupName={groupName}
|
|
168
|
+
row={row}
|
|
169
|
+
>
|
|
127
170
|
{visualizationConfig.type === 'chart' && (
|
|
128
171
|
<CdcChart
|
|
129
172
|
key={col.widget}
|
|
@@ -135,7 +178,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
135
178
|
}}
|
|
136
179
|
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
137
180
|
isDashboard={true}
|
|
138
|
-
link={
|
|
181
|
+
link={
|
|
182
|
+
config.table &&
|
|
183
|
+
config.table.show &&
|
|
184
|
+
config.datasets &&
|
|
185
|
+
visualizationConfig.table &&
|
|
186
|
+
visualizationConfig.table.showDataTableLink
|
|
187
|
+
? tableLink
|
|
188
|
+
: undefined
|
|
189
|
+
}
|
|
139
190
|
configUrl={undefined}
|
|
140
191
|
setEditing={undefined}
|
|
141
192
|
hostname={undefined}
|
|
@@ -154,7 +205,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
154
205
|
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
155
206
|
setSharedFilterValue={setSharedFilterValue}
|
|
156
207
|
isDashboard={true}
|
|
157
|
-
link={
|
|
208
|
+
link={
|
|
209
|
+
config.table &&
|
|
210
|
+
config.table.show &&
|
|
211
|
+
config.datasets &&
|
|
212
|
+
visualizationConfig.table &&
|
|
213
|
+
visualizationConfig.table.showDataTableLink
|
|
214
|
+
? tableLink
|
|
215
|
+
: undefined
|
|
216
|
+
}
|
|
158
217
|
/>
|
|
159
218
|
)}
|
|
160
219
|
{visualizationConfig.type === 'data-bite' && (
|
|
@@ -177,7 +236,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
177
236
|
updateChildConfig(col.widget, newConfig)
|
|
178
237
|
}}
|
|
179
238
|
isDashboard={true}
|
|
180
|
-
configUrl={
|
|
239
|
+
configUrl={
|
|
240
|
+
config.table &&
|
|
241
|
+
config.table.show &&
|
|
242
|
+
config.datasets &&
|
|
243
|
+
visualizationConfig.table &&
|
|
244
|
+
visualizationConfig.table.showDataTableLink
|
|
245
|
+
? tableLink
|
|
246
|
+
: undefined
|
|
247
|
+
}
|
|
181
248
|
/>
|
|
182
249
|
)}
|
|
183
250
|
{visualizationConfig.type === 'markup-include' && (
|
|
@@ -226,14 +293,28 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
|
|
|
226
293
|
viewport={currentViewport}
|
|
227
294
|
/>
|
|
228
295
|
)}
|
|
229
|
-
{visualizationConfig.type === 'footnotes' &&
|
|
296
|
+
{visualizationConfig.type === 'footnotes' && (
|
|
297
|
+
<FootnotesStandAlone
|
|
298
|
+
key={col.widget}
|
|
299
|
+
visualizationKey={col.widget}
|
|
300
|
+
config={visualizationConfig}
|
|
301
|
+
viewport={currentViewport}
|
|
302
|
+
/>
|
|
303
|
+
)}
|
|
230
304
|
</VisualizationWrapper>
|
|
231
305
|
</div>
|
|
232
306
|
)
|
|
233
307
|
}
|
|
234
308
|
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
235
309
|
})}
|
|
236
|
-
{row.footnotesId ?
|
|
310
|
+
{row.footnotesId ? (
|
|
311
|
+
<FootnotesStandAlone
|
|
312
|
+
isEditor={false}
|
|
313
|
+
visualizationKey={row.footnotesId}
|
|
314
|
+
config={footnotesConfig}
|
|
315
|
+
viewport={currentViewport}
|
|
316
|
+
/>
|
|
317
|
+
) : null}
|
|
237
318
|
</div>
|
|
238
319
|
)
|
|
239
320
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
|
|
5
|
+
// Gets filter values from dataset
|
|
6
|
+
const generateValuesForFilter = (columnName, data: Record<string, any[]>) => {
|
|
7
|
+
const values: string[] = []
|
|
8
|
+
// data is a dataset this loops through ALL datasets to find matching values
|
|
9
|
+
// not sure if this is desired behavior
|
|
10
|
+
|
|
11
|
+
const d = Object.values(data) || []
|
|
12
|
+
d.forEach((rows: any[]) => {
|
|
13
|
+
rows?.forEach(row => {
|
|
14
|
+
const value = row[columnName]
|
|
15
|
+
if (value !== undefined && !values.includes(value)) {
|
|
16
|
+
values.push(String(value))
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
return values
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getSelector = (filter: SharedFilter) => {
|
|
24
|
+
return filter.type === 'urlfilter' ? filter.apiFilter?.valueSelector : filter.columnName
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const addValuesToDashboardFilters = (filters: SharedFilter[], data: Record<string, any[]>): Array<SharedFilter> => {
|
|
28
|
+
return filters?.map(filter => {
|
|
29
|
+
if (filter.type === 'urlfilter') return filter
|
|
30
|
+
const filterCopy = _.cloneDeep(filter)
|
|
31
|
+
const filterValues = generateValuesForFilter(getSelector(filter), data)
|
|
32
|
+
filterCopy.values = filterValues
|
|
33
|
+
if (filterValues.length > 0) {
|
|
34
|
+
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
35
|
+
if (queryStringFilterValue) {
|
|
36
|
+
filterCopy.active = queryStringFilterValue
|
|
37
|
+
} else if (filter.multiSelect) {
|
|
38
|
+
const defaultValues = filterCopy.values
|
|
39
|
+
const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
40
|
+
filterCopy.active = active.filter(val => defaultValues.includes(val))
|
|
41
|
+
} else {
|
|
42
|
+
const defaultValue = filterCopy.values[0] || filterCopy.active
|
|
43
|
+
const active = Array.isArray(filterCopy.active) ? filterCopy.active[0] : filterCopy.active
|
|
44
|
+
filterCopy.active = filterCopy.values.includes(active) ? active : defaultValue
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return filterCopy
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -2,26 +2,42 @@ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
|
|
|
2
2
|
import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
|
|
3
3
|
import { APIFilter } from '../types/APIFilter'
|
|
4
4
|
import { SharedFilter } from '../types/SharedFilter'
|
|
5
|
+
import _ from 'lodash'
|
|
6
|
+
import { getQueryParams } from '@cdc/core/helpers/queryStringUtils'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/** key for the dropdowns object */
|
|
9
|
+
type DropdownsKey = string
|
|
10
|
+
|
|
11
|
+
export const getLoadingFilterMemo = (
|
|
12
|
+
apiFiltersEndpoints: string[],
|
|
13
|
+
apiFilterDropdowns,
|
|
14
|
+
changedChildFilterIndexes = []
|
|
15
|
+
): APIFilterDropdowns =>
|
|
16
|
+
apiFiltersEndpoints.reduce((acc, endpoint, currIndex) => {
|
|
17
|
+
const _key: DropdownsKey = endpoint
|
|
18
|
+
const hasChanged = changedChildFilterIndexes.includes(currIndex)
|
|
19
|
+
if (apiFilterDropdowns[_key] != null && !hasChanged) {
|
|
20
|
+
acc[_key] = apiFilterDropdowns[_key]
|
|
21
|
+
} else {
|
|
22
|
+
acc[_key] = null
|
|
23
|
+
}
|
|
11
24
|
return acc
|
|
12
25
|
}, {})
|
|
13
26
|
|
|
14
|
-
const getParentParams = (
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
const getParentParams = (
|
|
28
|
+
childFilter: SharedFilter,
|
|
29
|
+
sharedFilters: SharedFilter[]
|
|
30
|
+
): Record<'key' | 'value', string>[] | null => {
|
|
31
|
+
const _parents = sharedFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
|
|
32
|
+
if (!(_parents || []).length) return null
|
|
17
33
|
|
|
18
34
|
return _parents.flatMap(filter => {
|
|
19
35
|
const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
|
|
20
36
|
const value = filter.queuedActive || filter.active || ''
|
|
21
37
|
if (Array.isArray(value)) {
|
|
22
|
-
return value.map(_value => ({ key, value: _value }))
|
|
38
|
+
return value.map(_value => ({ key, value: _value.toString() }))
|
|
23
39
|
}
|
|
24
|
-
return [{ key, value }]
|
|
40
|
+
return [{ key, value: value.toString() }]
|
|
25
41
|
})
|
|
26
42
|
}
|
|
27
43
|
|
|
@@ -30,22 +46,57 @@ export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): Drop
|
|
|
30
46
|
return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
|
|
31
47
|
}
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
/** API endpoint to fetch */
|
|
50
|
+
type Endpoint = string
|
|
51
|
+
type SharedFilterIndex = number
|
|
52
|
+
export const getToFetch = (
|
|
53
|
+
sharedFilters: SharedFilter[],
|
|
54
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
55
|
+
): Record<Endpoint, [DropdownsKey, SharedFilterIndex]> => {
|
|
34
56
|
const toFetch = {}
|
|
35
|
-
|
|
36
|
-
const baseEndpoint = filter.apiFilter
|
|
57
|
+
sharedFilters.forEach((filter, index) => {
|
|
58
|
+
const baseEndpoint = filter.apiFilter?.apiEndpoint
|
|
59
|
+
if (!baseEndpoint) return
|
|
37
60
|
const _key = baseEndpoint
|
|
38
|
-
|
|
39
|
-
const parentParams = getParentParams(filter,
|
|
61
|
+
if (apiFilterDropdowns[_key]) return // don't reload cached filter
|
|
62
|
+
const parentParams = getParentParams(filter, sharedFilters)
|
|
40
63
|
const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
|
|
41
64
|
|
|
42
65
|
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
43
|
-
if (isAPIFilter && !parentParams) return // don't reload filter unless it's a child
|
|
44
|
-
const topLevelDataAlreadyLoaded = isAPIFilter && !filter.parents
|
|
45
|
-
if (topLevelDataAlreadyLoaded) return // don't reload top level filters
|
|
46
66
|
|
|
47
67
|
const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
|
|
48
68
|
toFetch[endpoint] = [_key, index]
|
|
49
69
|
})
|
|
50
70
|
return toFetch
|
|
51
71
|
}
|
|
72
|
+
|
|
73
|
+
export const setAutoLoadDefaultValue = (
|
|
74
|
+
sharedFilterIndex: number,
|
|
75
|
+
dropdownOptions: DropdownOptions,
|
|
76
|
+
sharedFilters,
|
|
77
|
+
autoLoadFilterIndexes: number[]
|
|
78
|
+
): SharedFilter => {
|
|
79
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
80
|
+
const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
|
|
81
|
+
if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) return sharedFilter // no autoLoading happening
|
|
82
|
+
if (autoLoadFilterIndexes.includes(sharedFilterIndex)) {
|
|
83
|
+
const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
|
|
84
|
+
const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
|
|
85
|
+
if (filterParents && notAllParentFiltersSelected) return sharedFilter
|
|
86
|
+
const defaultValue = sharedFilter.multiSelect ? [dropdownOptions[0]?.value] : dropdownOptions[0]?.value
|
|
87
|
+
if (!sharedFilter.active) {
|
|
88
|
+
const queryParams = getQueryParams()
|
|
89
|
+
const defaultQueryParamValue = queryParams[sharedFilter?.queryParameter]
|
|
90
|
+
sharedFilter.active = defaultQueryParamValue || defaultValue
|
|
91
|
+
} else if (sharedFilter.multiSelect) {
|
|
92
|
+
const currentOption = sharedFilter.active.filter(activeVal =>
|
|
93
|
+
dropdownOptions.find(option => option.value === activeVal)
|
|
94
|
+
)
|
|
95
|
+
sharedFilter.active = currentOption.length ? currentOption : defaultValue
|
|
96
|
+
} else {
|
|
97
|
+
const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
|
|
98
|
+
sharedFilter.active = currentOption ? currentOption.value : defaultValue
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return sharedFilter
|
|
102
|
+
}
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
|
-
import { FilterBehavior } from '../
|
|
2
|
+
import { FilterBehavior } from '../helpers/FilterBehavior'
|
|
3
3
|
import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
|
|
4
4
|
import { SharedFilter } from '../types/SharedFilter'
|
|
5
5
|
import { DashboardFilters } from '../types/DashboardFilters'
|
|
6
6
|
|
|
7
7
|
const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
|
|
8
8
|
const parentKey = sharedFilters[parentIndex].key
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const childFilterIndexes = sharedFilters
|
|
10
|
+
.map((filter, index) => (filter.parents?.includes(parentKey) ? index : null))
|
|
11
|
+
.filter(i => i !== null)
|
|
12
|
+
if (childFilterIndexes.length) {
|
|
13
|
+
childFilterIndexes.forEach(filterIndex => {
|
|
14
|
+
sharedFilters[filterIndex].active = ''
|
|
15
|
+
})
|
|
12
16
|
}
|
|
17
|
+
return childFilterIndexes
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
export const changeFilterActive = (
|
|
20
|
+
export const changeFilterActive = (
|
|
21
|
+
filterIndex: number,
|
|
22
|
+
value: string | string[],
|
|
23
|
+
sharedFilters: SharedFilter[],
|
|
24
|
+
vizConfig: DashboardFilters
|
|
25
|
+
): [SharedFilter[], number[]] => {
|
|
16
26
|
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
17
27
|
const currentFilter = sharedFilters[filterIndex]
|
|
18
28
|
if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
|
|
19
29
|
sharedFiltersCopy[filterIndex].active = value
|
|
20
|
-
handleChildren(sharedFiltersCopy, filterIndex)
|
|
21
30
|
const queryParams = getQueryParams()
|
|
22
31
|
if (currentFilter.setByQueryParameter && queryParams[currentFilter.setByQueryParameter] !== currentFilter.active) {
|
|
23
32
|
queryParams[currentFilter.setByQueryParameter] = currentFilter.active
|
|
@@ -26,5 +35,5 @@ export const changeFilterActive = (filterIndex: number, value: string | string[]
|
|
|
26
35
|
} else {
|
|
27
36
|
sharedFiltersCopy[filterIndex].queuedActive = value
|
|
28
37
|
}
|
|
29
|
-
return sharedFiltersCopy
|
|
38
|
+
return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
|
|
30
39
|
}
|
|
@@ -10,14 +10,14 @@ export const getApplicableFilters = (dashboard: Dashboard, key: string | number)
|
|
|
10
10
|
return c?.length > 0 ? c : false
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export const getFilteredData = (state: DashboardState, initialFilteredData
|
|
14
|
-
const newFilteredData = initialFilteredData
|
|
13
|
+
export const getFilteredData = (state: DashboardState, initialFilteredData?: Record<string, any>, dataOverride?: Object) => {
|
|
14
|
+
const newFilteredData = initialFilteredData || {}
|
|
15
15
|
const { config } = state
|
|
16
16
|
getVizKeys(config).forEach(key => {
|
|
17
17
|
const applicableFilters = getApplicableFilters(config.dashboard, key)
|
|
18
18
|
if (applicableFilters) {
|
|
19
19
|
const { dataKey, data, dataDescription } = config.visualizations[key]
|
|
20
|
-
const _data = state.data[dataKey] || data
|
|
20
|
+
const _data = (dataOverride || state.data)[dataKey] || data
|
|
21
21
|
const formattedData = dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
|
|
22
22
|
|
|
23
23
|
newFilteredData[key] = filterData(applicableFilters, formattedData)
|
|
@@ -27,7 +27,7 @@ export const getFilteredData = (state: DashboardState, initialFilteredData = {},
|
|
|
27
27
|
if (row.dataKey) {
|
|
28
28
|
const applicableFilters = getApplicableFilters(config.dashboard, index)
|
|
29
29
|
const { dataKey, data, dataDescription } = row
|
|
30
|
-
const _data = state.data[dataKey] || data
|
|
30
|
+
const _data = (dataOverride || state.data)[dataKey] || data
|
|
31
31
|
if (applicableFilters) {
|
|
32
32
|
const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
|
|
33
33
|
|
package/src/helpers/iconHash.tsx
CHANGED
|
@@ -5,6 +5,7 @@ export const iconHash = {
|
|
|
5
5
|
'data-bite': <Icon display='databite' base />,
|
|
6
6
|
Bar: <Icon display='chartBar' base />,
|
|
7
7
|
'Spark Line': <Icon display='chartLine' />,
|
|
8
|
+
'Bump Chart': <Icon display='chartLine' />,
|
|
8
9
|
'waffle-chart': <Icon display='grid' base />,
|
|
9
10
|
'markup-include': <Icon display='code' base />,
|
|
10
11
|
Line: <Icon display='chartLine' base />,
|
|
@@ -14,6 +15,7 @@ export const iconHash = {
|
|
|
14
15
|
world: <Icon display='mapWorld' base />,
|
|
15
16
|
'single-state': <Icon display='mapAl' base />,
|
|
16
17
|
gear: <Icon display='gear' base />,
|
|
18
|
+
gearMulti: <Icon display='gearMulti' base />,
|
|
17
19
|
tools: <Icon display='tools' base />,
|
|
18
20
|
'filtered-text': <Icon display='filtered-text' base />,
|
|
19
21
|
dashboardFilters: <Icon display='dashboardFilters' base />,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { APIFilterDropdowns } from '../components/DashboardFilters'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
import * as apiFilterHelpers from './apiFilterHelpers'
|
|
5
|
+
import { APIFilter } from '../types/APIFilter'
|
|
6
|
+
|
|
7
|
+
export const loadAPIFiltersFactory = (
|
|
8
|
+
dispatch: Function,
|
|
9
|
+
setAPIFilterDropdowns: Function,
|
|
10
|
+
autoLoadFilterIndexes: number[]
|
|
11
|
+
) => {
|
|
12
|
+
const loadAPIFilters = (
|
|
13
|
+
sharedFilters: SharedFilter[],
|
|
14
|
+
dropdowns: APIFilterDropdowns,
|
|
15
|
+
recursiveLimit = 3
|
|
16
|
+
): Promise<SharedFilter[]> => {
|
|
17
|
+
if (!sharedFilters) return
|
|
18
|
+
sharedFilters = sharedFilters.map((filter, index) =>
|
|
19
|
+
apiFilterHelpers.setAutoLoadDefaultValue(
|
|
20
|
+
index,
|
|
21
|
+
dropdowns[filter.apiFilter?.apiEndpoint],
|
|
22
|
+
sharedFilters,
|
|
23
|
+
autoLoadFilterIndexes
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
|
|
27
|
+
const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
|
|
28
|
+
const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
|
|
29
|
+
const newDropdowns = _.cloneDeep(dropdowns)
|
|
30
|
+
return Promise.all(
|
|
31
|
+
Object.keys(toFetch).map(
|
|
32
|
+
endpoint =>
|
|
33
|
+
new Promise<void>(resolve => {
|
|
34
|
+
fetch(endpoint)
|
|
35
|
+
.then(resp => resp.json())
|
|
36
|
+
.then(data => {
|
|
37
|
+
if (!Array.isArray(data)) {
|
|
38
|
+
console.error('COVE only supports response data in the shape Array<Object>')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const [_key, index] = toFetch[endpoint]
|
|
42
|
+
const apiFilter = filterLookup.get(_key) as APIFilter
|
|
43
|
+
const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
|
|
44
|
+
newDropdowns[_key] = _filterValues
|
|
45
|
+
const newDefaultSelectedFilter = apiFilterHelpers.setAutoLoadDefaultValue(
|
|
46
|
+
index,
|
|
47
|
+
_filterValues,
|
|
48
|
+
sharedFilters,
|
|
49
|
+
autoLoadFilterIndexes
|
|
50
|
+
)
|
|
51
|
+
sharedFilters[index] = newDefaultSelectedFilter
|
|
52
|
+
})
|
|
53
|
+
.catch(console.error)
|
|
54
|
+
.finally(() => {
|
|
55
|
+
resolve()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
).then(() => {
|
|
60
|
+
const finishedLoading = sharedFilters.reduce((acc, curr, index) => {
|
|
61
|
+
if (autoLoadFilterIndexes.includes(index) && !curr.active) return false
|
|
62
|
+
return acc
|
|
63
|
+
}, true)
|
|
64
|
+
if (finishedLoading || recursiveLimit === 0) {
|
|
65
|
+
setAPIFilterDropdowns(newDropdowns)
|
|
66
|
+
dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
|
|
67
|
+
return sharedFilters
|
|
68
|
+
} else {
|
|
69
|
+
return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
return loadAPIFilters
|
|
74
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
|
|
2
2
|
import { SharedFilter } from '../types/SharedFilter'
|
|
3
3
|
import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
|
|
4
|
-
import { Visualization } from '@cdc/core/types/Visualization'
|
|
4
|
+
import { AnyVisualization, Visualization } from '@cdc/core/types/Visualization'
|
|
5
5
|
import _ from 'lodash'
|
|
6
6
|
|
|
7
7
|
export const isUpdateNeeded = (filters: SharedFilter[], currentQueryParams: Record<string, string>, newQueryParams: Record<string, string>): boolean => {
|
|
@@ -20,8 +20,12 @@ export const isUpdateNeeded = (filters: SharedFilter[], currentQueryParams: Reco
|
|
|
20
20
|
return needsUpdate
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const getDataURL = (updatedQSParams: Record<string, string>, dataUrl: URL, newFileName: string) => {
|
|
24
|
-
const _params = Object.keys(updatedQSParams).
|
|
23
|
+
export const getDataURL = (updatedQSParams: Record<string, string | string[]>, dataUrl: URL, newFileName: string) => {
|
|
24
|
+
const _params = Object.keys(updatedQSParams).flatMap(key => {
|
|
25
|
+
const value = updatedQSParams[key]
|
|
26
|
+
if (Array.isArray(value)) return value.map(v => ({ key, value: v }))
|
|
27
|
+
return { key, value }
|
|
28
|
+
})
|
|
25
29
|
const baseURL = dataUrl.origin + dataUrl.pathname
|
|
26
30
|
let dataUrlFinal = `${baseURL}${gatherQueryParams(baseURL, _params)}`
|
|
27
31
|
|
|
@@ -66,3 +70,9 @@ export const getVisualizationsWithFormattedData = (visualizations: Record<string
|
|
|
66
70
|
return acc
|
|
67
71
|
}, _.cloneDeep(visualizations))
|
|
68
72
|
}
|
|
73
|
+
|
|
74
|
+
export const filterUsedByDataUrl = (filter: SharedFilter, datasetKey: string, visualizations: Record<string, AnyVisualization>) => {
|
|
75
|
+
if (!filter.usedBy || !filter.usedBy.length) return true
|
|
76
|
+
const vizUsingFilters = filter.usedBy?.map(vizKey => visualizations[vizKey])
|
|
77
|
+
return vizUsingFilters?.some(viz => viz?.dataKey === datasetKey)
|
|
78
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SharedFilter } from '../../types/SharedFilter'
|
|
2
|
+
import { addValuesToDashboardFilters } from '../addValuesToDashboardFilters'
|
|
3
|
+
|
|
4
|
+
describe('addValuesToDashboardFilters', () => {
|
|
5
|
+
const colA = { columnName: 'colA', id: 11, active: 'apple', values: [], type: 'datafilter' } as SharedFilter
|
|
6
|
+
const colB = { columnName: 'colB', id: 22, active: '1', values: [], type: 'datafilter' } as SharedFilter
|
|
7
|
+
const colC = { columnName: 'colC', id: 33, values: [], setByQueryParameter: 'colC', type: 'datafilter' } as SharedFilter
|
|
8
|
+
|
|
9
|
+
const data = {
|
|
10
|
+
key: [
|
|
11
|
+
{ colA: 'apple', colB: 3, colC: 'abc' },
|
|
12
|
+
{ colA: 'apple', colB: 1, colC: 'bcd' },
|
|
13
|
+
{ colA: 'pear', colB: 4, colC: 'test' }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
const filters = [colA, colC, colB]
|
|
17
|
+
it('adds filter values', () => {
|
|
18
|
+
const newFilters = addValuesToDashboardFilters(filters, data)
|
|
19
|
+
expect(newFilters[0].values).toEqual(['apple', 'pear'])
|
|
20
|
+
})
|
|
21
|
+
it('converts to multiselect', () => {
|
|
22
|
+
colA.multiSelect = true
|
|
23
|
+
const newFilters = addValuesToDashboardFilters(filters, data)
|
|
24
|
+
expect(newFilters[0].active).toEqual(['apple'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('sets active value by query string', () => {
|
|
28
|
+
delete window.location
|
|
29
|
+
window.location = new URL('https://www.example.com?colC=test')
|
|
30
|
+
const newFilters = addValuesToDashboardFilters(filters, data)
|
|
31
|
+
expect(newFilters[1].active).toEqual('test')
|
|
32
|
+
})
|
|
33
|
+
const colA2 = { apiFilter: { valueSelector: 'colA' }, id: 11, active: 'apple', values: [], type: 'urlfilter' } as SharedFilter
|
|
34
|
+
const colB2 = { apiFilter: { valueSelector: 'colB' }, id: 22, active: '1', values: [], type: 'urlfilter' } as SharedFilter
|
|
35
|
+
const colC2 = { apiFilter: { valueSelector: 'colC' }, id: 33, values: [], setByQueryParameter: 'colC', type: 'urlfilter' } as SharedFilter
|
|
36
|
+
const filters2 = [colA2, colC2, colB2]
|
|
37
|
+
it('skips urlfilters', () => {
|
|
38
|
+
// urlfilter reloading happens in the dashboard in the loadAPIFilters function
|
|
39
|
+
delete window.location
|
|
40
|
+
window.location = new URL('https://www.example.com?colC=test')
|
|
41
|
+
const newFilters = addValuesToDashboardFilters(filters2, data)
|
|
42
|
+
expect(newFilters[1].active).toEqual(undefined)
|
|
43
|
+
})
|
|
44
|
+
})
|