@cdc/dashboard 4.24.5 → 4.24.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcdashboard.js +144406 -127510
- package/examples/custom/css/respiratory.css +236 -0
- package/examples/custom/js/respiratory.js +242 -0
- package/examples/default-multi-dataset-shared-filter.json +1729 -0
- package/examples/ed-visits-county-file.json +618 -0
- package/examples/filtered-dash.json +6 -21
- package/examples/single-state-dashboard-filters.json +421 -0
- package/examples/state-level.json +90136 -0
- package/examples/state-points.json +10474 -0
- package/examples/test-file.json +147 -0
- package/examples/testing.json +94456 -0
- package/index.html +25 -4
- package/package.json +12 -11
- package/src/CdcDashboard.tsx +5 -1
- package/src/CdcDashboardComponent.tsx +250 -327
- package/src/DashboardContext.tsx +15 -1
- package/src/_stories/Dashboard.stories.tsx +158 -40
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/bump-chart.json +3554 -0
- package/src/_stories/_mock/methodology.json +412 -0
- package/src/_stories/_mock/methodologyAPI.ts +90 -0
- package/src/_stories/_mock/multi-viz.json +3 -4
- package/src/_stories/_mock/pivot-filter.json +14 -12
- package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
- package/src/components/CollapsibleVisualizationRow.tsx +44 -0
- package/src/components/Column.tsx +1 -1
- package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
- package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
- package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
- package/src/components/DashboardFilters/index.ts +3 -0
- package/src/components/DataDesignerModal.tsx +9 -9
- package/src/components/ExpandCollapseButtons.tsx +20 -0
- package/src/components/Header/Header.tsx +1 -102
- package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
- package/src/components/Row.tsx +52 -19
- package/src/components/Toggle/Toggle.tsx +2 -4
- package/src/components/VisualizationRow.tsx +169 -30
- package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
- package/src/components/VisualizationsPanel/index.ts +1 -0
- package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
- package/src/components/Widget.tsx +27 -90
- package/src/helpers/FilterBehavior.ts +4 -0
- package/src/helpers/addValuesToDashboardFilters.ts +49 -0
- package/src/helpers/apiFilterHelpers.ts +102 -0
- package/src/helpers/changeFilterActive.ts +39 -0
- package/src/helpers/filterData.ts +10 -48
- package/src/helpers/generateValuesForFilter.ts +1 -1
- package/src/helpers/getAutoLoadVisualization.ts +11 -0
- package/src/helpers/getFilteredData.ts +7 -5
- package/src/helpers/getVizConfig.ts +23 -2
- package/src/helpers/getVizRowColumnLocator.ts +2 -1
- package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
- package/src/helpers/iconHash.tsx +5 -3
- package/src/helpers/loadAPIFilters.ts +74 -0
- package/src/helpers/mapDataToConfig.ts +29 -0
- package/src/helpers/processData.ts +2 -3
- package/src/helpers/reloadURLHelpers.ts +78 -0
- package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
- package/src/helpers/tests/apiFilterHelpers.test.ts +156 -0
- package/src/helpers/tests/filterData.test.ts +1 -93
- package/src/helpers/tests/getFilteredData.test.ts +86 -0
- package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +176 -0
- package/src/helpers/tests/reloadURLHelpers.test.ts +195 -0
- package/src/scss/editor-panel.scss +1 -1
- package/src/scss/grid.scss +34 -27
- package/src/scss/main.scss +41 -3
- package/src/scss/variables.scss +4 -0
- package/src/store/dashboard.actions.ts +12 -4
- package/src/store/dashboard.reducer.ts +30 -4
- package/src/types/APIFilter.ts +1 -5
- package/src/types/ConfigRow.ts +2 -0
- package/src/types/Dashboard.ts +1 -1
- package/src/types/DashboardConfig.ts +2 -4
- package/src/types/DashboardFilters.ts +7 -0
- package/src/types/InitialState.ts +1 -1
- package/src/types/MultiDashboard.ts +2 -2
- package/src/types/SharedFilter.ts +4 -6
- package/src/types/Tab.ts +1 -1
- package/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'
|
|
@@ -19,25 +18,26 @@ const labelHash = {
|
|
|
19
18
|
Bar: 'Bar',
|
|
20
19
|
Line: 'Line',
|
|
21
20
|
'Spark Line': 'Spark Line',
|
|
21
|
+
'Bump Chart': 'Bump Chart',
|
|
22
22
|
Pie: 'Pie',
|
|
23
23
|
us: 'United States (State- or County-Level)',
|
|
24
24
|
'us-county': 'United States (State- or County-Level)',
|
|
25
25
|
world: 'World',
|
|
26
26
|
'single-state': 'U.S. State',
|
|
27
27
|
'filtered-text': 'Filtered Text',
|
|
28
|
-
|
|
28
|
+
dashboardFilters: 'Filter Dropdowns',
|
|
29
29
|
Sankey: 'Sankey Chart',
|
|
30
30
|
table: 'Table'
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
type
|
|
33
|
+
type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
|
|
34
34
|
type WidgetProps = {
|
|
35
|
-
|
|
35
|
+
widgetConfig?: WidgetConfig
|
|
36
36
|
addVisualization?: Function
|
|
37
37
|
type: string
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const Widget = ({
|
|
40
|
+
const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
|
|
41
41
|
const { overlay } = useGlobalContext()
|
|
42
42
|
const { config } = useContext(DashboardContext)
|
|
43
43
|
const rows = config.rows
|
|
@@ -54,10 +54,10 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
54
54
|
|
|
55
55
|
const { rowIdx, colIdx } = result
|
|
56
56
|
|
|
57
|
-
if (undefined !==
|
|
58
|
-
rows[
|
|
57
|
+
if (undefined !== widgetConfig?.rowIdx) {
|
|
58
|
+
rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null // Wipe from old position
|
|
59
59
|
|
|
60
|
-
rows[rowIdx].columns[colIdx].widget =
|
|
60
|
+
rows[rowIdx].columns[colIdx].widget = widgetConfig.uid // Add to new row and col
|
|
61
61
|
} else if (!!addVisualization) {
|
|
62
62
|
// Item does not exist, instantiate a new one
|
|
63
63
|
const newViz = addVisualization()
|
|
@@ -76,19 +76,19 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
76
76
|
isDragging: monitor.isDragging()
|
|
77
77
|
})
|
|
78
78
|
},
|
|
79
|
-
[config.activeDashboard, config.rows]
|
|
79
|
+
[config.activeDashboard, config.rows, config.dashboard.sharedFilters]
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
const deleteWidget = () => {
|
|
83
|
-
if (!
|
|
84
|
-
rows[
|
|
83
|
+
if (!widgetConfig) return
|
|
84
|
+
rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null
|
|
85
85
|
|
|
86
|
-
delete visualizations[
|
|
86
|
+
delete visualizations[widgetConfig.uid]
|
|
87
87
|
|
|
88
88
|
if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
|
|
89
89
|
config.dashboard.sharedFilters.forEach(sharedFilter => {
|
|
90
|
-
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(
|
|
91
|
-
sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(
|
|
90
|
+
if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(widgetConfig.uid) !== -1) {
|
|
91
|
+
sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(widgetConfig.uid), 1)
|
|
92
92
|
}
|
|
93
93
|
})
|
|
94
94
|
}
|
|
@@ -97,99 +97,36 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const editWidget = () => {
|
|
100
|
-
if (!
|
|
101
|
-
visualizations[
|
|
100
|
+
if (!widgetConfig) return
|
|
101
|
+
visualizations[widgetConfig.uid].editing = true
|
|
102
102
|
|
|
103
103
|
updateConfig({ ...config, visualizations })
|
|
104
104
|
}
|
|
105
105
|
|
|
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
106
|
let isConfigurationReady = false
|
|
170
|
-
const dataConfiguredForRow = !!rows[
|
|
171
|
-
if (dataConfiguredForRow || ['
|
|
107
|
+
const dataConfiguredForRow = !!rows[widgetConfig?.rowIdx]?.dataKey
|
|
108
|
+
if (dataConfiguredForRow || ['dashboardFilters', 'markup-include'].includes(type)) {
|
|
172
109
|
isConfigurationReady = true
|
|
173
110
|
} else {
|
|
174
|
-
if (
|
|
111
|
+
if (widgetConfig?.formattedData) {
|
|
175
112
|
isConfigurationReady = true
|
|
176
|
-
} else if (
|
|
177
|
-
const formattedDataAttempt = transform.autoStandardize(config.datasets[
|
|
178
|
-
const canFormatData = !!transform.developerStandardize(formattedDataAttempt,
|
|
113
|
+
} else if (widgetConfig?.dataKey && widgetConfig?.dataDescription && config.datasets[widgetConfig.dataKey]) {
|
|
114
|
+
const formattedDataAttempt = transform.autoStandardize(config.datasets[widgetConfig.dataKey].data)
|
|
115
|
+
const canFormatData = !!transform.developerStandardize(formattedDataAttempt, widgetConfig.dataDescription)
|
|
179
116
|
if (canFormatData) {
|
|
180
117
|
isConfigurationReady = true
|
|
181
118
|
}
|
|
182
119
|
}
|
|
183
120
|
}
|
|
184
121
|
|
|
185
|
-
const needsDataConfiguration = !dataConfiguredForRow
|
|
122
|
+
const needsDataConfiguration = !dataConfiguredForRow && widgetConfig?.type !== 'dashboardFilters'
|
|
186
123
|
|
|
187
124
|
return (
|
|
188
125
|
<>
|
|
189
126
|
<div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
|
|
190
127
|
<Icon display='move' className='drag-icon' />
|
|
191
128
|
<div className='widget__content'>
|
|
192
|
-
{
|
|
129
|
+
{widgetConfig?.rowIdx !== undefined && (
|
|
193
130
|
<div className='widget-menu'>
|
|
194
131
|
{isConfigurationReady && (
|
|
195
132
|
<button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
|
|
@@ -201,7 +138,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
201
138
|
title='Configure Data'
|
|
202
139
|
className='btn btn-configure'
|
|
203
140
|
onClick={() => {
|
|
204
|
-
overlay?.actions.openOverlay(
|
|
141
|
+
overlay?.actions.openOverlay(<DataDesignerModal rowIndex={widgetConfig.rowIdx} vizKey={widgetConfig.uid} />)
|
|
205
142
|
}}
|
|
206
143
|
>
|
|
207
144
|
{iconHash['gear']}
|
|
@@ -214,7 +151,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
214
151
|
)}
|
|
215
152
|
{iconHash[type]}
|
|
216
153
|
<span>{labelHash[type]}</span>
|
|
217
|
-
{
|
|
154
|
+
{widgetConfig?.newViz && type !== 'dashboardFilters' && (
|
|
218
155
|
<span onClick={editWidget} className='config-needed'>
|
|
219
156
|
Configuration needed
|
|
220
157
|
</span>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
|
|
5
|
+
// Gets filter values from dataset
|
|
6
|
+
const generateValuesForFilter = (columnName, data: Record<string, any[]>) => {
|
|
7
|
+
const values: string[] = []
|
|
8
|
+
// data is a dataset this loops through ALL datasets to find matching values
|
|
9
|
+
// not sure if this is desired behavior
|
|
10
|
+
|
|
11
|
+
const d = Object.values(data) || []
|
|
12
|
+
d.forEach((rows: any[]) => {
|
|
13
|
+
rows?.forEach(row => {
|
|
14
|
+
const value = row[columnName]
|
|
15
|
+
if (value !== undefined && !values.includes(value)) {
|
|
16
|
+
values.push(String(value))
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
return values
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const getSelector = (filter: SharedFilter) => {
|
|
24
|
+
return filter.type === 'urlfilter' ? filter.apiFilter?.valueSelector : filter.columnName
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const addValuesToDashboardFilters = (filters: SharedFilter[], data: Record<string, any[]>): Array<SharedFilter> => {
|
|
28
|
+
return filters?.map(filter => {
|
|
29
|
+
if (filter.type === 'urlfilter') return filter
|
|
30
|
+
const filterCopy = _.cloneDeep(filter)
|
|
31
|
+
const filterValues = generateValuesForFilter(getSelector(filter), data)
|
|
32
|
+
filterCopy.values = filterValues
|
|
33
|
+
if (filterValues.length > 0) {
|
|
34
|
+
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
35
|
+
if (queryStringFilterValue) {
|
|
36
|
+
filterCopy.active = queryStringFilterValue
|
|
37
|
+
} else if (filter.multiSelect) {
|
|
38
|
+
const defaultValues = filterCopy.values
|
|
39
|
+
const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
40
|
+
filterCopy.active = active.filter(val => defaultValues.includes(val))
|
|
41
|
+
} else {
|
|
42
|
+
const defaultValue = filterCopy.values[0] || filterCopy.active
|
|
43
|
+
const active = Array.isArray(filterCopy.active) ? filterCopy.active[0] : filterCopy.active
|
|
44
|
+
filterCopy.active = filterCopy.values.includes(active) ? active : defaultValue
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return filterCopy
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
import _ from 'lodash'
|
|
6
|
+
import { getQueryParams } from '@cdc/core/helpers/queryStringUtils'
|
|
7
|
+
|
|
8
|
+
/** key for the dropdowns object */
|
|
9
|
+
type DropdownsKey = string
|
|
10
|
+
|
|
11
|
+
export const getLoadingFilterMemo = (
|
|
12
|
+
apiFiltersEndpoints: string[],
|
|
13
|
+
apiFilterDropdowns,
|
|
14
|
+
changedChildFilterIndexes = []
|
|
15
|
+
): APIFilterDropdowns =>
|
|
16
|
+
apiFiltersEndpoints.reduce((acc, endpoint, currIndex) => {
|
|
17
|
+
const _key: DropdownsKey = endpoint
|
|
18
|
+
const hasChanged = changedChildFilterIndexes.includes(currIndex)
|
|
19
|
+
if (apiFilterDropdowns[_key] != null && !hasChanged) {
|
|
20
|
+
acc[_key] = apiFilterDropdowns[_key]
|
|
21
|
+
} else {
|
|
22
|
+
acc[_key] = null
|
|
23
|
+
}
|
|
24
|
+
return acc
|
|
25
|
+
}, {})
|
|
26
|
+
|
|
27
|
+
const getParentParams = (
|
|
28
|
+
childFilter: SharedFilter,
|
|
29
|
+
sharedFilters: SharedFilter[]
|
|
30
|
+
): Record<'key' | 'value', string>[] | null => {
|
|
31
|
+
const _parents = sharedFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
|
|
32
|
+
if (!(_parents || []).length) return null
|
|
33
|
+
|
|
34
|
+
return _parents.flatMap(filter => {
|
|
35
|
+
const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
|
|
36
|
+
const value = filter.queuedActive || filter.active || ''
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map(_value => ({ key, value: _value.toString() }))
|
|
39
|
+
}
|
|
40
|
+
return [{ key, value: value.toString() }]
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
|
|
45
|
+
const { textSelector, valueSelector } = apiFilter
|
|
46
|
+
return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** API endpoint to fetch */
|
|
50
|
+
type Endpoint = string
|
|
51
|
+
type SharedFilterIndex = number
|
|
52
|
+
export const getToFetch = (
|
|
53
|
+
sharedFilters: SharedFilter[],
|
|
54
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
55
|
+
): Record<Endpoint, [DropdownsKey, SharedFilterIndex]> => {
|
|
56
|
+
const toFetch = {}
|
|
57
|
+
sharedFilters.forEach((filter, index) => {
|
|
58
|
+
const baseEndpoint = filter.apiFilter?.apiEndpoint
|
|
59
|
+
if (!baseEndpoint) return
|
|
60
|
+
const _key = baseEndpoint
|
|
61
|
+
if (apiFilterDropdowns[_key]) return // don't reload cached filter
|
|
62
|
+
const parentParams = getParentParams(filter, sharedFilters)
|
|
63
|
+
const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
|
|
64
|
+
|
|
65
|
+
if (notAllParentsSelected) return // don't send request for dependent children filter options
|
|
66
|
+
|
|
67
|
+
const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
|
|
68
|
+
toFetch[endpoint] = [_key, index]
|
|
69
|
+
})
|
|
70
|
+
return toFetch
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const setAutoLoadDefaultValue = (
|
|
74
|
+
sharedFilterIndex: number,
|
|
75
|
+
dropdownOptions: DropdownOptions,
|
|
76
|
+
sharedFilters,
|
|
77
|
+
autoLoadFilterIndexes: number[]
|
|
78
|
+
): SharedFilter => {
|
|
79
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
80
|
+
const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
|
|
81
|
+
if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) return sharedFilter // no autoLoading happening
|
|
82
|
+
if (autoLoadFilterIndexes.includes(sharedFilterIndex)) {
|
|
83
|
+
const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
|
|
84
|
+
const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
|
|
85
|
+
if (filterParents && notAllParentFiltersSelected) return sharedFilter
|
|
86
|
+
const defaultValue = sharedFilter.multiSelect ? [dropdownOptions[0]?.value] : dropdownOptions[0]?.value
|
|
87
|
+
if (!sharedFilter.active) {
|
|
88
|
+
const queryParams = getQueryParams()
|
|
89
|
+
const defaultQueryParamValue = queryParams[sharedFilter?.queryParameter]
|
|
90
|
+
sharedFilter.active = defaultQueryParamValue || defaultValue
|
|
91
|
+
} else if (sharedFilter.multiSelect) {
|
|
92
|
+
const currentOption = sharedFilter.active.filter(activeVal =>
|
|
93
|
+
dropdownOptions.find(option => option.value === activeVal)
|
|
94
|
+
)
|
|
95
|
+
sharedFilter.active = currentOption.length ? currentOption : defaultValue
|
|
96
|
+
} else {
|
|
97
|
+
const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
|
|
98
|
+
sharedFilter.active = currentOption ? currentOption.value : defaultValue
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return sharedFilter
|
|
102
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { FilterBehavior } from '../helpers/FilterBehavior'
|
|
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 childFilterIndexes = sharedFilters
|
|
10
|
+
.map((filter, index) => (filter.parents?.includes(parentKey) ? index : null))
|
|
11
|
+
.filter(i => i !== null)
|
|
12
|
+
if (childFilterIndexes.length) {
|
|
13
|
+
childFilterIndexes.forEach(filterIndex => {
|
|
14
|
+
sharedFilters[filterIndex].active = ''
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
return childFilterIndexes
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const changeFilterActive = (
|
|
21
|
+
filterIndex: number,
|
|
22
|
+
value: string | string[],
|
|
23
|
+
sharedFilters: SharedFilter[],
|
|
24
|
+
vizConfig: DashboardFilters
|
|
25
|
+
): [SharedFilter[], number[]] => {
|
|
26
|
+
const sharedFiltersCopy = _.cloneDeep(sharedFilters)
|
|
27
|
+
const currentFilter = sharedFilters[filterIndex]
|
|
28
|
+
if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
|
|
29
|
+
sharedFiltersCopy[filterIndex].active = value
|
|
30
|
+
const queryParams = getQueryParams()
|
|
31
|
+
if (currentFilter.setByQueryParameter && queryParams[currentFilter.setByQueryParameter] !== currentFilter.active) {
|
|
32
|
+
queryParams[currentFilter.setByQueryParameter] = currentFilter.active
|
|
33
|
+
updateQueryString(queryParams)
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
sharedFiltersCopy[filterIndex].queuedActive = value
|
|
37
|
+
}
|
|
38
|
+
return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
|
|
39
|
+
}
|
|
@@ -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
|
+
}
|