@cdc/dashboard 4.24.2 → 4.24.4
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 +128512 -99417
- package/examples/chart-data.json +5409 -0
- package/examples/full-dash-test.json +14643 -0
- package/examples/full-dashboard.json +10036 -0
- package/examples/sankey.json +5218 -0
- package/index.html +4 -3
- package/package.json +11 -10
- package/src/CdcDashboard.tsx +129 -124
- package/src/CdcDashboardComponent.tsx +316 -441
- package/src/DashboardContext.tsx +4 -1
- package/src/_stories/Dashboard.stories.tsx +79 -36
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/dashboard-gallery.json +523 -534
- package/src/_stories/_mock/multi-viz.json +378 -0
- package/src/_stories/_mock/pivot-filter.json +161 -0
- package/src/_stories/_mock/standalone-table.json +122 -0
- package/src/_stories/_mock/toggle-example.json +4035 -0
- package/src/components/DataDesignerModal.tsx +145 -0
- package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
- package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
- package/src/components/Filters.tsx +88 -0
- package/src/components/Grid.tsx +3 -1
- package/src/components/Header/FilterModal.tsx +506 -0
- package/src/components/Header/Header.tsx +25 -465
- package/src/components/Row.tsx +65 -29
- package/src/components/Toggle/Toggle.tsx +36 -0
- package/src/components/Toggle/index.tsx +1 -0
- package/src/components/Toggle/toggle-style.css +34 -0
- package/src/components/VisualizationRow.tsx +174 -0
- package/src/components/VisualizationsPanel.tsx +13 -3
- package/src/components/Widget.tsx +28 -126
- package/src/helpers/filterData.ts +75 -50
- package/src/helpers/generateValuesForFilter.ts +2 -12
- package/src/helpers/getApiFilterKey.ts +5 -0
- package/src/helpers/getFilteredData.ts +39 -0
- package/src/helpers/getUpdateConfig.ts +39 -22
- package/src/helpers/getVizConfig.ts +31 -0
- package/src/helpers/getVizRowColumnLocator.ts +9 -0
- package/src/helpers/iconHash.tsx +34 -0
- package/src/helpers/tests/filterData.test.ts +149 -0
- package/src/images/icon-toggle.svg +1 -0
- package/src/scss/grid.scss +10 -3
- package/src/scss/main.scss +11 -0
- package/src/store/dashboard.actions.ts +35 -3
- package/src/store/dashboard.reducer.ts +33 -2
- package/src/types/APIFilter.ts +4 -5
- package/src/types/ConfigRow.ts +13 -2
- package/src/types/DataSet.ts +11 -8
- package/src/types/InitialState.ts +2 -1
- package/src/types/SharedFilter.ts +6 -3
- package/src/types/Tab.ts +1 -0
|
@@ -1,73 +1,98 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
1
2
|
import { SharedFilter } from '../types/SharedFilter'
|
|
2
|
-
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
3
3
|
|
|
4
4
|
const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
|
|
5
5
|
if (!sharedFilter.parents?.length) {
|
|
6
6
|
return 1
|
|
7
7
|
} else {
|
|
8
|
-
|
|
8
|
+
const parent = filters.find(filter => sharedFilter.parents!.includes(filter.key))
|
|
9
9
|
if (!parent) return 1
|
|
10
10
|
return 1 + findFilterTier(filters, parent)
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
let filteredData = _data
|
|
28
|
-
// TODO triple loop??
|
|
29
|
-
for (let i = 0; i < maxTier; i++) {
|
|
30
|
-
let filteredDataSubTier: any[] = []
|
|
31
|
-
|
|
32
|
-
filteredData.forEach(row => {
|
|
33
|
-
let add = true
|
|
14
|
+
function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
15
|
+
let maxTier = 1
|
|
16
|
+
filters.forEach(sharedFilter => {
|
|
17
|
+
sharedFilter.tier = findFilterTier(filters, sharedFilter)
|
|
18
|
+
if (sharedFilter.tier > maxTier) {
|
|
19
|
+
maxTier = sharedFilter.tier
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
return maxTier
|
|
23
|
+
}
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
function filter(data, filters, condition) {
|
|
26
|
+
return data
|
|
27
|
+
? data.filter(row => {
|
|
28
|
+
const found = filters.find(filter => {
|
|
29
|
+
if (filter.pivot) return false
|
|
30
|
+
const currentValue = row[filter.columnName]
|
|
31
|
+
const selectedValue = filter.queuedActive || filter.active
|
|
32
|
+
const isNotTheSelectedValue = selectedValue && currentValue != selectedValue
|
|
33
|
+
const isFirstOccurrenceOfTier = filter.tier === condition
|
|
34
|
+
if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
|
|
35
|
+
return true
|
|
39
36
|
}
|
|
40
37
|
})
|
|
41
|
-
|
|
42
|
-
if (add) filteredDataSubTier.push(row)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
filters.forEach(sharedFilter => {
|
|
46
|
-
if (sharedFilter.tier && sharedFilter.tier === i + 2) {
|
|
47
|
-
sharedFilter.values = generateValuesForFilter(sharedFilter.columnName, { data: filteredDataSubTier }, filterBehavior)
|
|
48
|
-
if (sharedFilter.values.length > 0 && (!sharedFilter.active || sharedFilter.values.indexOf(sharedFilter.active) === -1)) {
|
|
49
|
-
sharedFilter.active = sharedFilter.values[0]
|
|
50
|
-
}
|
|
51
|
-
}
|
|
38
|
+
return !found
|
|
52
39
|
})
|
|
40
|
+
: []
|
|
41
|
+
}
|
|
53
42
|
|
|
54
|
-
|
|
43
|
+
function setFilterValuesAndActiveFilter(filters: SharedFilter[], filteredData: Object[], i: number) {
|
|
44
|
+
filters.forEach(sharedFilter => {
|
|
45
|
+
if (sharedFilter.pivot) {
|
|
46
|
+
sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
|
|
47
|
+
} else if (sharedFilter.tier === i + 2 && !Array.isArray(sharedFilter.active)) {
|
|
48
|
+
sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
|
|
49
|
+
const valueAlreadySelected = sharedFilter.values.includes(sharedFilter.active)
|
|
50
|
+
if (!valueAlreadySelected && sharedFilter.values.length > 0) {
|
|
51
|
+
sharedFilter.active = sharedFilter.values[0]
|
|
52
|
+
}
|
|
55
53
|
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
})
|
|
57
|
+
const pivotData = (data, pivotFilter: SharedFilter) => {
|
|
58
|
+
const pivotActive = pivotFilter.active as string[]
|
|
59
|
+
const inactive = pivotFilter.values.filter(value => !pivotActive.includes(value))
|
|
60
|
+
const pivotColumn = pivotFilter.columnName
|
|
61
|
+
const valueColumn = pivotFilter.pivot
|
|
62
|
+
const grouped = _.groupBy(data, val => val[pivotColumn])
|
|
63
|
+
const newData = []
|
|
64
|
+
for (const key in grouped) {
|
|
65
|
+
const group = grouped[key]
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
group.forEach((val, index) => {
|
|
68
|
+
const row = newData[index] || {}
|
|
69
|
+
if (!inactive.includes(key)) row[key] = val[valueColumn]
|
|
70
|
+
const toAdd = _.omit(val, [pivotColumn, valueColumn, ...inactive])
|
|
71
|
+
newData[index] = { ...row, ...toAdd }
|
|
69
72
|
})
|
|
73
|
+
}
|
|
74
|
+
return newData
|
|
75
|
+
}
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
/** This function returns filtered data.
|
|
78
|
+
* It also manipulates the filters by adding: tiers, filterOptions, and default selections */
|
|
79
|
+
export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
|
|
80
|
+
const maxTier = getMaxTierAndSetFilterTiers(filters)
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < maxTier; i++) {
|
|
83
|
+
const lastIteration = i === maxTier - 1
|
|
84
|
+
|
|
85
|
+
const filteredData = filter(_data, filters, i + 1)
|
|
86
|
+
|
|
87
|
+
setFilterValuesAndActiveFilter(_.cloneDeep(filters), filteredData, i)
|
|
88
|
+
|
|
89
|
+
if (lastIteration) {
|
|
90
|
+
const pivotFilter = filters.find(filter => filter.pivot)
|
|
91
|
+
if (pivotFilter) {
|
|
92
|
+
return pivotData(filteredData, pivotFilter)
|
|
93
|
+
}
|
|
94
|
+
// not sure if this last run of filter() function is necessary.
|
|
95
|
+
return filter(filteredData, filters, maxTier - 1)
|
|
96
|
+
}
|
|
72
97
|
}
|
|
73
98
|
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import { FilterBehavior } from '../components/Header/Header'
|
|
2
|
-
|
|
3
|
-
// Gets filter values from API response
|
|
4
|
-
export const generateValuesForAPIFilter = (columnName, _data): string[] => {
|
|
5
|
-
type Row = { [key: string]: any }
|
|
6
|
-
return Object.values(_data)
|
|
7
|
-
.filter(row => row && !!(row as Row)[columnName])
|
|
8
|
-
.map(row => (row as Row)[columnName])
|
|
9
|
-
}
|
|
10
|
-
|
|
11
1
|
// Gets filter values from dataset
|
|
12
|
-
export const generateValuesForFilter = (columnName, _data
|
|
2
|
+
export const generateValuesForFilter = (columnName, _data) => {
|
|
13
3
|
const values: string[] = []
|
|
14
4
|
|
|
15
5
|
Object.keys(_data).forEach(key => {
|
|
16
6
|
_data[key]?.forEach(row => {
|
|
17
7
|
const value = row[columnName]
|
|
18
|
-
if (
|
|
8
|
+
if (!values.includes(value)) {
|
|
19
9
|
values.push(value)
|
|
20
10
|
}
|
|
21
11
|
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
|
+
import { Dashboard } from '../types/Dashboard'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
import { filterData } from './filterData'
|
|
5
|
+
import { getFormattedData } from './getFormattedData'
|
|
6
|
+
import { getVizKeys } from './getVizKeys'
|
|
7
|
+
|
|
8
|
+
export const getApplicableFilters = (dashboard: Dashboard, key: string | number): false | SharedFilter[] => {
|
|
9
|
+
const c = dashboard.sharedFilters?.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(`${key}`) !== -1)
|
|
10
|
+
return c?.length > 0 ? c : false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getFilteredData = (state: DashboardState, initialFilteredData = {}, dataOverride?: Object) => {
|
|
14
|
+
const newFilteredData = initialFilteredData
|
|
15
|
+
const { config } = state
|
|
16
|
+
getVizKeys(config).forEach(key => {
|
|
17
|
+
const applicableFilters = getApplicableFilters(config.dashboard, key)
|
|
18
|
+
if (applicableFilters) {
|
|
19
|
+
const { dataKey, data, dataDescription } = config.visualizations[key]
|
|
20
|
+
const _data = state.data[dataKey] || data
|
|
21
|
+
const formattedData = dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
|
|
22
|
+
|
|
23
|
+
newFilteredData[key] = filterData(applicableFilters, formattedData)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
config.rows.forEach((row, index) => {
|
|
27
|
+
if (row.dataKey) {
|
|
28
|
+
const applicableFilters = getApplicableFilters(config.dashboard, index)
|
|
29
|
+
if (applicableFilters) {
|
|
30
|
+
const { dataKey, data, dataDescription } = row
|
|
31
|
+
const _data = state.data[dataKey] || data
|
|
32
|
+
const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
|
|
33
|
+
|
|
34
|
+
newFilteredData[index] = filterData(applicableFilters, formattedData)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
return newFilteredData
|
|
39
|
+
}
|
|
@@ -4,6 +4,9 @@ import { filterData } from './filterData'
|
|
|
4
4
|
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
5
5
|
import { getFormattedData } from './getFormattedData'
|
|
6
6
|
import { getVizKeys } from './getVizKeys'
|
|
7
|
+
import { getVizRowColumnLocator } from './getVizRowColumnLocator'
|
|
8
|
+
|
|
9
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
7
10
|
|
|
8
11
|
type UpdateState = Omit<DashboardState, 'config'> & {
|
|
9
12
|
config?: DashboardConfig
|
|
@@ -14,17 +17,30 @@ export const getUpdateConfig =
|
|
|
14
17
|
(newConfig, dataOverride?: Object): [Config, Object] => {
|
|
15
18
|
let newFilteredData = {}
|
|
16
19
|
let visualizationKeys = getVizKeys(newConfig)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
const vizRowColumnLocator = getVizRowColumnLocator(newConfig.rows)
|
|
22
|
+
|
|
20
23
|
if (newConfig.dashboard.sharedFilters) {
|
|
21
24
|
newConfig.dashboard.sharedFilters.forEach((filter, i) => {
|
|
22
25
|
const filterIsSetByVizData = !!visualizationKeys.find(key => key === filter.setBy)
|
|
23
|
-
|
|
26
|
+
const _filter = newConfig.dashboard.sharedFilters[i]
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
const setValuesAndActive = filterValues => {
|
|
29
|
+
_filter.values = filterValues
|
|
30
|
+
if (filterValues.length > 0) {
|
|
31
|
+
const defaultValues = _filter.pivot ? _filter.values : _filter.values[0]
|
|
27
32
|
|
|
33
|
+
const queryStringFilterValue = getQueryStringFilterValue(_filter)
|
|
34
|
+
if(queryStringFilterValue){
|
|
35
|
+
_filter.active = queryStringFilterValue
|
|
36
|
+
} else {
|
|
37
|
+
_filter.active = _filter.active || defaultValues
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filterValues = generateValuesForFilter(filter.columnName, dataOverride || state.data)
|
|
43
|
+
if (filterIsSetByVizData) {
|
|
28
44
|
if (_filter.order === 'asc') {
|
|
29
45
|
filterValues.sort()
|
|
30
46
|
}
|
|
@@ -32,32 +48,33 @@ export const getUpdateConfig =
|
|
|
32
48
|
filterValues.sort().reverse()
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
setFilter(i, 'active', _filter.active || _filter.values[0])
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
|
|
43
|
-
const generatedValues = generateValuesForFilter(filter.columnName, dataOverride || state.data, state.config?.filterBehavior)
|
|
44
|
-
setFilter(i, 'values', generatedValues)
|
|
45
|
-
const _filter = newConfig.dashboard.sharedFilters[i]
|
|
46
|
-
if (_filter.values.length > 0) {
|
|
47
|
-
setFilter(i, 'active', filter.active || _filter.values[0])
|
|
48
|
-
}
|
|
51
|
+
setValuesAndActive(filterValues)
|
|
52
|
+
} else if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
|
|
53
|
+
setValuesAndActive(filterValues)
|
|
49
54
|
}
|
|
50
55
|
})
|
|
51
56
|
|
|
52
57
|
visualizationKeys.forEach(visualizationKey => {
|
|
53
|
-
|
|
58
|
+
const row = vizRowColumnLocator[visualizationKey]
|
|
59
|
+
if (newConfig.rows[row]?.datakey) return // data configured on the row level
|
|
60
|
+
const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
|
|
54
61
|
|
|
55
62
|
if (applicableFilters.length > 0) {
|
|
56
63
|
const visualization = newConfig.visualizations[visualizationKey]
|
|
57
64
|
const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
|
|
58
65
|
const formattedData = getFormattedData(_newConfigDataSet?.data || visualization.data, visualization.dataDescription)
|
|
59
66
|
const _data = formattedData || (dataOverride || state.data)[visualization.dataKey]
|
|
60
|
-
newFilteredData[visualizationKey] = filterData(applicableFilters, _data
|
|
67
|
+
newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
newConfig.rows.forEach((row, rowIndex) => {
|
|
72
|
+
const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1)
|
|
73
|
+
|
|
74
|
+
if (applicableFilters.length > 0) {
|
|
75
|
+
const formattedData = getFormattedData(row.data, row.dataDescription)
|
|
76
|
+
const _data = formattedData || (dataOverride || state.data)[rowIndex]
|
|
77
|
+
newFilteredData[rowIndex] = filterData(applicableFilters, _data)
|
|
61
78
|
}
|
|
62
79
|
})
|
|
63
80
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { MultiDashboardConfig } from '../types/MultiDashboard'
|
|
3
|
+
import DataTransform from '@cdc/core/helpers/DataTransform'
|
|
4
|
+
|
|
5
|
+
const transform = new DataTransform()
|
|
6
|
+
|
|
7
|
+
export const getVizConfig = (visualizationKey: string, rowNumber: number, config: MultiDashboardConfig, data: Object, filteredData?: Object) => {
|
|
8
|
+
const visualizationConfig = _.cloneDeep(config.visualizations[visualizationKey])
|
|
9
|
+
const rowData = config.rows[rowNumber]
|
|
10
|
+
if (rowData?.dataKey) {
|
|
11
|
+
// data configured on the row
|
|
12
|
+
Object.assign(visualizationConfig, _.pick(rowData, ['dataKey', 'dataDescription', 'formattedData', 'data']))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (visualizationConfig.formattedData) visualizationConfig.originalFormattedData = visualizationConfig.formattedData
|
|
16
|
+
const filteredVizData = filteredData?.[rowNumber] ?? filteredData?.[visualizationKey]
|
|
17
|
+
|
|
18
|
+
if (filteredVizData) {
|
|
19
|
+
visualizationConfig.data = filteredVizData
|
|
20
|
+
if (visualizationConfig.formattedData) {
|
|
21
|
+
visualizationConfig.formattedData = visualizationConfig.data
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
|
|
25
|
+
visualizationConfig.data = data[dataKey]
|
|
26
|
+
if (visualizationConfig.formattedData) {
|
|
27
|
+
visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return visualizationConfig
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ConfigRow } from '../types/ConfigRow'
|
|
2
|
+
|
|
3
|
+
export const getVizRowColumnLocator = (rows: ConfigRow[]) =>
|
|
4
|
+
rows.reduce((acc, curr, index) => {
|
|
5
|
+
curr.columns?.forEach((column, columnIndex) => {
|
|
6
|
+
if (column.widget !== undefined) acc[column.widget] = { row: index, column: columnIndex }
|
|
7
|
+
})
|
|
8
|
+
return acc
|
|
9
|
+
}, {})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Icon from '@cdc/core/components/ui/Icon'
|
|
2
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
|
|
4
|
+
export const iconHash = {
|
|
5
|
+
'data-bite': <Icon display='databite' base />,
|
|
6
|
+
Bar: <Icon display='chartBar' base />,
|
|
7
|
+
'Spark Line': <Icon display='chartLine' />,
|
|
8
|
+
'waffle-chart': <Icon display='grid' base />,
|
|
9
|
+
'markup-include': <Icon display='code' base />,
|
|
10
|
+
Line: <Icon display='chartLine' base />,
|
|
11
|
+
Pie: <Icon display='chartPie' base />,
|
|
12
|
+
us: <Icon display='mapUsa' base />,
|
|
13
|
+
'us-county': <Icon display='mapUsa' base />,
|
|
14
|
+
world: <Icon display='mapWorld' base />,
|
|
15
|
+
'single-state': <Icon display='mapAl' base />,
|
|
16
|
+
gear: <Icon display='gear' base />,
|
|
17
|
+
tools: <Icon display='tools' base />,
|
|
18
|
+
'filtered-text': <Icon display='filtered-text' base />,
|
|
19
|
+
'filter-dropdowns': <Icon display='filter-dropdowns' base />,
|
|
20
|
+
table: <Icon display='table' base />,
|
|
21
|
+
Sankey: <Icon display='sankey' base />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const getIcon = (visualization: Visualization) => {
|
|
25
|
+
const { type, visualizationType, general } = visualization
|
|
26
|
+
if (visualizationType) return iconHash[visualizationType]
|
|
27
|
+
if (general?.geoType) {
|
|
28
|
+
// for visualizations, mismatching state and state icon is not desired
|
|
29
|
+
// so instead of showing alabama as the default state icon we show the US icon.
|
|
30
|
+
if (general.geoType === 'single-state') return iconHash['us']
|
|
31
|
+
return iconHash[general.geoType]
|
|
32
|
+
}
|
|
33
|
+
return iconHash[type]
|
|
34
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { SharedFilter } from '../../types/SharedFilter'
|
|
2
|
+
import { filterData } from '../filterData'
|
|
3
|
+
|
|
4
|
+
describe('filterData', () => {
|
|
5
|
+
it('should filter data based on the provided filters', () => {
|
|
6
|
+
const filters = [
|
|
7
|
+
{ tier: 1, columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'abc' },
|
|
8
|
+
{ tier: 2, columnName: 'age', active: 30, queuedActive: 30, fileName: 'abc', key: 'abc' }
|
|
9
|
+
] as SharedFilter[]
|
|
10
|
+
const data = [
|
|
11
|
+
{ name: 'John', age: 30 },
|
|
12
|
+
{ name: 'Jane', age: 25 },
|
|
13
|
+
{ name: 'John', age: 35 },
|
|
14
|
+
{ name: 'Jane', age: 30 }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const result = filterData(filters, data)
|
|
18
|
+
|
|
19
|
+
expect(result).toEqual([{ name: 'John', age: 30 }])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('filters with parents', () => {
|
|
23
|
+
const filters = [
|
|
24
|
+
{ columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'abc' },
|
|
25
|
+
{ columnName: 'age', active: 30, queuedActive: 30, fileName: 'abc', key: 'abc', parents: ['name'] }
|
|
26
|
+
] as SharedFilter[]
|
|
27
|
+
const data = [
|
|
28
|
+
{ name: 'John', age: 30 },
|
|
29
|
+
{ name: 'Jane', age: 25 },
|
|
30
|
+
{ name: 'John', age: 35 },
|
|
31
|
+
{ name: 'Jane', age: 30 }
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const result = filterData(filters, data)
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual([{ name: 'John', age: 30 }])
|
|
37
|
+
})
|
|
38
|
+
|
|
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
|
+
it('should not include data that does not meet the filter criteria', () => {
|
|
82
|
+
const filters = [
|
|
83
|
+
//{ columnName: 'apple', fileName: 'abc', key: 'banana' },
|
|
84
|
+
{ columnName: 'color', active: 'red', queuedActive: 'red', fileName: 'abc', key: 'color' },
|
|
85
|
+
{ columnName: 'name', fileName: 'abc', key: 'name' },
|
|
86
|
+
{ columnName: 'age', fileName: 'abc', key: 'age', parents: ['name'] }
|
|
87
|
+
] as SharedFilter[]
|
|
88
|
+
const data = [
|
|
89
|
+
{ name: 'Jane', age: 30, color: 'blue' },
|
|
90
|
+
{ name: 'John', age: 25, color: 'red' },
|
|
91
|
+
{ name: 'John', age: 25, color: 'green' }
|
|
92
|
+
//{ name: 'John', age: 25, color: 'red', apple: 'banana' }
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const result = filterData(filters, data)
|
|
96
|
+
expect(result).toEqual([{ name: 'John', age: 25, color: 'red' }])
|
|
97
|
+
})
|
|
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
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 64C86 64 0 150 0 256S86 448 192 448H384c106 0 192-86 192-192s-86-192-192-192H192zm192 96a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
|
package/src/scss/grid.scss
CHANGED
|
@@ -35,8 +35,7 @@ $red: #f74242;
|
|
|
35
35
|
flex-flow: row;
|
|
36
36
|
width: 100%;
|
|
37
37
|
position: relative;
|
|
38
|
-
|
|
39
|
-
padding: 1em;
|
|
38
|
+
padding: 2em 1em 1em;
|
|
40
39
|
border: 1px solid #c2c2c2;
|
|
41
40
|
transition: border 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
42
41
|
background-color: #f2f2f2;
|
|
@@ -77,7 +76,7 @@ $red: #f74242;
|
|
|
77
76
|
|
|
78
77
|
.row-menu__btn:hover .row-menu__flyout {
|
|
79
78
|
transition: width 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
80
|
-
width:
|
|
79
|
+
width: 180px;
|
|
81
80
|
|
|
82
81
|
li {
|
|
83
82
|
display: flex;
|
|
@@ -330,6 +329,14 @@ $red: #f74242;
|
|
|
330
329
|
.builder-row {
|
|
331
330
|
position: relative;
|
|
332
331
|
|
|
332
|
+
.btn-configure-row {
|
|
333
|
+
background: none;
|
|
334
|
+
display: block;
|
|
335
|
+
position: absolute;
|
|
336
|
+
right: 1em;
|
|
337
|
+
top: 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
333
340
|
.widget__content {
|
|
334
341
|
padding: 0 2em;
|
|
335
342
|
|
package/src/scss/main.scss
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
margin-top: 20%;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
> .cove-editor__content {
|
|
14
|
+
width: 100% !important;
|
|
15
|
+
left: 0px;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
.editor-heading {
|
|
14
19
|
background-color: #ddd;
|
|
15
20
|
border-bottom: #c7c7c7 1px solid;
|
|
@@ -187,6 +192,9 @@
|
|
|
187
192
|
.dashboard-row {
|
|
188
193
|
display: flex;
|
|
189
194
|
flex-direction: column;
|
|
195
|
+
&.toggle {
|
|
196
|
+
display: block;
|
|
197
|
+
}
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
.dashboard-col {
|
|
@@ -197,6 +205,9 @@
|
|
|
197
205
|
margin-left: 0;
|
|
198
206
|
margin-right: 0;
|
|
199
207
|
}
|
|
208
|
+
&.hidden-toggle {
|
|
209
|
+
display: none;
|
|
210
|
+
}
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
.dashboard-col-12 {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { DashboardConfig as Config } from '../types/DashboardConfig'
|
|
2
2
|
import { type Action } from '@cdc/core/types/Action'
|
|
3
|
+
import { Tab } from '../types/Tab'
|
|
4
|
+
<<<<<<< HEAD
|
|
5
|
+
import { ConfigureData } from '@cdc/core/types/ConfigureData'
|
|
6
|
+
import { ConfigRow } from '../types/ConfigRow'
|
|
7
|
+
=======
|
|
8
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
9
|
+
>>>>>>> 35436844 (fixed api dropdowns)
|
|
3
10
|
|
|
4
|
-
type SET_CONFIG = Action<'SET_CONFIG', Config
|
|
11
|
+
type SET_CONFIG = Action<'SET_CONFIG', Partial<Config>>
|
|
5
12
|
type UPDATE_CONFIG = Action<'UPDATE_CONFIG', [Config, Object?]>
|
|
6
13
|
type SET_DATA = Action<'SET_DATA', Object>
|
|
7
14
|
type SET_LOADING = Action<'SET_LOADING', boolean>
|
|
8
15
|
type SET_PREVIEW = Action<'SET_PREVIEW', boolean>
|
|
9
16
|
type SET_FILTERED_DATA = Action<'SET_FILTERED_DATA', Object>
|
|
10
|
-
type SET_TAB_SELECTED = Action<'SET_TAB_SELECTED',
|
|
17
|
+
type SET_TAB_SELECTED = Action<'SET_TAB_SELECTED', Tab>
|
|
11
18
|
type RENAME_DASHBOARD_TAB = Action<'RENAME_DASHBOARD_TAB', { current: string; new: string }>
|
|
12
19
|
type INITIALIZE_MULTIDASHBOARDS = Action<'INITIALIZE_MULTIDASHBOARDS', undefined>
|
|
13
20
|
type REMOVE_MULTIDASHBOARD_AT_INDEX = Action<'REMOVE_MULTIDASHBOARD_AT_INDEX', number>
|
|
@@ -15,6 +22,31 @@ type REORDER_MULTIDASHBOARDS = Action<'REORDER_MULTIDASHBOARDS', { currentIndex:
|
|
|
15
22
|
type ADD_NEW_DASHBOARD = Action<'ADD_NEW_DASHBOARD', undefined>
|
|
16
23
|
type SAVE_CURRENT_CHANGES = Action<'SAVE_CURRENT_CHANGES', undefined>
|
|
17
24
|
type SWITCH_CONFIG = Action<'SWITCH_CONFIG', number>
|
|
25
|
+
type TOGGLE_ROW = Action<'TOGGLE_ROW', { rowIndex: number; colIndex: number }>
|
|
26
|
+
<<<<<<< HEAD
|
|
27
|
+
type UPDATE_VISUALIZATION = Action<'UPDATE_VISUALIZATION', { vizKey: string; configureData: Partial<ConfigureData> }>
|
|
28
|
+
type UPDATE_ROW = Action<'UPDATE_ROW', { rowIndex: number; rowData: Partial<ConfigRow> }>
|
|
29
|
+
=======
|
|
30
|
+
type SET_SHARED_FILTERS = Action<'SET_SHARED_FILTERS', SharedFilter[]>
|
|
31
|
+
>>>>>>> 35436844 (fixed api dropdowns)
|
|
18
32
|
|
|
19
|
-
type DashboardActions =
|
|
33
|
+
type DashboardActions =
|
|
34
|
+
| ADD_NEW_DASHBOARD
|
|
35
|
+
| SET_CONFIG
|
|
36
|
+
| UPDATE_CONFIG
|
|
37
|
+
| REMOVE_MULTIDASHBOARD_AT_INDEX
|
|
38
|
+
| RENAME_DASHBOARD_TAB
|
|
39
|
+
| REORDER_MULTIDASHBOARDS
|
|
40
|
+
| SAVE_CURRENT_CHANGES
|
|
41
|
+
| SET_DATA
|
|
42
|
+
| SET_LOADING
|
|
43
|
+
| SET_PREVIEW
|
|
44
|
+
| SET_FILTERED_DATA
|
|
45
|
+
| SET_SHARED_FILTERS
|
|
46
|
+
| SET_TAB_SELECTED
|
|
47
|
+
| SWITCH_CONFIG
|
|
48
|
+
| INITIALIZE_MULTIDASHBOARDS
|
|
49
|
+
| TOGGLE_ROW
|
|
50
|
+
| UPDATE_VISUALIZATION
|
|
51
|
+
| UPDATE_ROW
|
|
20
52
|
export default DashboardActions
|