@cdc/dashboard 4.24.5 → 4.24.7
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 +122872 -112065
- 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/index.html +10 -1
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +165 -306
- package/src/DashboardContext.tsx +9 -1
- package/src/_stories/Dashboard.stories.tsx +38 -34
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/multi-viz.json +2 -3
- package/src/_stories/_mock/pivot-filter.json +14 -12
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +80 -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 +367 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -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 -97
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +82 -24
- 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 +26 -90
- package/src/helpers/apiFilterHelpers.ts +51 -0
- package/src/helpers/changeFilterActive.ts +30 -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 +4 -2
- 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 +3 -3
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +68 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- 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 +2 -5
- package/src/types/Tab.ts +1 -1
- package/LICENSE +0 -201
- 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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useContext, useState } from 'react'
|
|
2
|
+
import type { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
import Widget from '../Widget'
|
|
4
|
+
import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
|
|
5
|
+
import { Table } from '@cdc/core/types/Table'
|
|
6
|
+
import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
|
|
7
|
+
import { mapDataToConfig } from '../../helpers/mapDataToConfig'
|
|
8
|
+
import './visualizations-panel-styles.css'
|
|
9
|
+
|
|
10
|
+
const addVisualization = (type, subType) => {
|
|
11
|
+
const modalWillOpen = type !== 'markup-include'
|
|
12
|
+
const newVisualizationConfig: Partial<AnyVisualization> = {
|
|
13
|
+
filters: [],
|
|
14
|
+
filterBehavior: 'Filter Change',
|
|
15
|
+
newViz: type !== 'table',
|
|
16
|
+
openModal: modalWillOpen,
|
|
17
|
+
uid: type + Date.now(),
|
|
18
|
+
type
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'chart':
|
|
23
|
+
newVisualizationConfig.visualizationType = subType
|
|
24
|
+
break
|
|
25
|
+
case 'map':
|
|
26
|
+
newVisualizationConfig.general = {}
|
|
27
|
+
newVisualizationConfig.general.geoType = subType
|
|
28
|
+
break
|
|
29
|
+
case 'data-bite' || 'waffle-chart' || 'filtered-text':
|
|
30
|
+
newVisualizationConfig.visualizationType = type
|
|
31
|
+
break
|
|
32
|
+
case 'table':
|
|
33
|
+
const tableConfig: Table = { label: 'Data Table', show: true, showDownloadUrl: false, showVertical: true, expanded: true, collapsible: true }
|
|
34
|
+
newVisualizationConfig.table = tableConfig
|
|
35
|
+
newVisualizationConfig.columns = {}
|
|
36
|
+
newVisualizationConfig.dataFormat = {}
|
|
37
|
+
newVisualizationConfig.visualizationType = type
|
|
38
|
+
break
|
|
39
|
+
case 'markup-include':
|
|
40
|
+
newVisualizationConfig.contentEditor = {
|
|
41
|
+
inlineHTML: '<h2>Inline HTML</h2>',
|
|
42
|
+
markupVariables: [],
|
|
43
|
+
showHeader: true,
|
|
44
|
+
srcUrl: '#example',
|
|
45
|
+
title: 'Markup Include',
|
|
46
|
+
useInlineHTML: true
|
|
47
|
+
}
|
|
48
|
+
newVisualizationConfig.theme = 'theme-blue'
|
|
49
|
+
newVisualizationConfig.visual = {
|
|
50
|
+
border: false,
|
|
51
|
+
accent: false,
|
|
52
|
+
background: false,
|
|
53
|
+
hideBackgroundColor: false,
|
|
54
|
+
borderColorTheme: false
|
|
55
|
+
}
|
|
56
|
+
newVisualizationConfig.showEditorPanel = true
|
|
57
|
+
newVisualizationConfig.visualizationType = type
|
|
58
|
+
|
|
59
|
+
break
|
|
60
|
+
case 'dashboardFilters': {
|
|
61
|
+
newVisualizationConfig.sharedFilterIndexes = []
|
|
62
|
+
newVisualizationConfig.visualizationType = type
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
default:
|
|
66
|
+
newVisualizationConfig.visualizationType = type
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return newVisualizationConfig
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const VisualizationsPanel = () => {
|
|
74
|
+
const [advancedEditing, setAdvancedEditing] = useState(false)
|
|
75
|
+
const { config } = useContext(DashboardContext)
|
|
76
|
+
const dispatch = useContext(DashboardDispatchContext)
|
|
77
|
+
const loadConfig = newConfig => dispatch({ type: 'APPLY_CONFIG', payload: [mapDataToConfig(newConfig)] })
|
|
78
|
+
return (
|
|
79
|
+
<div className={`visualizations-panel${advancedEditing ? ' advanced-editor' : ''}`}>
|
|
80
|
+
<p style={{ fontSize: '14px' }}>Click and drag an item onto the grid to add it to your dashboard.</p>
|
|
81
|
+
<span className='subheading-3'>Chart</span>
|
|
82
|
+
<div className='drag-grid'>
|
|
83
|
+
<Widget addVisualization={() => addVisualization('chart', 'Bar')} type='Bar' />
|
|
84
|
+
<Widget addVisualization={() => addVisualization('chart', 'Line')} type='Line' />
|
|
85
|
+
<Widget addVisualization={() => addVisualization('chart', 'Pie')} type='Pie' />
|
|
86
|
+
<Widget addVisualization={() => addVisualization('chart', 'Sankey')} type='Sankey' />
|
|
87
|
+
</div>
|
|
88
|
+
<span className='subheading-3'>Map</span>
|
|
89
|
+
<div className='drag-grid'>
|
|
90
|
+
<Widget addVisualization={() => addVisualization('map', 'us')} type='us' />
|
|
91
|
+
<Widget addVisualization={() => addVisualization('map', 'world')} type='world' />
|
|
92
|
+
<Widget addVisualization={() => addVisualization('map', 'single-state')} type='single-state' />
|
|
93
|
+
</div>
|
|
94
|
+
<span className='subheading-3'>Misc.</span>
|
|
95
|
+
<div className='drag-grid'>
|
|
96
|
+
<Widget addVisualization={() => addVisualization('data-bite', '')} type='data-bite' />
|
|
97
|
+
<Widget addVisualization={() => addVisualization('waffle-chart', '')} type='waffle-chart' />
|
|
98
|
+
<Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
|
|
99
|
+
<Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
|
|
100
|
+
<Widget addVisualization={() => addVisualization('dashboardFilters', '')} type='dashboardFilters' />
|
|
101
|
+
<Widget addVisualization={() => addVisualization('table', '')} type='table' />
|
|
102
|
+
</div>
|
|
103
|
+
<span className='subheading-3'>Advanced</span>
|
|
104
|
+
<AdvancedEditor
|
|
105
|
+
loadConfig={loadConfig}
|
|
106
|
+
config={config}
|
|
107
|
+
convertStateToConfig={() => undefined}
|
|
108
|
+
onExpandCollapse={() => {
|
|
109
|
+
setAdvancedEditing(!advancedEditing)
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default VisualizationsPanel
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './VisualizationsPanel'
|
|
@@ -6,8 +6,7 @@ import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
|
|
|
6
6
|
|
|
7
7
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
8
8
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
9
|
-
import
|
|
10
|
-
import { Visualization } from '@cdc/core/types/Visualization'
|
|
9
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
11
10
|
import { iconHash } from '../helpers/iconHash'
|
|
12
11
|
import _ from 'lodash'
|
|
13
12
|
import { DataDesignerModal } from './DataDesignerModal'
|
|
@@ -25,19 +24,19 @@ const labelHash = {
|
|
|
25
24
|
world: 'World',
|
|
26
25
|
'single-state': 'U.S. State',
|
|
27
26
|
'filtered-text': 'Filtered Text',
|
|
28
|
-
|
|
27
|
+
dashboardFilters: 'Filter Dropdowns',
|
|
29
28
|
Sankey: 'Sankey Chart',
|
|
30
29
|
table: 'Table'
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
type
|
|
32
|
+
type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
|
|
34
33
|
type WidgetProps = {
|
|
35
|
-
|
|
34
|
+
widgetConfig?: WidgetConfig
|
|
36
35
|
addVisualization?: Function
|
|
37
36
|
type: string
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
const Widget = ({
|
|
39
|
+
const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
|
|
41
40
|
const { overlay } = useGlobalContext()
|
|
42
41
|
const { config } = useContext(DashboardContext)
|
|
43
42
|
const rows = config.rows
|
|
@@ -54,10 +53,10 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
54
53
|
|
|
55
54
|
const { rowIdx, colIdx } = result
|
|
56
55
|
|
|
57
|
-
if (undefined !==
|
|
58
|
-
rows[
|
|
56
|
+
if (undefined !== widgetConfig?.rowIdx) {
|
|
57
|
+
rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null // Wipe from old position
|
|
59
58
|
|
|
60
|
-
rows[rowIdx].columns[colIdx].widget =
|
|
59
|
+
rows[rowIdx].columns[colIdx].widget = widgetConfig.uid // Add to new row and col
|
|
61
60
|
} else if (!!addVisualization) {
|
|
62
61
|
// Item does not exist, instantiate a new one
|
|
63
62
|
const newViz = addVisualization()
|
|
@@ -76,19 +75,19 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
76
75
|
isDragging: monitor.isDragging()
|
|
77
76
|
})
|
|
78
77
|
},
|
|
79
|
-
[config.activeDashboard, config.rows]
|
|
78
|
+
[config.activeDashboard, config.rows, config.dashboard.sharedFilters]
|
|
80
79
|
)
|
|
81
80
|
|
|
82
81
|
const deleteWidget = () => {
|
|
83
|
-
if (!
|
|
84
|
-
rows[
|
|
82
|
+
if (!widgetConfig) return
|
|
83
|
+
rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null
|
|
85
84
|
|
|
86
|
-
delete visualizations[
|
|
85
|
+
delete visualizations[widgetConfig.uid]
|
|
87
86
|
|
|
88
87
|
if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
|
|
89
88
|
config.dashboard.sharedFilters.forEach(sharedFilter => {
|
|
90
|
-
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(
|
|
91
|
-
sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(
|
|
89
|
+
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(widgetConfig.uid) !== -1) {
|
|
90
|
+
sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(widgetConfig.uid), 1)
|
|
92
91
|
}
|
|
93
92
|
})
|
|
94
93
|
}
|
|
@@ -97,99 +96,36 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
const editWidget = () => {
|
|
100
|
-
if (!
|
|
101
|
-
visualizations[
|
|
99
|
+
if (!widgetConfig) return
|
|
100
|
+
visualizations[widgetConfig.uid].editing = true
|
|
102
101
|
|
|
103
102
|
updateConfig({ ...config, visualizations })
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
const FilterHideModal = configureData => {
|
|
107
|
-
const currentVizKey = Object.keys(visualizations).find(vizKey => vizKey === configureData.uid) || ''
|
|
108
|
-
const currentViz = config.visualizations && config.visualizations[currentVizKey]
|
|
109
|
-
const onFilterHideChange = (e, index) => {
|
|
110
|
-
const visualizations = { ...config.visualizations }
|
|
111
|
-
|
|
112
|
-
if (currentVizKey) {
|
|
113
|
-
const currentVizConfig = visualizations[currentVizKey]
|
|
114
|
-
|
|
115
|
-
if (currentVizConfig) {
|
|
116
|
-
if (!currentVizConfig.hide) currentVizConfig.hide = []
|
|
117
|
-
if (!e.target.checked && currentVizConfig.hide.indexOf(index) === -1) {
|
|
118
|
-
visualizations[currentVizKey].hide.push(index)
|
|
119
|
-
} else if (e.target.checked && currentVizConfig.hide.indexOf(index) !== -1) {
|
|
120
|
-
visualizations[currentVizKey].hide.splice(currentVizConfig.hide.indexOf(index), 1)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
updateConfig({ ...config, visualizations })
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const vizWithAutoLoad = Object.keys(config.visualizations).find(vizKey => config.visualizations[vizKey].autoLoad)
|
|
129
|
-
const onToggleAutoLoad = e => {
|
|
130
|
-
if (currentViz) {
|
|
131
|
-
currentViz.autoLoad = e.target.checked
|
|
132
|
-
updateConfig({ ...config, visualizations: { ...visualizations, [currentVizKey]: currentViz } })
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const showAutoLoadCheckbox = !vizWithAutoLoad || vizWithAutoLoad === currentVizKey
|
|
137
|
-
return (
|
|
138
|
-
<Modal>
|
|
139
|
-
<Modal.Content>
|
|
140
|
-
<div>Choose which filters to display:</div>
|
|
141
|
-
|
|
142
|
-
{config.dashboard.sharedFilters &&
|
|
143
|
-
config.dashboard.sharedFilters.length > 0 &&
|
|
144
|
-
config.dashboard.sharedFilters.map((sharedFilter, index) => (
|
|
145
|
-
<label>
|
|
146
|
-
<input type='checkbox' defaultChecked={!configureData.hide || configureData.hide.indexOf(index) === -1} onChange={e => onFilterHideChange(e, index)} />
|
|
147
|
-
{sharedFilter.key}
|
|
148
|
-
</label>
|
|
149
|
-
))}
|
|
150
|
-
|
|
151
|
-
{(!config.dashboard.sharedFilters || config.dashboard.sharedFilters.length === 0) && <>No dashboard filters added yet.</>}
|
|
152
|
-
|
|
153
|
-
{showAutoLoadCheckbox && (
|
|
154
|
-
<label>
|
|
155
|
-
Make Autoload:
|
|
156
|
-
<input type='checkbox' defaultChecked={currentViz?.autoLoad} onChange={onToggleAutoLoad} />
|
|
157
|
-
</label>
|
|
158
|
-
)}
|
|
159
|
-
<div>
|
|
160
|
-
<button style={{ margin: '1em' }} className='cove-button' onClick={() => overlay?.actions.toggleOverlay()}>
|
|
161
|
-
Continue
|
|
162
|
-
</button>
|
|
163
|
-
</div>
|
|
164
|
-
</Modal.Content>
|
|
165
|
-
</Modal>
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
105
|
let isConfigurationReady = false
|
|
170
|
-
const dataConfiguredForRow = !!rows[
|
|
171
|
-
if (dataConfiguredForRow || ['
|
|
106
|
+
const dataConfiguredForRow = !!rows[widgetConfig?.rowIdx]?.dataKey
|
|
107
|
+
if (dataConfiguredForRow || ['dashboardFilters', 'markup-include'].includes(type)) {
|
|
172
108
|
isConfigurationReady = true
|
|
173
109
|
} else {
|
|
174
|
-
if (
|
|
110
|
+
if (widgetConfig?.formattedData) {
|
|
175
111
|
isConfigurationReady = true
|
|
176
|
-
} else if (
|
|
177
|
-
const formattedDataAttempt = transform.autoStandardize(config.datasets[
|
|
178
|
-
const canFormatData = !!transform.developerStandardize(formattedDataAttempt,
|
|
112
|
+
} else if (widgetConfig?.dataKey && widgetConfig?.dataDescription && config.datasets[widgetConfig.dataKey]) {
|
|
113
|
+
const formattedDataAttempt = transform.autoStandardize(config.datasets[widgetConfig.dataKey].data)
|
|
114
|
+
const canFormatData = !!transform.developerStandardize(formattedDataAttempt, widgetConfig.dataDescription)
|
|
179
115
|
if (canFormatData) {
|
|
180
116
|
isConfigurationReady = true
|
|
181
117
|
}
|
|
182
118
|
}
|
|
183
119
|
}
|
|
184
120
|
|
|
185
|
-
const needsDataConfiguration = !dataConfiguredForRow
|
|
121
|
+
const needsDataConfiguration = !dataConfiguredForRow && widgetConfig?.type !== 'dashboardFilters'
|
|
186
122
|
|
|
187
123
|
return (
|
|
188
124
|
<>
|
|
189
125
|
<div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
|
|
190
126
|
<Icon display='move' className='drag-icon' />
|
|
191
127
|
<div className='widget__content'>
|
|
192
|
-
{
|
|
128
|
+
{widgetConfig?.rowIdx !== undefined && (
|
|
193
129
|
<div className='widget-menu'>
|
|
194
130
|
{isConfigurationReady && (
|
|
195
131
|
<button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
|
|
@@ -201,7 +137,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
201
137
|
title='Configure Data'
|
|
202
138
|
className='btn btn-configure'
|
|
203
139
|
onClick={() => {
|
|
204
|
-
overlay?.actions.openOverlay(
|
|
140
|
+
overlay?.actions.openOverlay(<DataDesignerModal rowIndex={widgetConfig.rowIdx} vizKey={widgetConfig.uid} />)
|
|
205
141
|
}}
|
|
206
142
|
>
|
|
207
143
|
{iconHash['gear']}
|
|
@@ -214,7 +150,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
214
150
|
)}
|
|
215
151
|
{iconHash[type]}
|
|
216
152
|
<span>{labelHash[type]}</span>
|
|
217
|
-
{
|
|
153
|
+
{widgetConfig?.newViz && type !== 'dashboardFilters' && (
|
|
218
154
|
<span onClick={editWidget} className='config-needed'>
|
|
219
155
|
Configuration needed
|
|
220
156
|
</span>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
|
|
2
|
+
import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
|
|
3
|
+
import { APIFilter } from '../types/APIFilter'
|
|
4
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
5
|
+
|
|
6
|
+
export const getLoadingFilterMemo = (sharedAPIFilters, apiFilterDropdowns): APIFilterDropdowns =>
|
|
7
|
+
sharedAPIFilters.reduce((acc, curr) => {
|
|
8
|
+
const _key = curr.apiFilter.apiEndpoint
|
|
9
|
+
if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
|
|
10
|
+
acc[_key] = null
|
|
11
|
+
return acc
|
|
12
|
+
}, {})
|
|
13
|
+
|
|
14
|
+
const getParentParams = (childFilter: SharedFilter, sharedAPIFilters: SharedFilter[]): Record<'key' | 'value', string>[] | null => {
|
|
15
|
+
const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
|
|
16
|
+
if (!_parents.length) return null
|
|
17
|
+
|
|
18
|
+
return _parents.flatMap(filter => {
|
|
19
|
+
const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
|
|
20
|
+
const value = filter.queuedActive || filter.active || ''
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map(_value => ({ key, value: _value }))
|
|
23
|
+
}
|
|
24
|
+
return [{ key, value }]
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
29
|
+
const { textSelector, valueSelector } = apiFilter
|
|
30
|
+
return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const getToFetch = (sharedAPIFilters: SharedFilter[], apiFilterDropdowns: APIFilterDropdowns): Record<string, [string, number]> => {
|
|
34
|
+
const toFetch = {}
|
|
35
|
+
sharedAPIFilters.forEach((filter, index) => {
|
|
36
|
+
const baseEndpoint = filter.apiFilter.apiEndpoint
|
|
37
|
+
const _key = baseEndpoint
|
|
38
|
+
const isAPIFilter = apiFilterDropdowns[_key]
|
|
39
|
+
const parentParams = getParentParams(filter, sharedAPIFilters)
|
|
40
|
+
const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
|
|
41
|
+
|
|
42
|
+
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
43
|
+
if (isAPIFilter && !parentParams) return // don't reload filter unless it's a child
|
|
44
|
+
const topLevelDataAlreadyLoaded = isAPIFilter && !filter.parents
|
|
45
|
+
if (topLevelDataAlreadyLoaded) return // don't reload top level filters
|
|
46
|
+
|
|
47
|
+
const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
|
|
48
|
+
toFetch[endpoint] = [_key, index]
|
|
49
|
+
})
|
|
50
|
+
return toFetch
|
|
51
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { FilterBehavior } from '../components/Header/Header'
|
|
3
|
+
import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
|
|
4
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
5
|
+
import { DashboardFilters } from '../types/DashboardFilters'
|
|
6
|
+
|
|
7
|
+
const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
|
|
8
|
+
const parentKey = sharedFilters[parentIndex].key
|
|
9
|
+
const childIndex = sharedFilters.findIndex(filter => filter.parents?.includes(parentKey))
|
|
10
|
+
if (childIndex !== -1) {
|
|
11
|
+
sharedFilters[childIndex].active = ''
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const changeFilterActive = (filterIndex: number, value: string | string[], sharedFilters: SharedFilter[], vizConfig: DashboardFilters): SharedFilter[] => {
|
|
16
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
17
|
+
const currentFilter = sharedFilters[filterIndex]
|
|
18
|
+
if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
|
|
19
|
+
sharedFiltersCopy[filterIndex].active = value
|
|
20
|
+
handleChildren(sharedFiltersCopy, filterIndex)
|
|
21
|
+
const queryParams = getQueryParams()
|
|
22
|
+
if (currentFilter.setByQueryParameter && queryParams[currentFilter.setByQueryParameter] !== currentFilter.active) {
|
|
23
|
+
queryParams[currentFilter.setByQueryParameter] = currentFilter.active
|
|
24
|
+
updateQueryString(queryParams)
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
sharedFiltersCopy[filterIndex].queuedActive = value
|
|
28
|
+
}
|
|
29
|
+
return sharedFiltersCopy
|
|
30
|
+
}
|
|
@@ -22,58 +22,26 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
|
22
22
|
return maxTier
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function filter(data, filters, condition) {
|
|
26
|
-
return data
|
|
27
|
-
const
|
|
28
|
-
if (filter.pivot) return false
|
|
25
|
+
function filter(data = [], filters: SharedFilter[], condition) {
|
|
26
|
+
return data.filter(row => {
|
|
27
|
+
const foundMatchingFilter = filters.find(filter => {
|
|
29
28
|
const currentValue = row[filter.columnName]
|
|
30
29
|
const selectedValue = filter.queuedActive || filter.active
|
|
31
|
-
|
|
30
|
+
let isNotTheSelectedValue = true
|
|
31
|
+
if (Array.isArray(selectedValue)) {
|
|
32
|
+
isNotTheSelectedValue = !selectedValue.includes(currentValue)
|
|
33
|
+
} else {
|
|
34
|
+
isNotTheSelectedValue = selectedValue && currentValue != selectedValue
|
|
35
|
+
}
|
|
32
36
|
const isFirstOccurrenceOfTier = filter.tier === condition
|
|
33
37
|
if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
|
|
34
38
|
return true
|
|
35
39
|
}
|
|
36
40
|
})
|
|
37
|
-
return !
|
|
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
|
-
}
|
|
41
|
+
return !foundMatchingFilter
|
|
52
42
|
})
|
|
53
43
|
}
|
|
54
44
|
|
|
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]
|
|
64
|
-
|
|
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] = { ...toAdd, ...row }
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
return newData
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** This function returns filtered data.
|
|
76
|
-
* It also manipulates the filters by adding: tiers, filterOptions, and default selections */
|
|
77
45
|
export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
|
|
78
46
|
const maxTier = getMaxTierAndSetFilterTiers(filters)
|
|
79
47
|
|
|
@@ -82,13 +50,7 @@ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] =
|
|
|
82
50
|
|
|
83
51
|
const filteredData = filter(_data, filters, i + 1)
|
|
84
52
|
|
|
85
|
-
setFilterValuesAndActiveFilter(filters, filteredData, i)
|
|
86
|
-
|
|
87
53
|
if (lastIteration) {
|
|
88
|
-
const pivotFilter = filters.find(filter => filter.pivot)
|
|
89
|
-
if (pivotFilter) {
|
|
90
|
-
return pivotData(filteredData, pivotFilter)
|
|
91
|
-
}
|
|
92
54
|
// not sure if this last run of filter() function is necessary.
|
|
93
55
|
return filter(filteredData, filters, maxTier - 1)
|
|
94
56
|
}
|
|
@@ -5,7 +5,7 @@ export const generateValuesForFilter = (columnName, _data) => {
|
|
|
5
5
|
Object.keys(_data).forEach(key => {
|
|
6
6
|
_data[key]?.forEach(row => {
|
|
7
7
|
const value = row[columnName]
|
|
8
|
-
if (!values.includes(value)) {
|
|
8
|
+
if (value && !values.includes(value)) {
|
|
9
9
|
values.push(value)
|
|
10
10
|
}
|
|
11
11
|
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AnyVisualization } from '@cdc/core/types/Visualization'
|
|
2
|
+
import { DashboardFilters } from '../types/DashboardFilters'
|
|
3
|
+
|
|
4
|
+
export const getAutoLoadVisualization = (visualizations: Record<string, AnyVisualization>): DashboardFilters | undefined => {
|
|
5
|
+
const autoLoadViz = Object.values(visualizations).filter(vis => {
|
|
6
|
+
return vis.type === 'dashboardFilters' && vis.autoLoad
|
|
7
|
+
}) as DashboardFilters[]
|
|
8
|
+
if (autoLoadViz.length === 0) return
|
|
9
|
+
if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
|
|
10
|
+
return autoLoadViz[0]
|
|
11
|
+
}
|
|
@@ -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 = 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,5 +1,5 @@
|
|
|
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 />,
|
|
@@ -16,12 +16,12 @@ export const iconHash = {
|
|
|
16
16
|
gear: <Icon display='gear' base />,
|
|
17
17
|
tools: <Icon display='tools' base />,
|
|
18
18
|
'filtered-text': <Icon display='filtered-text' base />,
|
|
19
|
-
|
|
19
|
+
dashboardFilters: <Icon display='dashboardFilters' base />,
|
|
20
20
|
table: <Icon display='table' base />,
|
|
21
21
|
Sankey: <Icon display='sankey' base />
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export const getIcon = (visualization:
|
|
24
|
+
export const getIcon = (visualization: AnyVisualization) => {
|
|
25
25
|
const { type, visualizationType, general } = visualization
|
|
26
26
|
if (visualizationType) return iconHash[visualizationType]
|
|
27
27
|
if (general?.geoType) {
|
|
@@ -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
|
+
}
|