@cdc/dashboard 4.24.5 → 4.24.9-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/dist/cdcdashboard.js +147572 -128223
- package/examples/custom/css/respiratory.css +236 -0
- package/examples/custom/js/respiratory.js +242 -0
- package/examples/default-multi-dataset-shared-filter.json +1729 -0
- package/examples/ed-visits-county-file.json +618 -0
- package/examples/filtered-dash.json +6 -21
- 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 +25 -4
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +250 -327
- package/src/DashboardContext.tsx +15 -1
- package/src/_stories/Dashboard.stories.tsx +158 -40
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- 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 +3 -4
- package/src/_stories/_mock/pivot-filter.json +14 -12
- package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
- package/src/components/DashboardFilters/index.ts +3 -0
- package/src/components/DataDesignerModal.tsx +9 -9
- package/src/components/ExpandCollapseButtons.tsx +20 -0
- package/src/components/Header/Header.tsx +1 -102
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +169 -30
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
- package/src/components/VisualizationsPanel/index.ts +1 -0
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
- package/src/components/Widget.tsx +27 -90
- package/src/helpers/FilterBehavior.ts +4 -0
- package/src/helpers/addValuesToDashboardFilters.ts +49 -0
- package/src/helpers/apiFilterHelpers.ts +103 -0
- package/src/helpers/changeFilterActive.ts +39 -0
- package/src/helpers/filterData.ts +10 -48
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +7 -5
- package/src/helpers/getVizConfig.ts +23 -2
- package/src/helpers/getVizRowColumnLocator.ts +2 -1
- package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
- package/src/helpers/iconHash.tsx +5 -3
- package/src/helpers/loadAPIFilters.ts +74 -0
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +102 -0
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +155 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- package/src/helpers/tests/getFilteredData.test.ts +86 -0
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +220 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +232 -0
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/grid.scss +34 -27
- package/src/scss/main.scss +41 -3
- package/src/scss/variables.scss +4 -0
- package/src/store/dashboard.actions.ts +12 -4
- package/src/store/dashboard.reducer.ts +30 -4
- package/src/types/APIFilter.ts +1 -5
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +2 -4
- package/src/types/DashboardFilters.ts +7 -0
- package/src/types/InitialState.ts +1 -1
- package/src/types/MultiDashboard.ts +2 -2
- package/src/types/SharedFilter.ts +4 -6
- package/src/types/Tab.ts +1 -1
- package/src/components/Filters.tsx +0 -88
- package/src/components/Header/FilterModal.tsx +0 -510
- package/src/components/VisualizationsPanel.tsx +0 -95
- package/src/helpers/getApiFilterKey.ts +0 -5
|
@@ -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)
|
|
@@ -26,12 +26,14 @@ export const getFilteredData = (state: DashboardState, initialFilteredData = {},
|
|
|
26
26
|
config.rows.forEach((row, index) => {
|
|
27
27
|
if (row.dataKey) {
|
|
28
28
|
const applicableFilters = getApplicableFilters(config.dashboard, index)
|
|
29
|
+
const { dataKey, data, dataDescription } = row
|
|
30
|
+
const _data = (dataOverride || state.data)[dataKey] || data
|
|
29
31
|
if (applicableFilters) {
|
|
30
|
-
const { dataKey, data, dataDescription } = row
|
|
31
|
-
const _data = state.data[dataKey] || data
|
|
32
32
|
const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
|
|
33
33
|
|
|
34
34
|
newFilteredData[index] = filterData(applicableFilters, formattedData)
|
|
35
|
+
} else {
|
|
36
|
+
newFilteredData[index] = _data || []
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
})
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
import { MultiDashboardConfig } from '../types/MultiDashboard'
|
|
3
3
|
import DataTransform from '@cdc/core/helpers/DataTransform'
|
|
4
|
+
import { getApplicableFilters } from './getFilteredData'
|
|
5
|
+
import { filterData } from './filterData'
|
|
6
|
+
import Footnotes from '@cdc/core/types/Footnotes'
|
|
4
7
|
|
|
5
8
|
const transform = new DataTransform()
|
|
6
9
|
|
|
10
|
+
export const getFootnotesVizConfig = (vizKey: string, rowNumber: number, config: MultiDashboardConfig) => {
|
|
11
|
+
const visualizationConfig = _.cloneDeep(config.visualizations[vizKey])
|
|
12
|
+
|
|
13
|
+
const data = config.datasets[visualizationConfig.dataKey]?.data
|
|
14
|
+
const dataColumns = data?.length ? Object.keys(data[0]) : []
|
|
15
|
+
const filters = (getApplicableFilters(config.dashboard, rowNumber) || []).filter(filter => dataColumns.includes(filter.columnName))
|
|
16
|
+
if (filters.length) {
|
|
17
|
+
visualizationConfig.formattedData = filterData(filters, data)
|
|
18
|
+
}
|
|
19
|
+
visualizationConfig.data = data
|
|
20
|
+
return visualizationConfig as Footnotes
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
export const getVizConfig = (visualizationKey: string, rowNumber: number, config: MultiDashboardConfig, data: Object, filteredData?: Object) => {
|
|
24
|
+
if (rowNumber === undefined) return {}
|
|
8
25
|
const visualizationConfig = _.cloneDeep(config.visualizations[visualizationKey])
|
|
9
26
|
const rowData = config.rows[rowNumber]
|
|
27
|
+
if (rowData.footnotesId && rowData.footnotesId === visualizationKey) {
|
|
28
|
+
// return the footnotes visualization config with filtered data
|
|
29
|
+
return getFootnotesVizConfig(visualizationKey, rowNumber, config)
|
|
30
|
+
}
|
|
10
31
|
if (rowData?.dataKey) {
|
|
11
32
|
// data configured on the row
|
|
12
33
|
Object.assign(visualizationConfig, _.pick(rowData, ['dataKey', 'dataDescription', 'formattedData', 'data']))
|
|
@@ -16,13 +37,13 @@ export const getVizConfig = (visualizationKey: string, rowNumber: number, config
|
|
|
16
37
|
const filteredVizData = filteredData?.[rowNumber] ?? filteredData?.[visualizationKey]
|
|
17
38
|
|
|
18
39
|
if (filteredVizData) {
|
|
19
|
-
visualizationConfig.data = filteredVizData
|
|
40
|
+
visualizationConfig.data = filteredVizData || []
|
|
20
41
|
if (visualizationConfig.formattedData) {
|
|
21
42
|
visualizationConfig.formattedData = visualizationConfig.data
|
|
22
43
|
}
|
|
23
44
|
} else {
|
|
24
45
|
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
25
|
-
visualizationConfig.data = data[dataKey]
|
|
46
|
+
visualizationConfig.data = data[dataKey] || []
|
|
26
47
|
if (visualizationConfig.formattedData) {
|
|
27
48
|
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
28
49
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ConfigRow } from '../types/ConfigRow'
|
|
2
2
|
|
|
3
|
-
export const getVizRowColumnLocator = (rows: ConfigRow[]) =>
|
|
3
|
+
export const getVizRowColumnLocator = (rows: ConfigRow[]): Record<string, { row: number; column: number }> =>
|
|
4
4
|
rows.reduce((acc, curr, index) => {
|
|
5
5
|
curr.columns?.forEach((column, columnIndex) => {
|
|
6
6
|
if (column.widget !== undefined) acc[column.widget] = { row: index, column: columnIndex }
|
|
7
7
|
})
|
|
8
|
+
if (curr.footnotesId) acc[curr.footnotesId] = { row: index, column: 0 }
|
|
8
9
|
return acc
|
|
9
10
|
}, {})
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
2
|
+
|
|
3
|
+
export const hasDashboardApplyBehavior = (visualizations: Record<string, AnyVisualization>) => {
|
|
4
|
+
return Object.values(visualizations).some(v => v.filterBehavior === 'Apply Button' && v.type === 'dashboardFilters')
|
|
5
|
+
}
|
package/src/helpers/iconHash.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
2
|
-
import {
|
|
2
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
3
|
|
|
4
4
|
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,14 +15,15 @@ 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 />,
|
|
20
22
|
table: <Icon display='table' base />,
|
|
21
23
|
Sankey: <Icon display='sankey' base />
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export const getIcon = (visualization:
|
|
26
|
+
export const getIcon = (visualization: AnyVisualization) => {
|
|
25
27
|
const { type, visualizationType, general } = visualization
|
|
26
28
|
if (visualizationType) return iconHash[visualizationType]
|
|
27
29
|
if (general?.geoType) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getFormattedData } from './getFormattedData'
|
|
2
|
+
import { DashboardConfig } from '../types/DashboardConfig'
|
|
3
|
+
|
|
4
|
+
const mapDataToVisualizations = (config: DashboardConfig) => {
|
|
5
|
+
Object.keys(config.visualizations).forEach((vizKey, i) => {
|
|
6
|
+
const viz = config.visualizations[vizKey]
|
|
7
|
+
if (viz.dataKey && !viz.data) {
|
|
8
|
+
const data = config.datasets[viz.dataKey].data
|
|
9
|
+
config.visualizations[vizKey].data = data
|
|
10
|
+
config.visualizations[vizKey].formattedData = getFormattedData(data, viz.dataDescription)
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const mapDataToRows = (config: DashboardConfig) => {
|
|
16
|
+
config.rows.forEach((row, i) => {
|
|
17
|
+
if (row.dataKey && !row.data) {
|
|
18
|
+
const data = config.datasets[row.dataKey].data
|
|
19
|
+
config.rows[i].data = data
|
|
20
|
+
config.rows[i].formattedData = getFormattedData(data, row.dataDescription)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const mapDataToConfig = (config: DashboardConfig) => {
|
|
26
|
+
mapDataToVisualizations(config)
|
|
27
|
+
mapDataToRows(config)
|
|
28
|
+
return config
|
|
29
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { FilterBehavior } from '../components/Header/Header'
|
|
2
1
|
import { DataSet } from '../types/DataSet'
|
|
3
2
|
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
4
3
|
import { getFormattedData } from './getFormattedData'
|
|
5
4
|
|
|
6
|
-
export const processData = async (dataSet: DataSet,
|
|
7
|
-
if (dataSet.dataUrl &&
|
|
5
|
+
export const processData = async (dataSet: DataSet, hasFilterChangeBehavior: boolean) => {
|
|
6
|
+
if (dataSet.dataUrl && hasFilterChangeBehavior) {
|
|
8
7
|
const dataset = await fetchRemoteData(`${dataSet.dataUrl}`)
|
|
9
8
|
return getFormattedData(dataset, dataSet.dataDescription)
|
|
10
9
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
|
|
2
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
3
|
+
import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
|
|
4
|
+
import { AnyVisualization, Visualization } from '@cdc/core/types/Visualization'
|
|
5
|
+
import _ from 'lodash'
|
|
6
|
+
|
|
7
|
+
export const isUpdateNeeded = (
|
|
8
|
+
filters: SharedFilter[],
|
|
9
|
+
currentQueryParams: Record<string, string>,
|
|
10
|
+
newQueryParams: Record<string, string>
|
|
11
|
+
): boolean => {
|
|
12
|
+
let needsUpdate = false
|
|
13
|
+
filters.find(filter => {
|
|
14
|
+
if (filter.type === 'urlfilter' && !Array.isArray(filter.active) && filter.filterBy === 'File Name') {
|
|
15
|
+
needsUpdate = true
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
Object.keys(newQueryParams).forEach(updatedParam => {
|
|
20
|
+
if (decodeURIComponent(newQueryParams[updatedParam]) !== currentQueryParams[updatedParam]) {
|
|
21
|
+
needsUpdate = true
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
return needsUpdate
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getDataURL = (updatedQSParams: Record<string, string | string[]>, dataUrl: URL, newFileName: string) => {
|
|
28
|
+
const _params = Object.keys(updatedQSParams).flatMap(key => {
|
|
29
|
+
const value = updatedQSParams[key]
|
|
30
|
+
if (value === undefined) return []
|
|
31
|
+
if (typeof value === 'string' && (value as String).match(/undefined/)) return []
|
|
32
|
+
if (Array.isArray(value)) return value.map(v => ({ key, value: v }))
|
|
33
|
+
return { key, value }
|
|
34
|
+
})
|
|
35
|
+
const baseURL = dataUrl.origin + dataUrl.pathname
|
|
36
|
+
let dataUrlFinal = `${baseURL}${gatherQueryParams(baseURL, _params)}`
|
|
37
|
+
|
|
38
|
+
if (newFileName !== '') {
|
|
39
|
+
const fileExtension = dataUrl.pathname.split('.').pop()
|
|
40
|
+
const pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
|
|
41
|
+
dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(
|
|
42
|
+
baseURL,
|
|
43
|
+
_params
|
|
44
|
+
)}`
|
|
45
|
+
}
|
|
46
|
+
return dataUrlFinal
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const getNewFileName = (newFileName: string, filter: SharedFilter, datasetKey: string) => {
|
|
50
|
+
const replacements = {
|
|
51
|
+
'Remove Spaces': '',
|
|
52
|
+
'Keep Spaces': ' ',
|
|
53
|
+
'Replace With Underscore': '_'
|
|
54
|
+
}
|
|
55
|
+
let fileName = newFileName
|
|
56
|
+
if (filter.datasetKey === datasetKey) {
|
|
57
|
+
if (filter.fileName) {
|
|
58
|
+
// if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
|
|
59
|
+
fileName = capitalizeSplitAndJoin.call(
|
|
60
|
+
String(filter.fileName),
|
|
61
|
+
' ',
|
|
62
|
+
replacements[filter.whitespaceReplacement ?? 'Keep Spaces']
|
|
63
|
+
)
|
|
64
|
+
} else {
|
|
65
|
+
// if no file name is entered use the default active filter. ie. /activeFilter.json
|
|
66
|
+
fileName = filter.active as string
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fileName?.includes('${query}')) {
|
|
71
|
+
fileName = fileName.replace(
|
|
72
|
+
'${query}',
|
|
73
|
+
capitalizeSplitAndJoin.call(
|
|
74
|
+
String(filter.active),
|
|
75
|
+
' ',
|
|
76
|
+
replacements[filter.whitespaceReplacement ?? 'Keep Spaces']
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return fileName
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const getVisualizationsWithFormattedData = (visualizations: Record<string, Visualization>, newData: Object) => {
|
|
85
|
+
return Object.keys(visualizations).reduce((acc, vizKey) => {
|
|
86
|
+
const dataKey = visualizations[vizKey].dataKey
|
|
87
|
+
if (newData[dataKey]) {
|
|
88
|
+
acc[vizKey].formattedData = newData[dataKey]
|
|
89
|
+
}
|
|
90
|
+
return acc
|
|
91
|
+
}, _.cloneDeep(visualizations))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const filterUsedByDataUrl = (
|
|
95
|
+
filter: SharedFilter,
|
|
96
|
+
datasetKey: string,
|
|
97
|
+
visualizations: Record<string, AnyVisualization>
|
|
98
|
+
) => {
|
|
99
|
+
if (!filter.usedBy || !filter.usedBy.length) return true
|
|
100
|
+
const vizUsingFilters = filter.usedBy?.map(vizKey => visualizations[vizKey])
|
|
101
|
+
return vizUsingFilters?.some(viz => viz?.dataKey === datasetKey)
|
|
102
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { setAutoLoadDefaultValue, getToFetch, getFilterValues, getLoadingFilterMemo } from '../apiFilterHelpers'
|
|
2
|
+
import _ from 'lodash'
|
|
3
|
+
import type { APIFilterDropdowns } from '../../components/DashboardFilters'
|
|
4
|
+
|
|
5
|
+
describe('getLoadingFilterMemo', () => {
|
|
6
|
+
it('should return correct APIFilterDropdowns for valid inputs', () => {
|
|
7
|
+
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
8
|
+
const apiFilterDropdowns: APIFilterDropdowns = {
|
|
9
|
+
endpoint1: { text: 'text1', value: 'value1' }
|
|
10
|
+
}
|
|
11
|
+
const expectedOutput: APIFilterDropdowns = {
|
|
12
|
+
endpoint1: { text: 'text1', value: 'value1' },
|
|
13
|
+
endpoint2: null
|
|
14
|
+
}
|
|
15
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should return an empty object for empty sharedAPIFilters', () => {
|
|
19
|
+
const sharedAPIFilters: string[] = []
|
|
20
|
+
const apiFilterDropdowns: APIFilterDropdowns = {
|
|
21
|
+
endpoint1: { text: 'text1', value: 'value1' }
|
|
22
|
+
}
|
|
23
|
+
const expectedOutput: APIFilterDropdowns = {}
|
|
24
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should return APIFilterDropdowns with null values for empty apiFilterDropdowns', () => {
|
|
28
|
+
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
29
|
+
const apiFilterDropdowns: APIFilterDropdowns = {}
|
|
30
|
+
const expectedOutput: APIFilterDropdowns = {
|
|
31
|
+
endpoint1: null,
|
|
32
|
+
endpoint2: null
|
|
33
|
+
}
|
|
34
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(expectedOutput)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should not overwrite existing data in apiFilterDropdowns, so return original dropdowns', () => {
|
|
38
|
+
const sharedAPIFilters = ['endpoint1', 'endpoint2']
|
|
39
|
+
const apiFilterDropdowns: APIFilterDropdowns = {
|
|
40
|
+
endpoint1: { text: 'text1', value: 'value1' },
|
|
41
|
+
endpoint2: { text: 'text2', value: 'value2' }
|
|
42
|
+
}
|
|
43
|
+
expect(getLoadingFilterMemo(sharedAPIFilters, apiFilterDropdowns)).toEqual(apiFilterDropdowns)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('getFilterValues', () => {
|
|
48
|
+
it('should return correct filter values for valid inputs', () => {
|
|
49
|
+
const data = [{ key1: 'value1', key2: 'value2' }]
|
|
50
|
+
const apiFilter = { textSelector: 'key1', valueSelector: 'key2' }
|
|
51
|
+
const expectedOutput = [{ text: 'value1', value: 'value2' }]
|
|
52
|
+
expect(getFilterValues(data, apiFilter)).toEqual(expectedOutput)
|
|
53
|
+
delete apiFilter.textSelector
|
|
54
|
+
const expectedOutput2 = [{ text: 'value2', value: 'value2' }]
|
|
55
|
+
expect(getFilterValues(data, apiFilter)).toEqual(expectedOutput2)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('getToFetch', () => {
|
|
60
|
+
it('should return an empty object when sharedAPIFilters is empty', () => {
|
|
61
|
+
const result = getToFetch([], {})
|
|
62
|
+
expect(result).toEqual({})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should return an object with endpoints when apiFilterDropdowns is empty', () => {
|
|
66
|
+
const sharedAPIFilters = [{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] }]
|
|
67
|
+
const result = getToFetch(sharedAPIFilters, {})
|
|
68
|
+
expect(result).toEqual({ '/endpoint1': ['/endpoint1', 0] })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should return and empty object when sharedAPIFilters contains filters with no parents', () => {
|
|
72
|
+
const sharedAPIFilters = [{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] }]
|
|
73
|
+
const apiFilterDropdowns = { '/endpoint1': true }
|
|
74
|
+
const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
|
|
75
|
+
expect(result).toEqual({})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should return an empty object when parentParams contains an empty value', () => {
|
|
79
|
+
const sharedAPIFilters = [
|
|
80
|
+
{ key: 'parent1', apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] },
|
|
81
|
+
{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: ['parent1'] }
|
|
82
|
+
]
|
|
83
|
+
const apiFilterDropdowns = { '/endpoint1': true }
|
|
84
|
+
const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
|
|
85
|
+
expect(result).toEqual({})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should return an empty object when parentParams contains an empty value', () => {
|
|
89
|
+
const sharedAPIFilters = [
|
|
90
|
+
{ key: 'parent1', value: '', apiFilter: { apiEndpoint: '/endpoint1' }, parents: [] },
|
|
91
|
+
{ apiFilter: { apiEndpoint: '/endpoint1' }, parents: ['parent1'] }
|
|
92
|
+
]
|
|
93
|
+
const apiFilterDropdowns = { '/endpoint1': true }
|
|
94
|
+
const result = getToFetch(sharedAPIFilters, apiFilterDropdowns)
|
|
95
|
+
expect(result).toEqual({})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('setAutoLoadDefaultValue', () => {
|
|
100
|
+
const dropdownOptions = [
|
|
101
|
+
{ value: 'option1', label: 'Option 1' },
|
|
102
|
+
{ value: 'option2', label: 'Option 2' }
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
const sharedFilters = [
|
|
106
|
+
{ key: 'filter1', active: null, queuedActive: null, parents: [] },
|
|
107
|
+
{ key: 'filter2', active: null, queuedActive: null, parents: ['filter1'] }
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
it('should return the original filter when autoLoadFilterIndexes is empty', () => {
|
|
111
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFilters, [])
|
|
112
|
+
expect(result).toEqual(sharedFilters[0])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should return the original filter when dropdownOptions is empty', () => {
|
|
116
|
+
const result = setAutoLoadDefaultValue(0, [], sharedFilters, [0])
|
|
117
|
+
expect(result).toEqual(sharedFilters[0])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should return the original filter when dropdownOptions is undefined', () => {
|
|
121
|
+
const result = setAutoLoadDefaultValue(0, undefined, sharedFilters, [0])
|
|
122
|
+
expect(result).toEqual(sharedFilters[0])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should return the original filter when sharedFilterIndex is not in autoLoadFilterIndexes', () => {
|
|
126
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFilters, [1])
|
|
127
|
+
expect(result).toEqual(sharedFilters[0])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should return the original filter when not all parent filters are selected', () => {
|
|
131
|
+
const result = setAutoLoadDefaultValue(1, dropdownOptions, sharedFilters, [1])
|
|
132
|
+
expect(result).toEqual(sharedFilters[1])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should assign the default value from dropdownOptions when no active value is set', () => {
|
|
136
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
137
|
+
sharedFiltersCopy[0].active = null
|
|
138
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
|
|
139
|
+
expect(result.active).toEqual('option1')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should retain the current active value if it exists in dropdownOptions', () => {
|
|
143
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
144
|
+
sharedFiltersCopy[0].active = 'option1'
|
|
145
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
|
|
146
|
+
expect(result.active).toEqual('option1')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should assign the default value if the current active value does not exist in dropdownOptions', () => {
|
|
150
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
151
|
+
sharedFiltersCopy[0].active = 'nonexistent'
|
|
152
|
+
const result = setAutoLoadDefaultValue(0, dropdownOptions, sharedFiltersCopy, [0])
|
|
153
|
+
expect(result.active).toEqual('option1')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
1
2
|
import { SharedFilter } from '../../types/SharedFilter'
|
|
2
3
|
import { filterData } from '../filterData'
|
|
3
4
|
|
|
@@ -36,48 +37,6 @@ describe('filterData', () => {
|
|
|
36
37
|
expect(result).toEqual([{ name: 'John', age: 30 }])
|
|
37
38
|
})
|
|
38
39
|
|
|
39
|
-
it('causes sideEffects to filters', () => {
|
|
40
|
-
// the side effect is not desired, but current functionality depends on the sideEffect.
|
|
41
|
-
// hopefully the side effect will be refactored in the future to be a returned value.
|
|
42
|
-
const filters = [
|
|
43
|
-
{ columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'name' },
|
|
44
|
-
{ columnName: 'age', fileName: 'abc', key: 'age' },
|
|
45
|
-
{ columnName: 'color', fileName: 'abc', key: 'color', parents: ['age'] }
|
|
46
|
-
] as SharedFilter[]
|
|
47
|
-
const data = [
|
|
48
|
-
{ name: 'John', age: 30, color: 'blue' },
|
|
49
|
-
{ name: 'Jane', age: 25, color: 'red' },
|
|
50
|
-
{ name: 'John', age: 35, color: 'yellow' },
|
|
51
|
-
{ name: 'Jane', age: 30, color: 'green' }
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
const result = filterData(filters, data)
|
|
55
|
-
|
|
56
|
-
expect(result).toEqual([{ name: 'John', age: 30, color: 'blue' }])
|
|
57
|
-
|
|
58
|
-
const sideEffectOfFiltering = [
|
|
59
|
-
{
|
|
60
|
-
columnName: 'name',
|
|
61
|
-
active: 'John',
|
|
62
|
-
queuedActive: 'John',
|
|
63
|
-
fileName: 'abc',
|
|
64
|
-
key: 'name',
|
|
65
|
-
tier: 1
|
|
66
|
-
},
|
|
67
|
-
{ columnName: 'age', fileName: 'abc', key: 'age', tier: 1 },
|
|
68
|
-
{
|
|
69
|
-
columnName: 'color',
|
|
70
|
-
fileName: 'abc',
|
|
71
|
-
key: 'color',
|
|
72
|
-
parents: ['age'],
|
|
73
|
-
tier: 2,
|
|
74
|
-
values: ['blue', 'yellow'],
|
|
75
|
-
active: 'blue'
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
expect(filters).toEqual(sideEffectOfFiltering)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
40
|
it('should not include data that does not meet the filter criteria', () => {
|
|
82
41
|
const filters = [
|
|
83
42
|
//{ columnName: 'apple', fileName: 'abc', key: 'banana' },
|
|
@@ -95,55 +54,4 @@ describe('filterData', () => {
|
|
|
95
54
|
const result = filterData(filters, data)
|
|
96
55
|
expect(result).toEqual([{ name: 'John', age: 25, color: 'red' }])
|
|
97
56
|
})
|
|
98
|
-
|
|
99
|
-
it('should pivot data based on the provided filters', () => {
|
|
100
|
-
const filters = [{ key: 'Race', type: 'datafilter', showDropdown: true, columnName: 'Race', pivot: 'Age-adjusted rate', usedBy: ['table1707935263149'] }] as SharedFilter[]
|
|
101
|
-
const data = [
|
|
102
|
-
{
|
|
103
|
-
Race: 'Hispanic or Latino',
|
|
104
|
-
'Age-adjusted rate': '644.2',
|
|
105
|
-
Year: '2016'
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
Race: 'Non-Hispanic American Indian',
|
|
109
|
-
'Age-adjusted rate': '636.1',
|
|
110
|
-
Year: '2016'
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
Race: 'Non-Hispanic Black',
|
|
114
|
-
'Age-adjusted rate': '563.7',
|
|
115
|
-
Year: '2016'
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
Race: 'Hispanic or Latino',
|
|
119
|
-
'Age-adjusted rate': '644.2',
|
|
120
|
-
Year: '2017'
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
Race: 'Non-Hispanic American Indian',
|
|
124
|
-
'Age-adjusted rate': '636.1',
|
|
125
|
-
Year: '2017'
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
Race: 'Non-Hispanic Black',
|
|
129
|
-
'Age-adjusted rate': '563.7',
|
|
130
|
-
Year: '2017'
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
expect(filterData(filters, data)).toEqual([
|
|
135
|
-
{
|
|
136
|
-
'Hispanic or Latino': '644.2',
|
|
137
|
-
'Non-Hispanic American Indian': '636.1',
|
|
138
|
-
'Non-Hispanic Black': '563.7',
|
|
139
|
-
Year: '2016'
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
'Hispanic or Latino': '644.2',
|
|
143
|
-
'Non-Hispanic American Indian': '636.1',
|
|
144
|
-
'Non-Hispanic Black': '563.7',
|
|
145
|
-
Year: '2017'
|
|
146
|
-
}
|
|
147
|
-
])
|
|
148
|
-
})
|
|
149
57
|
})
|