@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.
Files changed (34) hide show
  1. package/dist/cdcdashboard.js +119414 -103311
  2. package/examples/chart-data.json +5409 -0
  3. package/examples/full-dash-test.json +14643 -0
  4. package/examples/full-dashboard.json +10036 -0
  5. package/index.html +2 -2
  6. package/package.json +9 -9
  7. package/src/CdcDashboard.tsx +8 -3
  8. package/src/CdcDashboardComponent.tsx +232 -344
  9. package/src/_stories/Dashboard.stories.tsx +59 -38
  10. package/src/_stories/_mock/api-filter-chart.json +11 -35
  11. package/src/_stories/_mock/api-filter-map.json +17 -31
  12. package/src/_stories/_mock/dashboard-gallery.json +523 -534
  13. package/src/_stories/_mock/multi-viz.json +378 -0
  14. package/src/_stories/_mock/pivot-filter.json +0 -2
  15. package/src/components/DataDesignerModal.tsx +145 -0
  16. package/src/components/Grid.tsx +3 -1
  17. package/src/components/Header/FilterModal.tsx +49 -23
  18. package/src/components/Row.tsx +50 -25
  19. package/src/components/Toggle/Toggle.tsx +6 -7
  20. package/src/components/VisualizationRow.tsx +174 -0
  21. package/src/components/Widget.tsx +21 -103
  22. package/src/helpers/filterData.ts +16 -14
  23. package/src/helpers/getFilteredData.ts +39 -0
  24. package/src/helpers/getUpdateConfig.ts +15 -0
  25. package/src/helpers/getVizConfig.ts +31 -0
  26. package/src/helpers/getVizRowColumnLocator.ts +9 -0
  27. package/src/scss/grid.scss +9 -2
  28. package/src/scss/main.scss +5 -0
  29. package/src/store/dashboard.actions.ts +16 -1
  30. package/src/store/dashboard.reducer.ts +25 -2
  31. package/src/types/APIFilter.ts +4 -5
  32. package/src/types/ConfigRow.ts +12 -3
  33. package/src/types/DataSet.ts +11 -8
  34. package/src/types/SharedFilter.ts +1 -1
@@ -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, row }) => {
25
+ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
23
26
  const { config } = useContext(DashboardContext)
24
27
  const dispatch = useContext(DashboardDispatchContext)
25
- const { rows } = config
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 getCurr = () => {
29
- if (row[0].toggle) return 'toggle'
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 = [...rows]
42
- const row = newRows[rowIdx]
43
-
44
- row.forEach((col, i) => {
45
- col.width = layout[i] ?? null
46
- col.toggle = toggle
47
- if (!toggle) col.hide = undefined
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} row={row} />
156
+ <RowMenu rowIdx={rowIdx} />
144
157
  <div className='column-container'>
145
- {row
146
- .filter(column => column.width)
147
- .map((column, colIdx) => (
148
- <Column data={column} key={`row-${uuid}-col-${colIdx}`} rowIdx={rowIdx} colIdx={colIdx} />
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> = ({ row, rowIndex, visualizations }) => {
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
- dispatch({ type: 'TOGGLE_ROW', payload: { rowIndex, colIndex } })
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 = col.hide !== undefined ? col.hide : colIndex === 0
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 React, { useContext, useRef, useEffect } from 'react'
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:&nbsp;
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
- if (type === 'markup-include' || type === 'filter-dropdowns') {
258
- isConfigurationReady = true
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 if (data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]) {
262
- let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data)
263
- formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription)
264
- if (formattedDataAttempt) {
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
- {type !== 'markup-include' && (
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) : dataDesignerModal(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 ? data.filter(row => {
27
- const found = filters.find(filter => {
28
- if (filter.pivot) return false
29
- const currentValue = row[filter.columnName]
30
- const selectedValue = filter.queuedActive || filter.active
31
- const isNotTheSelectedValue = selectedValue && currentValue != selectedValue
32
- const isFirstOccurrenceOfTier = filter.tier === condition
33
- if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
34
- return true
35
- }
36
- })
37
- return !found
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 = {}