@cdc/dashboard 4.24.2 → 4.24.3
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 +98192 -85200
- package/examples/sankey.json +5218 -0
- package/index.html +3 -2
- package/package.json +11 -10
- package/src/CdcDashboard.tsx +124 -124
- package/src/CdcDashboardComponent.tsx +173 -186
- package/src/DashboardContext.tsx +4 -1
- package/src/_stories/Dashboard.stories.tsx +27 -5
- package/src/_stories/_mock/pivot-filter.json +163 -0
- package/src/_stories/_mock/standalone-table.json +122 -0
- package/src/_stories/_mock/toggle-example.json +4035 -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/Header/FilterModal.tsx +480 -0
- package/src/components/Header/Header.tsx +25 -465
- package/src/components/Row.tsx +28 -17
- package/src/components/Toggle/Toggle.tsx +37 -0
- package/src/components/Toggle/index.tsx +1 -0
- package/src/components/Toggle/toggle-style.css +34 -0
- package/src/components/VisualizationsPanel.tsx +13 -3
- package/src/components/Widget.tsx +14 -30
- package/src/helpers/filterData.ts +72 -49
- package/src/helpers/generateValuesForFilter.ts +2 -12
- package/src/helpers/getApiFilterKey.ts +5 -0
- package/src/helpers/getUpdateConfig.ts +24 -22
- 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 +1 -1
- package/src/scss/main.scss +6 -0
- package/src/store/dashboard.actions.ts +19 -2
- package/src/store/dashboard.reducer.ts +9 -1
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/DataSet.ts +7 -7
- package/src/types/InitialState.ts +2 -1
- package/src/types/SharedFilter.ts +5 -2
- package/src/types/Tab.ts +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.cdc-open-viz-module {
|
|
2
|
+
--border: 1px solid var(--lightGray);
|
|
3
|
+
.toggle-component {
|
|
4
|
+
display: flex;
|
|
5
|
+
justify-content: right;
|
|
6
|
+
width: 100%;
|
|
7
|
+
margin-bottom: 15px;
|
|
8
|
+
:first-child:is(div) {
|
|
9
|
+
border: var(--border);
|
|
10
|
+
border-radius: 5px 0 0 5px;
|
|
11
|
+
}
|
|
12
|
+
:last-child:is(div) {
|
|
13
|
+
border: var(--border);
|
|
14
|
+
border-radius: 0 5px 5px 0;
|
|
15
|
+
}
|
|
16
|
+
:is(div) {
|
|
17
|
+
border-top: var(--border);
|
|
18
|
+
border-bottom: var(--border);
|
|
19
|
+
padding: 7px 15px;
|
|
20
|
+
display: inline;
|
|
21
|
+
float: right;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
&.selected {
|
|
24
|
+
background-color: var(--primary);
|
|
25
|
+
color: white;
|
|
26
|
+
}
|
|
27
|
+
background-color: var(--white);
|
|
28
|
+
color: var(--primary);
|
|
29
|
+
:is(svg) {
|
|
30
|
+
height: 25px;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -2,11 +2,12 @@ import React from 'react'
|
|
|
2
2
|
import type { Visualization } from '@cdc/core/types/Visualization'
|
|
3
3
|
import Widget from './Widget'
|
|
4
4
|
import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
|
|
5
|
+
import { Table } from '@cdc/core/types/Table'
|
|
5
6
|
|
|
6
7
|
const addVisualization = (type, subType) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
newViz:
|
|
8
|
+
const modalWillOpen = type !== 'markup-include'
|
|
9
|
+
const newVisualizationConfig: Partial<Visualization> = {
|
|
10
|
+
newViz: type !== 'table',
|
|
10
11
|
openModal: modalWillOpen,
|
|
11
12
|
uid: type + Date.now(),
|
|
12
13
|
type
|
|
@@ -23,6 +24,13 @@ const addVisualization = (type, subType) => {
|
|
|
23
24
|
case 'data-bite' || 'waffle-chart' || 'markup-include' || 'filtered-text':
|
|
24
25
|
newVisualizationConfig.visualizationType = type
|
|
25
26
|
break
|
|
27
|
+
case 'table':
|
|
28
|
+
const tableConfig: Table = { label: 'Data Table', show: true, showDownloadUrl: false, showVertical: true, expanded: true }
|
|
29
|
+
newVisualizationConfig.table = tableConfig
|
|
30
|
+
newVisualizationConfig.columns = {}
|
|
31
|
+
newVisualizationConfig.dataFormat = {}
|
|
32
|
+
newVisualizationConfig.visualizationType = type
|
|
33
|
+
break
|
|
26
34
|
default:
|
|
27
35
|
newVisualizationConfig.visualizationType = type
|
|
28
36
|
break
|
|
@@ -39,6 +47,7 @@ const VisualizationsPanel = ({ loadConfig, config }) => (
|
|
|
39
47
|
<Widget addVisualization={() => addVisualization('chart', 'Bar')} type='Bar' />
|
|
40
48
|
<Widget addVisualization={() => addVisualization('chart', 'Line')} type='Line' />
|
|
41
49
|
<Widget addVisualization={() => addVisualization('chart', 'Pie')} type='Pie' />
|
|
50
|
+
<Widget addVisualization={() => addVisualization('chart', 'Sankey')} type='Sankey' />
|
|
42
51
|
</div>
|
|
43
52
|
<span className='subheading-3'>Map</span>
|
|
44
53
|
<div className='drag-grid'>
|
|
@@ -53,6 +62,7 @@ const VisualizationsPanel = ({ loadConfig, config }) => (
|
|
|
53
62
|
<Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
|
|
54
63
|
<Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
|
|
55
64
|
<Widget addVisualization={() => addVisualization('filter-dropdowns', '')} type='filter-dropdowns' />
|
|
65
|
+
<Widget addVisualization={() => addVisualization('table', '')} type='table' />
|
|
56
66
|
</div>
|
|
57
67
|
<span className='subheading-3'>Advanced</span>
|
|
58
68
|
<AdvancedEditor loadConfig={loadConfig} state={config} convertStateToConfig={undefined} />
|
|
@@ -11,24 +11,7 @@ import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
|
11
11
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
12
12
|
import Modal from '@cdc/core/components/ui/Modal'
|
|
13
13
|
import { Visualization } from '@cdc/core/types/Visualization'
|
|
14
|
-
|
|
15
|
-
const iconHash = {
|
|
16
|
-
'data-bite': <Icon display='databite' base />,
|
|
17
|
-
Bar: <Icon display='chartBar' base />,
|
|
18
|
-
'Spark Line': <Icon display='chartLine' />,
|
|
19
|
-
'waffle-chart': <Icon display='grid' base />,
|
|
20
|
-
'markup-include': <Icon display='code' base />,
|
|
21
|
-
Line: <Icon display='chartLine' base />,
|
|
22
|
-
Pie: <Icon display='chartPie' base />,
|
|
23
|
-
us: <Icon display='mapUsa' base />,
|
|
24
|
-
'us-county': <Icon display='mapUsa' base />,
|
|
25
|
-
world: <Icon display='mapWorld' base />,
|
|
26
|
-
'single-state': <Icon display='mapAl' base />,
|
|
27
|
-
gear: <Icon display='gear' base />,
|
|
28
|
-
tools: <Icon display='tools' base />,
|
|
29
|
-
'filtered-text': <Icon display='filtered-text' base />,
|
|
30
|
-
'filter-dropdowns': <Icon display='filter-dropdowns' base />
|
|
31
|
-
}
|
|
14
|
+
import { iconHash } from '../helpers/iconHash'
|
|
32
15
|
|
|
33
16
|
const labelHash = {
|
|
34
17
|
'data-bite': 'Data Bite',
|
|
@@ -43,7 +26,9 @@ const labelHash = {
|
|
|
43
26
|
world: 'World',
|
|
44
27
|
'single-state': 'U.S. State',
|
|
45
28
|
'filtered-text': 'Filtered Text',
|
|
46
|
-
'filter-dropdowns': 'Filter Dropdowns'
|
|
29
|
+
'filter-dropdowns': 'Filter Dropdowns',
|
|
30
|
+
Sankey: 'Sankey Chart',
|
|
31
|
+
table: 'Table'
|
|
47
32
|
}
|
|
48
33
|
|
|
49
34
|
type WidgetData = Visualization & { rowIdx: number; colIdx: number }
|
|
@@ -56,7 +41,6 @@ type WidgetProps = {
|
|
|
56
41
|
const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
57
42
|
const { overlay } = useGlobalContext()
|
|
58
43
|
const { config } = useContext(DashboardContext)
|
|
59
|
-
if (!config) return null
|
|
60
44
|
const rows = config.rows
|
|
61
45
|
const visualizations = config.visualizations
|
|
62
46
|
const dispatch = useContext(DashboardDispatchContext)
|
|
@@ -269,16 +253,16 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
269
253
|
}
|
|
270
254
|
}, [data?.openModal])
|
|
271
255
|
|
|
272
|
-
let isConfigurationReady = false
|
|
273
|
-
if(type === 'markup-include' || type === 'filter-dropdowns'){
|
|
274
|
-
isConfigurationReady = true
|
|
275
|
-
} else if(data && data.formattedData) {
|
|
276
|
-
isConfigurationReady = true
|
|
277
|
-
} else if(data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]){
|
|
278
|
-
let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data)
|
|
279
|
-
formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription)
|
|
280
|
-
if(formattedDataAttempt){
|
|
281
|
-
isConfigurationReady = true
|
|
256
|
+
let isConfigurationReady = false
|
|
257
|
+
if (type === 'markup-include' || type === 'filter-dropdowns') {
|
|
258
|
+
isConfigurationReady = true
|
|
259
|
+
} else if (data && data.formattedData) {
|
|
260
|
+
isConfigurationReady = true
|
|
261
|
+
} else if (data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]) {
|
|
262
|
+
let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data)
|
|
263
|
+
formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription)
|
|
264
|
+
if (formattedDataAttempt) {
|
|
265
|
+
isConfigurationReady = true
|
|
282
266
|
}
|
|
283
267
|
}
|
|
284
268
|
|
|
@@ -1,73 +1,96 @@
|
|
|
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
|
-
|
|
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
|
+
}
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
function filter(data, filters, condition) {
|
|
26
|
+
return data ? data.filter(row => {
|
|
27
|
+
const found = filters.find(filter => {
|
|
28
|
+
if (filter.pivot) return false
|
|
29
|
+
const currentValue = row[filter.columnName]
|
|
30
|
+
const selectedValue = filter.queuedActive || filter.active
|
|
31
|
+
const isNotTheSelectedValue = selectedValue && currentValue != selectedValue
|
|
32
|
+
const isFirstOccurrenceOfTier = filter.tier === condition
|
|
33
|
+
if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
|
|
34
|
+
return true
|
|
24
35
|
}
|
|
25
36
|
})
|
|
37
|
+
return !found
|
|
38
|
+
}) : []
|
|
39
|
+
}
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
filteredData.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
41
|
+
function setFilterValuesAndActiveFilter(filters: SharedFilter[], filteredData: Object[], i: number) {
|
|
42
|
+
filters.forEach(sharedFilter => {
|
|
43
|
+
if (sharedFilter.pivot) {
|
|
44
|
+
sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
|
|
45
|
+
} else if (sharedFilter.tier === i + 2 && !Array.isArray(sharedFilter.active)) {
|
|
46
|
+
sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
|
|
47
|
+
const valueAlreadySelected = sharedFilter.values.includes(sharedFilter.active)
|
|
48
|
+
if (!valueAlreadySelected && sharedFilter.values.length > 0) {
|
|
49
|
+
sharedFilter.active = sharedFilter.values[0]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
const pivotData = (data, pivotFilter: SharedFilter) => {
|
|
56
|
+
const pivotActive = pivotFilter.active as string[]
|
|
57
|
+
const inactive = pivotFilter.values.filter(value => !pivotActive.includes(value))
|
|
58
|
+
const pivotColumn = pivotFilter.columnName
|
|
59
|
+
const valueColumn = pivotFilter.pivot
|
|
60
|
+
const grouped = _.groupBy(data, val => val[pivotColumn])
|
|
61
|
+
const newData = []
|
|
62
|
+
for (const key in grouped) {
|
|
63
|
+
const group = grouped[key]
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
group.forEach((val, index) => {
|
|
66
|
+
const row = newData[index] || {}
|
|
67
|
+
if (!inactive.includes(key)) row[key] = val[valueColumn]
|
|
68
|
+
const toAdd = _.omit(val, [pivotColumn, valueColumn, ...inactive])
|
|
69
|
+
newData[index] = { ...row, ...toAdd }
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return newData
|
|
73
|
+
}
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
/** This function returns filtered data.
|
|
76
|
+
* It also manipulates the filters by adding: tiers, filterOptions, and default selections */
|
|
77
|
+
export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
|
|
78
|
+
const maxTier = getMaxTierAndSetFilterTiers(filters)
|
|
56
79
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
let add = true
|
|
80
|
+
for (let i = 0; i < maxTier; i++) {
|
|
81
|
+
const lastIteration = i === maxTier - 1
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
// eslint-disable-next-line eqeqeq
|
|
63
|
-
if (filter.type !== 'urlfilter' && filter.tier && filter.tier === maxTier - 1 && (filter.queuedActive || filter.active) && row[filter.columnName!] != (filter.queuedActive || filter.active)) {
|
|
64
|
-
add = false
|
|
65
|
-
}
|
|
66
|
-
})
|
|
83
|
+
const filteredData = filter(_data, filters, i + 1)
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
})
|
|
85
|
+
setFilterValuesAndActiveFilter(filters, filteredData, i)
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
if (lastIteration) {
|
|
88
|
+
const pivotFilter = filters.find(filter => filter.pivot)
|
|
89
|
+
if (pivotFilter) {
|
|
90
|
+
return pivotData(filteredData, pivotFilter)
|
|
91
|
+
}
|
|
92
|
+
// not sure if this last run of filter() function is necessary.
|
|
93
|
+
return filter(filteredData, filters, maxTier - 1)
|
|
94
|
+
}
|
|
72
95
|
}
|
|
73
96
|
}
|
|
@@ -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
|
})
|
|
@@ -5,6 +5,8 @@ import { generateValuesForFilter } from './generateValuesForFilter'
|
|
|
5
5
|
import { getFormattedData } from './getFormattedData'
|
|
6
6
|
import { getVizKeys } from './getVizKeys'
|
|
7
7
|
|
|
8
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
9
|
+
|
|
8
10
|
type UpdateState = Omit<DashboardState, 'config'> & {
|
|
9
11
|
config?: DashboardConfig
|
|
10
12
|
}
|
|
@@ -14,17 +16,28 @@ export const getUpdateConfig =
|
|
|
14
16
|
(newConfig, dataOverride?: Object): [Config, Object] => {
|
|
15
17
|
let newFilteredData = {}
|
|
16
18
|
let visualizationKeys = getVizKeys(newConfig)
|
|
17
|
-
|
|
18
|
-
newConfig.dashboard.sharedFilters[filterIndex][key] = value
|
|
19
|
-
}
|
|
19
|
+
|
|
20
20
|
if (newConfig.dashboard.sharedFilters) {
|
|
21
21
|
newConfig.dashboard.sharedFilters.forEach((filter, i) => {
|
|
22
22
|
const filterIsSetByVizData = !!visualizationKeys.find(key => key === filter.setBy)
|
|
23
|
-
|
|
23
|
+
const _filter = newConfig.dashboard.sharedFilters[i]
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const setValuesAndActive = filterValues => {
|
|
26
|
+
_filter.values = filterValues
|
|
27
|
+
if (filterValues.length > 0) {
|
|
28
|
+
const defaultValues = _filter.pivot ? _filter.values : _filter.values[0]
|
|
29
|
+
|
|
30
|
+
const queryStringFilterValue = getQueryStringFilterValue(_filter)
|
|
31
|
+
if(queryStringFilterValue){
|
|
32
|
+
_filter.active = queryStringFilterValue
|
|
33
|
+
} else {
|
|
34
|
+
_filter.active = _filter.active || defaultValues
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
27
38
|
|
|
39
|
+
const filterValues = generateValuesForFilter(filter.columnName, dataOverride || state.data)
|
|
40
|
+
if (filterIsSetByVizData) {
|
|
28
41
|
if (_filter.order === 'asc') {
|
|
29
42
|
filterValues.sort()
|
|
30
43
|
}
|
|
@@ -32,32 +45,21 @@ export const getUpdateConfig =
|
|
|
32
45
|
filterValues.sort().reverse()
|
|
33
46
|
}
|
|
34
47
|
|
|
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
|
-
}
|
|
48
|
+
setValuesAndActive(filterValues)
|
|
49
|
+
} else if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
|
|
50
|
+
setValuesAndActive(filterValues)
|
|
49
51
|
}
|
|
50
52
|
})
|
|
51
53
|
|
|
52
54
|
visualizationKeys.forEach(visualizationKey => {
|
|
53
|
-
|
|
55
|
+
const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
|
|
54
56
|
|
|
55
57
|
if (applicableFilters.length > 0) {
|
|
56
58
|
const visualization = newConfig.visualizations[visualizationKey]
|
|
57
59
|
const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
|
|
58
60
|
const formattedData = getFormattedData(_newConfigDataSet?.data || visualization.data, visualization.dataDescription)
|
|
59
61
|
const _data = formattedData || (dataOverride || state.data)[visualization.dataKey]
|
|
60
|
-
newFilteredData[visualizationKey] = filterData(applicableFilters, _data
|
|
62
|
+
newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
|
|
61
63
|
}
|
|
62
64
|
})
|
|
63
65
|
}
|
|
@@ -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
package/src/scss/main.scss
CHANGED
|
@@ -187,6 +187,9 @@
|
|
|
187
187
|
.dashboard-row {
|
|
188
188
|
display: flex;
|
|
189
189
|
flex-direction: column;
|
|
190
|
+
&.toggle {
|
|
191
|
+
display: block;
|
|
192
|
+
}
|
|
190
193
|
}
|
|
191
194
|
|
|
192
195
|
.dashboard-col {
|
|
@@ -197,6 +200,9 @@
|
|
|
197
200
|
margin-left: 0;
|
|
198
201
|
margin-right: 0;
|
|
199
202
|
}
|
|
203
|
+
&.hidden-toggle {
|
|
204
|
+
display: none;
|
|
205
|
+
}
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
.dashboard-col-12 {
|