@cdc/dashboard 4.24.2 → 4.24.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdcdashboard.js +128512 -99417
- package/examples/chart-data.json +5409 -0
- package/examples/full-dash-test.json +14643 -0
- package/examples/full-dashboard.json +10036 -0
- package/examples/sankey.json +5218 -0
- package/index.html +4 -3
- package/package.json +11 -10
- package/src/CdcDashboard.tsx +129 -124
- package/src/CdcDashboardComponent.tsx +316 -441
- package/src/DashboardContext.tsx +4 -1
- package/src/_stories/Dashboard.stories.tsx +79 -36
- package/src/_stories/_mock/api-filter-chart.json +11 -35
- package/src/_stories/_mock/api-filter-map.json +17 -31
- package/src/_stories/_mock/dashboard-gallery.json +523 -534
- package/src/_stories/_mock/multi-viz.json +378 -0
- package/src/_stories/_mock/pivot-filter.json +161 -0
- package/src/_stories/_mock/standalone-table.json +122 -0
- package/src/_stories/_mock/toggle-example.json +4035 -0
- package/src/components/DataDesignerModal.tsx +145 -0
- package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
- package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
- package/src/components/Filters.tsx +88 -0
- package/src/components/Grid.tsx +3 -1
- package/src/components/Header/FilterModal.tsx +506 -0
- package/src/components/Header/Header.tsx +25 -465
- package/src/components/Row.tsx +65 -29
- package/src/components/Toggle/Toggle.tsx +36 -0
- package/src/components/Toggle/index.tsx +1 -0
- package/src/components/Toggle/toggle-style.css +34 -0
- package/src/components/VisualizationRow.tsx +174 -0
- package/src/components/VisualizationsPanel.tsx +13 -3
- package/src/components/Widget.tsx +28 -126
- package/src/helpers/filterData.ts +75 -50
- package/src/helpers/generateValuesForFilter.ts +2 -12
- package/src/helpers/getApiFilterKey.ts +5 -0
- package/src/helpers/getFilteredData.ts +39 -0
- package/src/helpers/getUpdateConfig.ts +39 -22
- package/src/helpers/getVizConfig.ts +31 -0
- package/src/helpers/getVizRowColumnLocator.ts +9 -0
- package/src/helpers/iconHash.tsx +34 -0
- package/src/helpers/tests/filterData.test.ts +149 -0
- package/src/images/icon-toggle.svg +1 -0
- package/src/scss/grid.scss +10 -3
- package/src/scss/main.scss +11 -0
- package/src/store/dashboard.actions.ts +35 -3
- package/src/store/dashboard.reducer.ts +33 -2
- package/src/types/APIFilter.ts +4 -5
- package/src/types/ConfigRow.ts +13 -2
- package/src/types/DataSet.ts +11 -8
- package/src/types/InitialState.ts +2 -1
- package/src/types/SharedFilter.ts +6 -3
- package/src/types/Tab.ts +1 -0
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
|
|
|
@@ -11,35 +11,55 @@ import TwoColIcon from '../images/icon-col-6.svg'
|
|
|
11
11
|
import ThreeColIcon from '../images/icon-col-4.svg'
|
|
12
12
|
import FourEightColIcon from '../images/icon-col-4-8.svg'
|
|
13
13
|
import EightFourColIcon from '../images/icon-col-8-4.svg'
|
|
14
|
+
import ToggleIcon from '../images/icon-toggle.svg'
|
|
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'
|
|
20
|
+
|
|
21
|
+
type RowMenuProps = {
|
|
22
|
+
rowIdx: number
|
|
23
|
+
}
|
|
14
24
|
|
|
15
|
-
const RowMenu = ({ rowIdx
|
|
25
|
+
const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
|
|
16
26
|
const { config } = useContext(DashboardContext)
|
|
17
|
-
if (!config) return null
|
|
18
|
-
const { rows } = config
|
|
19
27
|
const dispatch = useContext(DashboardDispatchContext)
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
let res = [] as Object[]
|
|
23
|
-
|
|
24
|
-
for (let i = 0; i < row.length; i++) {
|
|
25
|
-
if (row[i].width) res.push(row[i].width)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return res.join('')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const [curr, setCurr] = useState(getCurr())
|
|
32
|
-
|
|
33
|
-
const setRowLayout = layout => {
|
|
34
|
-
const newRows = [...rows]
|
|
35
|
-
const r = newRows[rowIdx]
|
|
28
|
+
const rows = _.cloneDeep(config.rows)
|
|
29
|
+
const row = config.rows[rowIdx]
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
|
|
32
|
+
const curr = useMemo(() => {
|
|
33
|
+
if (row.toggle) return 'toggle'
|
|
34
|
+
return row.columns.reduce((acc, curr) => {
|
|
35
|
+
if (curr.width) {
|
|
36
|
+
acc += curr.width
|
|
37
|
+
}
|
|
38
|
+
return acc
|
|
39
|
+
}, '')
|
|
40
|
+
}, [row])
|
|
41
|
+
|
|
42
|
+
const setRowLayout = (layout: number[], toggle = undefined) => {
|
|
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
|
+
})
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
updateConfig({ ...config, rows: newRows })
|
|
42
|
-
setCurr(layout.join(''))
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
const moveRow = (dir = 'down') => {
|
|
@@ -104,6 +124,9 @@ const RowMenu = ({ rowIdx, row }) => {
|
|
|
104
124
|
</li>,
|
|
105
125
|
<li className={curr === '84' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([8, 4])} key='84' title='2 Columns'>
|
|
106
126
|
<EightFourColIcon />
|
|
127
|
+
</li>,
|
|
128
|
+
<li className={curr === 'toggle' ? `current row-menu__list--item` : `row-menu__list--item`} onClick={() => setRowLayout([12, 12, 12], true)} key='toggle' title='Toggle between up to three visualizations'>
|
|
129
|
+
<ToggleIcon />
|
|
107
130
|
</li>
|
|
108
131
|
]
|
|
109
132
|
|
|
@@ -127,15 +150,28 @@ const RowMenu = ({ rowIdx, row }) => {
|
|
|
127
150
|
}
|
|
128
151
|
|
|
129
152
|
const Row = ({ row, idx: rowIdx, uuid }) => {
|
|
153
|
+
const { overlay } = useGlobalContext()
|
|
130
154
|
return (
|
|
131
155
|
<div className='builder-row' data-row-id={rowIdx}>
|
|
132
|
-
<RowMenu rowIdx={rowIdx}
|
|
156
|
+
<RowMenu rowIdx={rowIdx} />
|
|
133
157
|
<div className='column-container'>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
</>
|
|
139
175
|
</div>
|
|
140
176
|
</div>
|
|
141
177
|
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { DashboardDispatchContext } from '../../DashboardContext'
|
|
3
|
+
import { ConfigRow } from '../../types/ConfigRow'
|
|
4
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
5
|
+
import { getIcon } from '../../helpers/iconHash'
|
|
6
|
+
import './toggle-style.css'
|
|
7
|
+
import _ from 'lodash'
|
|
8
|
+
|
|
9
|
+
type ToggleProps = {
|
|
10
|
+
active: number
|
|
11
|
+
row: ConfigRow
|
|
12
|
+
visualizations: Record<string, Visualization>
|
|
13
|
+
setToggled: (colIndex: number) => void
|
|
14
|
+
}
|
|
15
|
+
const Toggle: React.FC<ToggleProps> = ({ active, row, visualizations, setToggled }) => {
|
|
16
|
+
const selectItem = (colIndex, e = null) => {
|
|
17
|
+
if (e?.key && e.key !== 'Enter') return
|
|
18
|
+
setToggled(colIndex)
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<div className='toggle-component'>
|
|
22
|
+
{row.columns.map((col, colIndex) => {
|
|
23
|
+
if (!col.widget) return null
|
|
24
|
+
const type = visualizations[col.widget].type
|
|
25
|
+
const selected = colIndex === active
|
|
26
|
+
return (
|
|
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}`}>
|
|
28
|
+
{getIcon(visualizations[col.widget])} <span>{_.capitalize(type)}</span>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
})}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Toggle
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Toggle'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.cdc-open-viz-module {
|
|
2
|
+
--border: 1px solid var(--lightGray);
|
|
3
|
+
.toggle-component {
|
|
4
|
+
display: flex;
|
|
5
|
+
justify-content: right;
|
|
6
|
+
width: 100%;
|
|
7
|
+
margin-bottom: 15px;
|
|
8
|
+
:first-child:is(div) {
|
|
9
|
+
border: var(--border);
|
|
10
|
+
border-radius: 5px 0 0 5px;
|
|
11
|
+
}
|
|
12
|
+
:last-child:is(div) {
|
|
13
|
+
border: var(--border);
|
|
14
|
+
border-radius: 0 5px 5px 0;
|
|
15
|
+
}
|
|
16
|
+
:is(div) {
|
|
17
|
+
border-top: var(--border);
|
|
18
|
+
border-bottom: var(--border);
|
|
19
|
+
padding: 7px 15px;
|
|
20
|
+
display: inline;
|
|
21
|
+
float: right;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
&.selected {
|
|
24
|
+
background-color: var(--primary);
|
|
25
|
+
color: white;
|
|
26
|
+
}
|
|
27
|
+
background-color: var(--white);
|
|
28
|
+
color: var(--primary);
|
|
29
|
+
:is(svg) {
|
|
30
|
+
height: 25px;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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
|
|
@@ -2,11 +2,12 @@ import React from 'react'
|
|
|
2
2
|
import type { Visualization } from '@cdc/core/types/Visualization'
|
|
3
3
|
import Widget from './Widget'
|
|
4
4
|
import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
|
|
5
|
+
import { Table } from '@cdc/core/types/Table'
|
|
5
6
|
|
|
6
7
|
const addVisualization = (type, subType) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
newViz:
|
|
8
|
+
const modalWillOpen = type !== 'markup-include'
|
|
9
|
+
const newVisualizationConfig: Partial<Visualization> = {
|
|
10
|
+
newViz: type !== 'table',
|
|
10
11
|
openModal: modalWillOpen,
|
|
11
12
|
uid: type + Date.now(),
|
|
12
13
|
type
|
|
@@ -23,6 +24,13 @@ const addVisualization = (type, subType) => {
|
|
|
23
24
|
case 'data-bite' || 'waffle-chart' || 'markup-include' || 'filtered-text':
|
|
24
25
|
newVisualizationConfig.visualizationType = type
|
|
25
26
|
break
|
|
27
|
+
case 'table':
|
|
28
|
+
const tableConfig: Table = { label: 'Data Table', show: true, showDownloadUrl: false, showVertical: true, expanded: true }
|
|
29
|
+
newVisualizationConfig.table = tableConfig
|
|
30
|
+
newVisualizationConfig.columns = {}
|
|
31
|
+
newVisualizationConfig.dataFormat = {}
|
|
32
|
+
newVisualizationConfig.visualizationType = type
|
|
33
|
+
break
|
|
26
34
|
default:
|
|
27
35
|
newVisualizationConfig.visualizationType = type
|
|
28
36
|
break
|
|
@@ -39,6 +47,7 @@ const VisualizationsPanel = ({ loadConfig, config }) => (
|
|
|
39
47
|
<Widget addVisualization={() => addVisualization('chart', 'Bar')} type='Bar' />
|
|
40
48
|
<Widget addVisualization={() => addVisualization('chart', 'Line')} type='Line' />
|
|
41
49
|
<Widget addVisualization={() => addVisualization('chart', 'Pie')} type='Pie' />
|
|
50
|
+
<Widget addVisualization={() => addVisualization('chart', 'Sankey')} type='Sankey' />
|
|
42
51
|
</div>
|
|
43
52
|
<span className='subheading-3'>Map</span>
|
|
44
53
|
<div className='drag-grid'>
|
|
@@ -53,6 +62,7 @@ const VisualizationsPanel = ({ loadConfig, config }) => (
|
|
|
53
62
|
<Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
|
|
54
63
|
<Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
|
|
55
64
|
<Widget addVisualization={() => addVisualization('filter-dropdowns', '')} type='filter-dropdowns' />
|
|
65
|
+
<Widget addVisualization={() => addVisualization('table', '')} type='table' />
|
|
56
66
|
</div>
|
|
57
67
|
<span className='subheading-3'>Advanced</span>
|
|
58
68
|
<AdvancedEditor loadConfig={loadConfig} state={config} convertStateToConfig={undefined} />
|
|
@@ -1,34 +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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Bar: <Icon display='chartBar' base />,
|
|
18
|
-
'Spark Line': <Icon display='chartLine' />,
|
|
19
|
-
'waffle-chart': <Icon display='grid' base />,
|
|
20
|
-
'markup-include': <Icon display='code' base />,
|
|
21
|
-
Line: <Icon display='chartLine' base />,
|
|
22
|
-
Pie: <Icon display='chartPie' base />,
|
|
23
|
-
us: <Icon display='mapUsa' base />,
|
|
24
|
-
'us-county': <Icon display='mapUsa' base />,
|
|
25
|
-
world: <Icon display='mapWorld' base />,
|
|
26
|
-
'single-state': <Icon display='mapAl' base />,
|
|
27
|
-
gear: <Icon display='gear' base />,
|
|
28
|
-
tools: <Icon display='tools' base />,
|
|
29
|
-
'filtered-text': <Icon display='filtered-text' base />,
|
|
30
|
-
'filter-dropdowns': <Icon display='filter-dropdowns' base />
|
|
31
|
-
}
|
|
11
|
+
import { iconHash } from '../helpers/iconHash'
|
|
12
|
+
import _ from 'lodash'
|
|
13
|
+
import { DataDesignerModal } from './DataDesignerModal'
|
|
32
14
|
|
|
33
15
|
const labelHash = {
|
|
34
16
|
'data-bite': 'Data Bite',
|
|
@@ -43,7 +25,9 @@ const labelHash = {
|
|
|
43
25
|
world: 'World',
|
|
44
26
|
'single-state': 'U.S. State',
|
|
45
27
|
'filtered-text': 'Filtered Text',
|
|
46
|
-
'filter-dropdowns': 'Filter Dropdowns'
|
|
28
|
+
'filter-dropdowns': 'Filter Dropdowns',
|
|
29
|
+
Sankey: 'Sankey Chart',
|
|
30
|
+
table: 'Table'
|
|
47
31
|
}
|
|
48
32
|
|
|
49
33
|
type WidgetData = Visualization & { rowIdx: number; colIdx: number }
|
|
@@ -56,13 +40,10 @@ type WidgetProps = {
|
|
|
56
40
|
const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
57
41
|
const { overlay } = useGlobalContext()
|
|
58
42
|
const { config } = useContext(DashboardContext)
|
|
59
|
-
if (!config) return null
|
|
60
43
|
const rows = config.rows
|
|
61
44
|
const visualizations = config.visualizations
|
|
62
45
|
const dispatch = useContext(DashboardDispatchContext)
|
|
63
46
|
const updateConfig = config => dispatch({ type: 'UPDATE_CONFIG', payload: [config] })
|
|
64
|
-
const dataRef = useRef<WidgetData>()
|
|
65
|
-
dataRef.current = data
|
|
66
47
|
|
|
67
48
|
const transform = new DataTransform()
|
|
68
49
|
|
|
@@ -74,14 +55,14 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
74
55
|
const { rowIdx, colIdx } = result
|
|
75
56
|
|
|
76
57
|
if (undefined !== data?.rowIdx) {
|
|
77
|
-
rows[data.rowIdx][data.colIdx].widget = null // Wipe from old position
|
|
58
|
+
rows[data.rowIdx].columns[data.colIdx].widget = null // Wipe from old position
|
|
78
59
|
|
|
79
|
-
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
|
|
80
61
|
} else if (!!addVisualization) {
|
|
81
62
|
// Item does not exist, instantiate a new one
|
|
82
63
|
const newViz = addVisualization()
|
|
83
64
|
visualizations[newViz.uid] = newViz // Add to widgets collection
|
|
84
|
-
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
|
|
85
66
|
}
|
|
86
67
|
|
|
87
68
|
updateConfig({ ...config, rows, visualizations })
|
|
@@ -100,7 +81,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
100
81
|
|
|
101
82
|
const deleteWidget = () => {
|
|
102
83
|
if (!data) return
|
|
103
|
-
rows[data.rowIdx][data.colIdx].widget = null
|
|
84
|
+
rows[data.rowIdx].columns[data.colIdx].widget = null
|
|
104
85
|
|
|
105
86
|
delete visualizations[data.uid]
|
|
106
87
|
|
|
@@ -122,78 +103,6 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
122
103
|
updateConfig({ ...config, visualizations })
|
|
123
104
|
}
|
|
124
105
|
|
|
125
|
-
const changeDataset = (uid, value) => {
|
|
126
|
-
visualizations[uid].dataDescription = {}
|
|
127
|
-
visualizations[uid].formattedData = undefined
|
|
128
|
-
|
|
129
|
-
visualizations[uid].dataKey = value
|
|
130
|
-
|
|
131
|
-
updateConfig({ ...config, visualizations })
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const updateDescriptionProp = async (visualizationKey, datasetKey, key, value) => {
|
|
135
|
-
let dataDescription = { ...(dataRef.current?.dataDescription as Object), [key]: value }
|
|
136
|
-
|
|
137
|
-
let newData
|
|
138
|
-
if (!config.datasets[datasetKey].data && config.datasets[datasetKey].dataUrl) {
|
|
139
|
-
newData = await fetchRemoteData(config.datasets[datasetKey].dataUrl)
|
|
140
|
-
newData = transform.autoStandardize(newData)
|
|
141
|
-
} else {
|
|
142
|
-
newData = config.datasets[datasetKey].data
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
let formattedData = transform.developerStandardize(newData, dataDescription)
|
|
146
|
-
|
|
147
|
-
let newVisualizations = { ...config.visualizations }
|
|
148
|
-
newVisualizations[visualizationKey] = { ...newVisualizations[visualizationKey], data: newData, dataDescription, formattedData }
|
|
149
|
-
|
|
150
|
-
updateConfig({ ...config, visualizations: newVisualizations })
|
|
151
|
-
|
|
152
|
-
overlay?.actions.openOverlay(dataDesignerModal(newVisualizations[visualizationKey]))
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const dataDesignerModal = (configureData, dataKeyOverride?) => {
|
|
156
|
-
const dataKey = !dataKeyOverride && dataKeyOverride !== '' ? data?.dataKey || dataRef.current?.dataKey : dataKeyOverride
|
|
157
|
-
|
|
158
|
-
overlay?.actions.toggleOverlay()
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<Modal>
|
|
162
|
-
<Modal.Content>
|
|
163
|
-
<div className='dataset-selector-container'>
|
|
164
|
-
Select a dataset:
|
|
165
|
-
<select
|
|
166
|
-
className='dataset-selector'
|
|
167
|
-
defaultValue={dataKey}
|
|
168
|
-
onChange={e => {
|
|
169
|
-
changeDataset(data?.uid, e.target.value)
|
|
170
|
-
overlay?.actions.openOverlay(dataDesignerModal(data, e.target.value || ''))
|
|
171
|
-
}}
|
|
172
|
-
>
|
|
173
|
-
<option value=''>Select a dataset</option>
|
|
174
|
-
{config.datasets && Object.keys(config.datasets).map(datasetKey => <option key={datasetKey}>{datasetKey}</option>)}
|
|
175
|
-
</select>
|
|
176
|
-
</div>
|
|
177
|
-
{dataKey && (
|
|
178
|
-
<DataDesigner
|
|
179
|
-
{...{
|
|
180
|
-
configureData,
|
|
181
|
-
visualizationKey: data?.uid,
|
|
182
|
-
dataKey: dataKey,
|
|
183
|
-
updateDescriptionProp
|
|
184
|
-
}}
|
|
185
|
-
/>
|
|
186
|
-
)}
|
|
187
|
-
{configureData.formattedData && (
|
|
188
|
-
<button style={{ margin: '1em' }} className='cove-button' onClick={() => overlay?.actions.toggleOverlay()}>
|
|
189
|
-
Continue
|
|
190
|
-
</button>
|
|
191
|
-
)}
|
|
192
|
-
</Modal.Content>
|
|
193
|
-
</Modal>
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
106
|
const FilterHideModal = configureData => {
|
|
198
107
|
const currentVizKey = Object.keys(visualizations).find(vizKey => vizKey === configureData.uid) || ''
|
|
199
108
|
const currentViz = config.visualizations && config.visualizations[currentVizKey]
|
|
@@ -224,8 +133,6 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
224
133
|
}
|
|
225
134
|
}
|
|
226
135
|
|
|
227
|
-
overlay?.actions.toggleOverlay()
|
|
228
|
-
|
|
229
136
|
const showAutoLoadCheckbox = !vizWithAutoLoad || vizWithAutoLoad === currentVizKey
|
|
230
137
|
return (
|
|
231
138
|
<Modal>
|
|
@@ -259,29 +166,24 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
259
166
|
)
|
|
260
167
|
}
|
|
261
168
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
} else if(data && data.formattedData) {
|
|
276
|
-
isConfigurationReady = true;
|
|
277
|
-
} else if(data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]){
|
|
278
|
-
let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data);
|
|
279
|
-
formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription);
|
|
280
|
-
if(formattedDataAttempt){
|
|
281
|
-
isConfigurationReady = true;
|
|
169
|
+
let isConfigurationReady = false
|
|
170
|
+
const dataConfiguredForRow = !!rows[data?.rowIdx]?.dataKey
|
|
171
|
+
if (dataConfiguredForRow || ['markup-include', 'filter-dropdowns'].includes(type)) {
|
|
172
|
+
isConfigurationReady = true
|
|
173
|
+
} else {
|
|
174
|
+
if (data?.formattedData) {
|
|
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
|
+
}
|
|
282
182
|
}
|
|
283
183
|
}
|
|
284
184
|
|
|
185
|
+
const needsDataConfiguration = !dataConfiguredForRow && type !== 'markup-include'
|
|
186
|
+
|
|
285
187
|
return (
|
|
286
188
|
<>
|
|
287
189
|
<div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
|
|
@@ -294,12 +196,12 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
|
|
|
294
196
|
{iconHash['tools']}
|
|
295
197
|
</button>
|
|
296
198
|
)}
|
|
297
|
-
{
|
|
199
|
+
{needsDataConfiguration && (
|
|
298
200
|
<button
|
|
299
201
|
title='Configure Data'
|
|
300
202
|
className='btn btn-configure'
|
|
301
203
|
onClick={() => {
|
|
302
|
-
overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(data) :
|
|
204
|
+
overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(data) : <DataDesignerModal rowIndex={data.rowIdx} vizKey={data.uid} />)
|
|
303
205
|
}}
|
|
304
206
|
>
|
|
305
207
|
{iconHash['gear']}
|