@cdc/dashboard 4.24.3 → 4.24.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcdashboard.js +119414 -103311
- package/examples/chart-data.json +5409 -0
- package/examples/full-dash-test.json +14643 -0
- package/examples/full-dashboard.json +10036 -0
- package/index.html +2 -2
- package/package.json +9 -9
- package/src/CdcDashboard.tsx +8 -3
- package/src/CdcDashboardComponent.tsx +232 -344
- package/src/_stories/Dashboard.stories.tsx +59 -38
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/dashboard-gallery.json +523 -534
- package/src/_stories/_mock/multi-viz.json +378 -0
- package/src/_stories/_mock/pivot-filter.json +0 -2
- package/src/components/DataDesignerModal.tsx +145 -0
- package/src/components/Grid.tsx +3 -1
- package/src/components/Header/FilterModal.tsx +49 -23
- package/src/components/Row.tsx +50 -25
- package/src/components/Toggle/Toggle.tsx +6 -7
- package/src/components/VisualizationRow.tsx +174 -0
- package/src/components/Widget.tsx +21 -103
- package/src/helpers/filterData.ts +16 -14
- package/src/helpers/getFilteredData.ts +39 -0
- package/src/helpers/getUpdateConfig.ts +15 -0
- package/src/helpers/getVizConfig.ts +31 -0
- package/src/helpers/getVizRowColumnLocator.ts +9 -0
- package/src/scss/grid.scss +9 -2
- package/src/scss/main.scss +5 -0
- package/src/store/dashboard.actions.ts +16 -1
- package/src/store/dashboard.reducer.ts +25 -2
- package/src/types/APIFilter.ts +4 -5
- package/src/types/ConfigRow.ts +12 -3
- package/src/types/DataSet.ts +11 -8
- package/src/types/SharedFilter.ts +1 -1
package/src/components/Row.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, useState } from 'react'
|
|
1
|
+
import React, { useContext, useMemo, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
|
|
4
4
|
|
|
@@ -13,41 +13,53 @@ import FourEightColIcon from '../images/icon-col-4-8.svg'
|
|
|
13
13
|
import EightFourColIcon from '../images/icon-col-8-4.svg'
|
|
14
14
|
import ToggleIcon from '../images/icon-toggle.svg'
|
|
15
15
|
import { ConfigRow } from '../types/ConfigRow'
|
|
16
|
+
import { DataDesignerModal } from './DataDesignerModal'
|
|
17
|
+
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
18
|
+
import { iconHash } from '../helpers/iconHash'
|
|
19
|
+
import _ from 'lodash'
|
|
16
20
|
|
|
17
21
|
type RowMenuProps = {
|
|
18
22
|
rowIdx: number
|
|
19
|
-
row: ConfigRow
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const RowMenu: React.FC<RowMenuProps> = ({ rowIdx
|
|
25
|
+
const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
|
|
23
26
|
const { config } = useContext(DashboardContext)
|
|
24
27
|
const dispatch = useContext(DashboardDispatchContext)
|
|
25
|
-
const
|
|
28
|
+
const rows = _.cloneDeep(config.rows)
|
|
29
|
+
const row = config.rows[rowIdx]
|
|
26
30
|
|
|
27
31
|
const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
|
|
28
|
-
const
|
|
29
|
-
if (row
|
|
30
|
-
return row.reduce((acc, curr) => {
|
|
32
|
+
const curr = useMemo(() => {
|
|
33
|
+
if (row.toggle) return 'toggle'
|
|
34
|
+
return row.columns.reduce((acc, curr) => {
|
|
31
35
|
if (curr.width) {
|
|
32
36
|
acc += curr.width
|
|
33
37
|
}
|
|
34
38
|
return acc
|
|
35
39
|
}, '')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const [curr, setCurr] = useState(getCurr())
|
|
40
|
+
}, [row])
|
|
39
41
|
|
|
40
42
|
const setRowLayout = (layout: number[], toggle = undefined) => {
|
|
41
|
-
const newRows =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const newRows = _.cloneDeep(rows)
|
|
44
|
+
newRows[rowIdx].toggle = toggle
|
|
45
|
+
const rowColumns = newRows[rowIdx].columns
|
|
46
|
+
const columnsWithWidgets = rowColumns.filter(c => c.widget)
|
|
47
|
+
|
|
48
|
+
const totalWidgets = columnsWithWidgets.length
|
|
49
|
+
if (totalWidgets > layout.length) {
|
|
50
|
+
// don't let them change to a smaller layout and lose viz config work
|
|
51
|
+
return
|
|
52
|
+
} else {
|
|
53
|
+
// a 3 column becoming a 2 column with only a VizConfig in the second column will maintain order
|
|
54
|
+
// a 2 column becoming a 1 column with only a VizConfig in the second column will move the VizConfig to the first column
|
|
55
|
+
const mapRow = rowColumns.length > layout.length ? columnsWithWidgets : rowColumns
|
|
56
|
+
newRows[rowIdx].columns = layout.map((width, colIndex) => {
|
|
57
|
+
const col = mapRow[colIndex]
|
|
58
|
+
return col ? { ...col, width } : { width }
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
updateConfig({ ...config, rows: newRows })
|
|
50
|
-
setCurr(toggle ? 'toggle' : layout.join(''))
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
const moveRow = (dir = 'down') => {
|
|
@@ -138,15 +150,28 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx, row }) => {
|
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
const Row = ({ row, idx: rowIdx, uuid }) => {
|
|
153
|
+
const { overlay } = useGlobalContext()
|
|
141
154
|
return (
|
|
142
155
|
<div className='builder-row' data-row-id={rowIdx}>
|
|
143
|
-
<RowMenu rowIdx={rowIdx}
|
|
156
|
+
<RowMenu rowIdx={rowIdx} />
|
|
144
157
|
<div className='column-container'>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
<>
|
|
159
|
+
<button
|
|
160
|
+
title='Configure Data'
|
|
161
|
+
className='btn btn-configure-row'
|
|
162
|
+
onClick={() => {
|
|
163
|
+
overlay?.actions.openOverlay(<DataDesignerModal rowIndex={rowIdx} />)
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
{iconHash['gear']}
|
|
167
|
+
</button>
|
|
168
|
+
|
|
169
|
+
{row.columns
|
|
170
|
+
.filter(column => column.width)
|
|
171
|
+
.map((column, colIdx) => (
|
|
172
|
+
<Column data={column} key={`row-${uuid}-col-${colIdx}`} rowIdx={rowIdx} colIdx={colIdx} />
|
|
173
|
+
))}
|
|
174
|
+
</>
|
|
150
175
|
</div>
|
|
151
176
|
</div>
|
|
152
177
|
)
|
|
@@ -7,23 +7,22 @@ import './toggle-style.css'
|
|
|
7
7
|
import _ from 'lodash'
|
|
8
8
|
|
|
9
9
|
type ToggleProps = {
|
|
10
|
+
active: number
|
|
10
11
|
row: ConfigRow
|
|
11
|
-
rowIndex: number
|
|
12
12
|
visualizations: Record<string, Visualization>
|
|
13
|
+
setToggled: (colIndex: number) => void
|
|
13
14
|
}
|
|
14
|
-
const Toggle: React.FC<ToggleProps> = ({
|
|
15
|
-
const dispatch = useContext(DashboardDispatchContext)
|
|
16
|
-
|
|
15
|
+
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled }) => {
|
|
17
16
|
const selectItem = (colIndex, e = null) => {
|
|
18
17
|
if (e?.key && e.key !== 'Enter') return
|
|
19
|
-
|
|
18
|
+
setToggled(colIndex)
|
|
20
19
|
}
|
|
21
20
|
return (
|
|
22
21
|
<div className='toggle-component'>
|
|
23
|
-
{row.map((col, colIndex) => {
|
|
22
|
+
{row.columns.map((col, colIndex) => {
|
|
24
23
|
if (!col.widget) return null
|
|
25
24
|
const type = visualizations[col.widget].type
|
|
26
|
-
const selected =
|
|
25
|
+
const selected = colIndex === active
|
|
27
26
|
return (
|
|
28
27
|
<div role='radio' className={selected ? 'selected' : ''} key={colIndex} onClick={() => selectItem(colIndex)} onKeyUp={e => selectItem(colIndex, e)} aria-checked={selected} tabIndex={0} aria-label={`Toggle ${type}`}>
|
|
29
28
|
{getIcon(visualizations[col.widget])} <span>{_.capitalize(type)}</span>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
|
|
2
|
+
import React, { MouseEventHandler, useContext, useMemo } from 'react'
|
|
3
|
+
import Toggle from './Toggle'
|
|
4
|
+
import _ from 'lodash'
|
|
5
|
+
import { DashboardConfig } from '../types/DashboardConfig'
|
|
6
|
+
import { ConfigRow } from '../types/ConfigRow'
|
|
7
|
+
import DataTransform from '@cdc/core/helpers/DataTransform'
|
|
8
|
+
import CdcMap from '@cdc/map'
|
|
9
|
+
import CdcChart from '@cdc/chart'
|
|
10
|
+
import CdcDataBite from '@cdc/data-bite'
|
|
11
|
+
import CdcWaffleChart from '@cdc/waffle-chart'
|
|
12
|
+
import CdcMarkupInclude from '@cdc/markup-include'
|
|
13
|
+
import CdcFilteredText from '@cdc/filtered-text'
|
|
14
|
+
import Filters, { APIFilterDropdowns } from './Filters'
|
|
15
|
+
import { FilterBehavior } from './Header/Header'
|
|
16
|
+
import { DashboardContext } from '../DashboardContext'
|
|
17
|
+
import { ViewPort } from '@cdc/core/types/ViewPort'
|
|
18
|
+
import { getVizConfig } from '../helpers/getVizConfig'
|
|
19
|
+
|
|
20
|
+
type VizRowProps = {
|
|
21
|
+
filteredDataOverride?: Object[]
|
|
22
|
+
row: ConfigRow
|
|
23
|
+
rowIndex: number
|
|
24
|
+
setSharedFilter: Function
|
|
25
|
+
updateChildConfig: Function
|
|
26
|
+
applyFilters: MouseEventHandler<HTMLButtonElement>
|
|
27
|
+
apiFilterDropdowns: APIFilterDropdowns
|
|
28
|
+
handleOnChange: Function
|
|
29
|
+
currentViewport: ViewPort
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VisualizationRow: React.FC<VizRowProps> = ({ filteredDataOverride, row, rowIndex: index, setSharedFilter, updateChildConfig, applyFilters, apiFilterDropdowns, handleOnChange, currentViewport }) => {
|
|
33
|
+
const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
|
|
34
|
+
const [show, setShow] = React.useState(row.columns.map((col, i) => i === 0))
|
|
35
|
+
const setToggled = (colIndex: number) => {
|
|
36
|
+
setShow(show.map((_, i) => i === colIndex))
|
|
37
|
+
}
|
|
38
|
+
const inNoDataState = useMemo(() => {
|
|
39
|
+
const vals = Object.values(rawData)
|
|
40
|
+
if (!vals.length) return true
|
|
41
|
+
return vals.some(val => val === undefined)
|
|
42
|
+
}, [rawData])
|
|
43
|
+
const GoButton = ({ autoLoad }: { autoLoad?: boolean }) => {
|
|
44
|
+
if (config.filterBehavior === FilterBehavior.Apply && !autoLoad) {
|
|
45
|
+
return <button onClick={applyFilters}>GO!</button>
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<div className={`dashboard-row ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`} key={`row__${index}`}>
|
|
51
|
+
{row.toggle && <Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />}
|
|
52
|
+
{row.columns.map((col, colIndex) => {
|
|
53
|
+
if (col.width) {
|
|
54
|
+
if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`dashboard-col dashboard-col-${col.width}`}></div>
|
|
55
|
+
|
|
56
|
+
const visualizationConfig = getVizConfig(col.widget, index, config, rawData, dashboardFilteredData)
|
|
57
|
+
if (filteredDataOverride) {
|
|
58
|
+
visualizationConfig.data = filteredDataOverride
|
|
59
|
+
if (visualizationConfig.formattedData) {
|
|
60
|
+
visualizationConfig.formattedData = filteredDataOverride
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
|
|
65
|
+
const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
|
|
66
|
+
const tableLink = (
|
|
67
|
+
<a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
|
|
68
|
+
{visualizationConfig.dataKey} (Go to Table)
|
|
69
|
+
</a>
|
|
70
|
+
)
|
|
71
|
+
const hideFilter = visualizationConfig.autoLoad && inNoDataState
|
|
72
|
+
|
|
73
|
+
const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
|
|
74
|
+
return (
|
|
75
|
+
<React.Fragment key={`vis__${index}__${colIndex}`}>
|
|
76
|
+
<div className={`dashboard-col dashboard-col-${col.width} ${!shouldShow ? 'hidden-toggle' : ''}`}>
|
|
77
|
+
{visualizationConfig.type === 'chart' && (
|
|
78
|
+
<CdcChart
|
|
79
|
+
key={col.widget}
|
|
80
|
+
config={visualizationConfig}
|
|
81
|
+
dashboardConfig={config}
|
|
82
|
+
isEditor={false}
|
|
83
|
+
setConfig={newConfig => {
|
|
84
|
+
updateChildConfig(col.widget, newConfig)
|
|
85
|
+
}}
|
|
86
|
+
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
87
|
+
isDashboard={true}
|
|
88
|
+
link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
89
|
+
configUrl={undefined}
|
|
90
|
+
setEditing={undefined}
|
|
91
|
+
hostname={undefined}
|
|
92
|
+
setSharedFilterValue={undefined}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
{visualizationConfig.type === 'map' && (
|
|
96
|
+
<CdcMap
|
|
97
|
+
key={col.widget}
|
|
98
|
+
config={visualizationConfig}
|
|
99
|
+
isEditor={false}
|
|
100
|
+
setConfig={newConfig => {
|
|
101
|
+
updateChildConfig(col.widget, newConfig)
|
|
102
|
+
}}
|
|
103
|
+
showLoader={false}
|
|
104
|
+
setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
|
|
105
|
+
setSharedFilterValue={setSharedFilterValue}
|
|
106
|
+
isDashboard={true}
|
|
107
|
+
link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
{visualizationConfig.type === 'data-bite' && (
|
|
111
|
+
<CdcDataBite
|
|
112
|
+
key={col.widget}
|
|
113
|
+
config={visualizationConfig}
|
|
114
|
+
isEditor={false}
|
|
115
|
+
setConfig={newConfig => {
|
|
116
|
+
updateChildConfig(col.widget, newConfig)
|
|
117
|
+
}}
|
|
118
|
+
isDashboard={true}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
{visualizationConfig.type === 'waffle-chart' && (
|
|
122
|
+
<CdcWaffleChart
|
|
123
|
+
key={col.widget}
|
|
124
|
+
config={visualizationConfig}
|
|
125
|
+
isEditor={false}
|
|
126
|
+
setConfig={newConfig => {
|
|
127
|
+
updateChildConfig(col.widget, newConfig)
|
|
128
|
+
}}
|
|
129
|
+
isDashboard={true}
|
|
130
|
+
configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
{visualizationConfig.type === 'markup-include' && (
|
|
134
|
+
<CdcMarkupInclude
|
|
135
|
+
key={col.widget}
|
|
136
|
+
config={visualizationConfig}
|
|
137
|
+
isEditor={false}
|
|
138
|
+
setConfig={newConfig => {
|
|
139
|
+
updateChildConfig(col.widget, newConfig)
|
|
140
|
+
}}
|
|
141
|
+
isDashboard={true}
|
|
142
|
+
configUrl={undefined}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
{visualizationConfig.type === 'filtered-text' && (
|
|
146
|
+
<CdcFilteredText
|
|
147
|
+
key={col.widget}
|
|
148
|
+
config={visualizationConfig}
|
|
149
|
+
isEditor={false}
|
|
150
|
+
setConfig={newConfig => {
|
|
151
|
+
updateChildConfig(col.widget, newConfig)
|
|
152
|
+
}}
|
|
153
|
+
isDashboard={true}
|
|
154
|
+
configUrl={undefined}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
{visualizationConfig.type === 'filter-dropdowns' && !hideFilter && (
|
|
158
|
+
<React.Fragment key={col.widget}>
|
|
159
|
+
<Filters hide={visualizationConfig.hide} filters={config.dashboard.sharedFilters} apiFilterDropdowns={apiFilterDropdowns} handleOnChange={handleOnChange} />
|
|
160
|
+
<GoButton autoLoad={visualizationConfig.autoLoad} />
|
|
161
|
+
</React.Fragment>
|
|
162
|
+
)}
|
|
163
|
+
{visualizationConfig.type === 'table' && <DataTableStandAlone key={col.widget} visualizationKey={col.widget} config={visualizationConfig} viewport={currentViewport} />}
|
|
164
|
+
</div>
|
|
165
|
+
</React.Fragment>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
|
|
169
|
+
})}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default VisualizationRow
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useContext } from 'react'
|
|
2
2
|
import { useDrag } from 'react-dnd'
|
|
3
3
|
|
|
4
4
|
import { useGlobalContext } from '@cdc/core/components/GlobalContext'
|
|
5
5
|
import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
|
|
6
6
|
|
|
7
7
|
import { DataTransform } from '@cdc/core/helpers/DataTransform'
|
|
8
|
-
import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
|
|
9
|
-
|
|
10
|
-
import DataDesigner from '@cdc/core/components/managers/DataDesigner'
|
|
11
8
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
12
9
|
import Modal from '@cdc/core/components/ui/Modal'
|
|
13
10
|
import { Visualization } from '@cdc/core/types/Visualization'
|
|
14
11
|
import { iconHash } from '../helpers/iconHash'
|
|
12
|
+
import _ from 'lodash'
|
|
13
|
+
import { DataDesignerModal } from './DataDesignerModal'
|
|
15
14
|
|
|
16
15
|
const labelHash = {
|
|
17
16
|
'data-bite': 'Data Bite',
|
|
@@ -45,8 +44,6 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
45
44
|
const visualizations = config.visualizations
|
|
46
45
|
const dispatch = useContext(DashboardDispatchContext)
|
|
47
46
|
const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
|
|
48
|
-
const dataRef = useRef<WidgetData>()
|
|
49
|
-
dataRef.current = data
|
|
50
47
|
|
|
51
48
|
const transform = new DataTransform()
|
|
52
49
|
|
|
@@ -58,14 +55,14 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
58
55
|
const { rowIdx, colIdx } = result
|
|
59
56
|
|
|
60
57
|
if (undefined !== data?.rowIdx) {
|
|
61
|
-
rows[data.rowIdx][data.colIdx].widget = null // Wipe from old position
|
|
58
|
+
rows[data.rowIdx].columns[data.colIdx].widget = null // Wipe from old position
|
|
62
59
|
|
|
63
|
-
rows[rowIdx][colIdx].widget = data.uid // Add to new row and col
|
|
60
|
+
rows[rowIdx].columns[colIdx].widget = data.uid // Add to new row and col
|
|
64
61
|
} else if (!!addVisualization) {
|
|
65
62
|
// Item does not exist, instantiate a new one
|
|
66
63
|
const newViz = addVisualization()
|
|
67
64
|
visualizations[newViz.uid] = newViz // Add to widgets collection
|
|
68
|
-
rows[rowIdx][colIdx].widget = newViz.uid // Store reference in rows collection under the specific column
|
|
65
|
+
rows[rowIdx].columns[colIdx].widget = newViz.uid // Store reference in rows collection under the specific column
|
|
69
66
|
}
|
|
70
67
|
|
|
71
68
|
updateConfig({ ...config, rows, visualizations })
|
|
@@ -84,7 +81,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
84
81
|
|
|
85
82
|
const deleteWidget = () => {
|
|
86
83
|
if (!data) return
|
|
87
|
-
rows[data.rowIdx][data.colIdx].widget = null
|
|
84
|
+
rows[data.rowIdx].columns[data.colIdx].widget = null
|
|
88
85
|
|
|
89
86
|
delete visualizations[data.uid]
|
|
90
87
|
|
|
@@ -106,78 +103,6 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
106
103
|
updateConfig({ ...config, visualizations })
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
const changeDataset = (uid, value) => {
|
|
110
|
-
visualizations[uid].dataDescription = {}
|
|
111
|
-
visualizations[uid].formattedData = undefined
|
|
112
|
-
|
|
113
|
-
visualizations[uid].dataKey = value
|
|
114
|
-
|
|
115
|
-
updateConfig({ ...config, visualizations })
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const updateDescriptionProp = async (visualizationKey, datasetKey, key, value) => {
|
|
119
|
-
let dataDescription = { ...(dataRef.current?.dataDescription as Object), [key]: value }
|
|
120
|
-
|
|
121
|
-
let newData
|
|
122
|
-
if (!config.datasets[datasetKey].data && config.datasets[datasetKey].dataUrl) {
|
|
123
|
-
newData = await fetchRemoteData(config.datasets[datasetKey].dataUrl)
|
|
124
|
-
newData = transform.autoStandardize(newData)
|
|
125
|
-
} else {
|
|
126
|
-
newData = config.datasets[datasetKey].data
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let formattedData = transform.developerStandardize(newData, dataDescription)
|
|
130
|
-
|
|
131
|
-
let newVisualizations = { ...config.visualizations }
|
|
132
|
-
newVisualizations[visualizationKey] = { ...newVisualizations[visualizationKey], data: newData, dataDescription, formattedData }
|
|
133
|
-
|
|
134
|
-
updateConfig({ ...config, visualizations: newVisualizations })
|
|
135
|
-
|
|
136
|
-
overlay?.actions.openOverlay(dataDesignerModal(newVisualizations[visualizationKey]))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const dataDesignerModal = (configureData, dataKeyOverride?) => {
|
|
140
|
-
const dataKey = !dataKeyOverride && dataKeyOverride !== '' ? data?.dataKey || dataRef.current?.dataKey : dataKeyOverride
|
|
141
|
-
|
|
142
|
-
overlay?.actions.toggleOverlay()
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<Modal>
|
|
146
|
-
<Modal.Content>
|
|
147
|
-
<div className='dataset-selector-container'>
|
|
148
|
-
Select a dataset:
|
|
149
|
-
<select
|
|
150
|
-
className='dataset-selector'
|
|
151
|
-
defaultValue={dataKey}
|
|
152
|
-
onChange={e => {
|
|
153
|
-
changeDataset(data?.uid, e.target.value)
|
|
154
|
-
overlay?.actions.openOverlay(dataDesignerModal(data, e.target.value || ''))
|
|
155
|
-
}}
|
|
156
|
-
>
|
|
157
|
-
<option value=''>Select a dataset</option>
|
|
158
|
-
{config.datasets && Object.keys(config.datasets).map(datasetKey => <option key={datasetKey}>{datasetKey}</option>)}
|
|
159
|
-
</select>
|
|
160
|
-
</div>
|
|
161
|
-
{dataKey && (
|
|
162
|
-
<DataDesigner
|
|
163
|
-
{...{
|
|
164
|
-
configureData,
|
|
165
|
-
visualizationKey: data?.uid,
|
|
166
|
-
dataKey: dataKey,
|
|
167
|
-
updateDescriptionProp
|
|
168
|
-
}}
|
|
169
|
-
/>
|
|
170
|
-
)}
|
|
171
|
-
{configureData.formattedData && (
|
|
172
|
-
<button style={{ margin: '1em' }} className='cove-button' onClick={() => overlay?.actions.toggleOverlay()}>
|
|
173
|
-
Continue
|
|
174
|
-
</button>
|
|
175
|
-
)}
|
|
176
|
-
</Modal.Content>
|
|
177
|
-
</Modal>
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
106
|
const FilterHideModal = configureData => {
|
|
182
107
|
const currentVizKey = Object.keys(visualizations).find(vizKey => vizKey === configureData.uid) || ''
|
|
183
108
|
const currentViz = config.visualizations && config.visualizations[currentVizKey]
|
|
@@ -208,8 +133,6 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
208
133
|
}
|
|
209
134
|
}
|
|
210
135
|
|
|
211
|
-
overlay?.actions.toggleOverlay()
|
|
212
|
-
|
|
213
136
|
const showAutoLoadCheckbox = !vizWithAutoLoad || vizWithAutoLoad === currentVizKey
|
|
214
137
|
return (
|
|
215
138
|
<Modal>
|
|
@@ -243,29 +166,24 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
243
166
|
)
|
|
244
167
|
}
|
|
245
168
|
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
if (data?.openModal) {
|
|
248
|
-
overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(dataRef.current) : dataDesignerModal(dataRef.current))
|
|
249
|
-
|
|
250
|
-
visualizations[data.uid].openModal = false
|
|
251
|
-
|
|
252
|
-
updateConfig({ ...config, visualizations })
|
|
253
|
-
}
|
|
254
|
-
}, [data?.openModal])
|
|
255
|
-
|
|
256
169
|
let isConfigurationReady = false
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
} else if (data && data.formattedData) {
|
|
170
|
+
const dataConfiguredForRow = !!rows[data?.rowIdx]?.dataKey
|
|
171
|
+
if (dataConfiguredForRow || ['markup-include', 'filter-dropdowns'].includes(type)) {
|
|
260
172
|
isConfigurationReady = true
|
|
261
|
-
} else
|
|
262
|
-
|
|
263
|
-
formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription)
|
|
264
|
-
if (formattedDataAttempt) {
|
|
173
|
+
} else {
|
|
174
|
+
if (data?.formattedData) {
|
|
265
175
|
isConfigurationReady = true
|
|
176
|
+
} else if (data?.dataKey && data?.dataDescription && config.datasets[data.dataKey]) {
|
|
177
|
+
const formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data)
|
|
178
|
+
const canFormatData = !!transform.developerStandardize(formattedDataAttempt, data.dataDescription)
|
|
179
|
+
if (canFormatData) {
|
|
180
|
+
isConfigurationReady = true
|
|
181
|
+
}
|
|
266
182
|
}
|
|
267
183
|
}
|
|
268
184
|
|
|
185
|
+
const needsDataConfiguration = !dataConfiguredForRow && type !== 'markup-include'
|
|
186
|
+
|
|
269
187
|
return (
|
|
270
188
|
<>
|
|
271
189
|
<div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
|
|
@@ -278,12 +196,12 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
278
196
|
{iconHash['tools']}
|
|
279
197
|
</button>
|
|
280
198
|
)}
|
|
281
|
-
{
|
|
199
|
+
{needsDataConfiguration && (
|
|
282
200
|
<button
|
|
283
201
|
title='Configure Data'
|
|
284
202
|
className='btn btn-configure'
|
|
285
203
|
onClick={() => {
|
|
286
|
-
overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(data) :
|
|
204
|
+
overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(data) : <DataDesignerModal rowIndex={data.rowIdx} vizKey={data.uid} />)
|
|
287
205
|
}}
|
|
288
206
|
>
|
|
289
207
|
{iconHash['gear']}
|
|
@@ -23,19 +23,21 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function filter(data, filters, condition) {
|
|
26
|
-
return data
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
return data
|
|
27
|
+
? data.filter(row => {
|
|
28
|
+
const found = filters.find(filter => {
|
|
29
|
+
if (filter.pivot) return false
|
|
30
|
+
const currentValue = row[filter.columnName]
|
|
31
|
+
const selectedValue = filter.queuedActive || filter.active
|
|
32
|
+
const isNotTheSelectedValue = selectedValue && currentValue != selectedValue
|
|
33
|
+
const isFirstOccurrenceOfTier = filter.tier === condition
|
|
34
|
+
if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
return !found
|
|
39
|
+
})
|
|
40
|
+
: []
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function setFilterValuesAndActiveFilter(filters: SharedFilter[], filteredData: Object[], i: number) {
|
|
@@ -82,7 +84,7 @@ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] =
|
|
|
82
84
|
|
|
83
85
|
const filteredData = filter(_data, filters, i + 1)
|
|
84
86
|
|
|
85
|
-
setFilterValuesAndActiveFilter(filters, filteredData, i)
|
|
87
|
+
setFilterValuesAndActiveFilter(_.cloneDeep(filters), filteredData, i)
|
|
86
88
|
|
|
87
89
|
if (lastIteration) {
|
|
88
90
|
const pivotFilter = filters.find(filter => filter.pivot)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DashboardState } from '../store/dashboard.reducer'
|
|
2
|
+
import { Dashboard } from '../types/Dashboard'
|
|
3
|
+
import { SharedFilter } from '../types/SharedFilter'
|
|
4
|
+
import { filterData } from './filterData'
|
|
5
|
+
import { getFormattedData } from './getFormattedData'
|
|
6
|
+
import { getVizKeys } from './getVizKeys'
|
|
7
|
+
|
|
8
|
+
export const getApplicableFilters = (dashboard: Dashboard, key: string | number): false | SharedFilter[] => {
|
|
9
|
+
const c = dashboard.sharedFilters?.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(`${key}`) !== -1)
|
|
10
|
+
return c?.length > 0 ? c : false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getFilteredData = (state: DashboardState, initialFilteredData = {}, dataOverride?: Object) => {
|
|
14
|
+
const newFilteredData = initialFilteredData
|
|
15
|
+
const { config } = state
|
|
16
|
+
getVizKeys(config).forEach(key => {
|
|
17
|
+
const applicableFilters = getApplicableFilters(config.dashboard, key)
|
|
18
|
+
if (applicableFilters) {
|
|
19
|
+
const { dataKey, data, dataDescription } = config.visualizations[key]
|
|
20
|
+
const _data = state.data[dataKey] || data
|
|
21
|
+
const formattedData = dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
|
|
22
|
+
|
|
23
|
+
newFilteredData[key] = filterData(applicableFilters, formattedData)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
config.rows.forEach((row, index) => {
|
|
27
|
+
if (row.dataKey) {
|
|
28
|
+
const applicableFilters = getApplicableFilters(config.dashboard, index)
|
|
29
|
+
if (applicableFilters) {
|
|
30
|
+
const { dataKey, data, dataDescription } = row
|
|
31
|
+
const _data = state.data[dataKey] || data
|
|
32
|
+
const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
|
|
33
|
+
|
|
34
|
+
newFilteredData[index] = filterData(applicableFilters, formattedData)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
return newFilteredData
|
|
39
|
+
}
|
|
@@ -4,6 +4,7 @@ import { filterData } from './filterData'
|
|
|
4
4
|
import { generateValuesForFilter } from './generateValuesForFilter'
|
|
5
5
|
import { getFormattedData } from './getFormattedData'
|
|
6
6
|
import { getVizKeys } from './getVizKeys'
|
|
7
|
+
import { getVizRowColumnLocator } from './getVizRowColumnLocator'
|
|
7
8
|
|
|
8
9
|
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
9
10
|
|
|
@@ -17,6 +18,8 @@ export const getUpdateConfig =
|
|
|
17
18
|
let newFilteredData = {}
|
|
18
19
|
let visualizationKeys = getVizKeys(newConfig)
|
|
19
20
|
|
|
21
|
+
const vizRowColumnLocator = getVizRowColumnLocator(newConfig.rows)
|
|
22
|
+
|
|
20
23
|
if (newConfig.dashboard.sharedFilters) {
|
|
21
24
|
newConfig.dashboard.sharedFilters.forEach((filter, i) => {
|
|
22
25
|
const filterIsSetByVizData = !!visualizationKeys.find(key => key === filter.setBy)
|
|
@@ -52,6 +55,8 @@ export const getUpdateConfig =
|
|
|
52
55
|
})
|
|
53
56
|
|
|
54
57
|
visualizationKeys.forEach(visualizationKey => {
|
|
58
|
+
const row = vizRowColumnLocator[visualizationKey]
|
|
59
|
+
if (newConfig.rows[row]?.datakey) return // data configured on the row level
|
|
55
60
|
const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
|
|
56
61
|
|
|
57
62
|
if (applicableFilters.length > 0) {
|
|
@@ -62,6 +67,16 @@ export const getUpdateConfig =
|
|
|
62
67
|
newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
|
|
63
68
|
}
|
|
64
69
|
})
|
|
70
|
+
|
|
71
|
+
newConfig.rows.forEach((row, rowIndex) => {
|
|
72
|
+
const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(rowIndex) !== -1)
|
|
73
|
+
|
|
74
|
+
if (applicableFilters.length > 0) {
|
|
75
|
+
const formattedData = getFormattedData(row.data, row.dataDescription)
|
|
76
|
+
const _data = formattedData || (dataOverride || state.data)[rowIndex]
|
|
77
|
+
newFilteredData[rowIndex] = filterData(applicableFilters, _data)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
65
80
|
}
|
|
66
81
|
//Enforce default values that need to be calculated at runtime
|
|
67
82
|
newConfig.runtime = {}
|