@cdc/dashboard 4.24.4 → 4.24.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/cdcdashboard.js +179228 -141419
  2. package/examples/custom/css/respiratory.css +236 -0
  3. package/examples/custom/js/respiratory.js +242 -0
  4. package/examples/default-multi-dataset-shared-filter.json +1729 -0
  5. package/examples/ed-visits-county-file.json +618 -0
  6. package/examples/filtered-dash.json +6 -21
  7. package/index.html +12 -3
  8. package/package.json +12 -11
  9. package/src/CdcDashboard.tsx +5 -1
  10. package/src/CdcDashboardComponent.tsx +156 -334
  11. package/src/DashboardContext.tsx +9 -1
  12. package/src/_stories/Dashboard.stories.tsx +31 -3
  13. package/src/_stories/_mock/dashboard-gallery.json +534 -523
  14. package/src/_stories/_mock/markup-include.json +78 -0
  15. package/src/_stories/_mock/multi-dashboards.json +914 -0
  16. package/src/_stories/_mock/multi-viz.json +2 -3
  17. package/src/_stories/_mock/pivot-filter.json +15 -11
  18. package/src/_stories/_mock/standalone-table.json +2 -0
  19. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  20. package/src/components/Column.tsx +1 -1
  21. package/src/components/DashboardFilters/DashboardFilters.tsx +80 -0
  22. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  23. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  24. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +367 -0
  25. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  26. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +143 -0
  27. package/src/components/DashboardFilters/index.ts +3 -0
  28. package/src/components/DataDesignerModal.tsx +9 -9
  29. package/src/components/ExpandCollapseButtons.tsx +20 -0
  30. package/src/components/Header/Header.tsx +1 -97
  31. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +4 -4
  32. package/src/components/MultiConfigTabs/MultiTabs.tsx +3 -2
  33. package/src/components/Row.tsx +52 -19
  34. package/src/components/Toggle/Toggle.tsx +2 -4
  35. package/src/components/VisualizationRow.tsx +96 -29
  36. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  37. package/src/components/VisualizationsPanel/index.ts +1 -0
  38. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  39. package/src/components/Widget.tsx +26 -90
  40. package/src/helpers/apiFilterHelpers.ts +51 -0
  41. package/src/helpers/changeFilterActive.ts +30 -0
  42. package/src/helpers/filterData.ts +16 -56
  43. package/src/helpers/generateValuesForFilter.ts +1 -1
  44. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  45. package/src/helpers/getFilteredData.ts +4 -2
  46. package/src/helpers/getVizConfig.ts +23 -2
  47. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  48. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  49. package/src/helpers/iconHash.tsx +3 -3
  50. package/src/helpers/mapDataToConfig.ts +29 -0
  51. package/src/helpers/processData.ts +2 -3
  52. package/src/helpers/reloadURLHelpers.ts +68 -0
  53. package/src/helpers/tests/filterData.test.ts +1 -93
  54. package/src/scss/editor-panel.scss +1 -1
  55. package/src/scss/grid.scss +34 -27
  56. package/src/scss/main.scss +41 -3
  57. package/src/scss/variables.scss +4 -0
  58. package/src/store/dashboard.actions.ts +9 -10
  59. package/src/store/dashboard.reducer.ts +41 -13
  60. package/src/types/APIFilter.ts +1 -4
  61. package/src/types/ConfigRow.ts +2 -0
  62. package/src/types/Dashboard.ts +1 -1
  63. package/src/types/DashboardConfig.ts +2 -4
  64. package/src/types/DashboardFilters.ts +7 -0
  65. package/src/types/InitialState.ts +1 -1
  66. package/src/types/MultiDashboard.ts +2 -2
  67. package/src/types/SharedFilter.ts +2 -5
  68. package/src/types/Tab.ts +1 -1
  69. package/LICENSE +0 -201
  70. package/src/components/EditorWrapper/EditorWrapper.tsx +0 -52
  71. package/src/components/EditorWrapper/editor-wrapper.style.css +0 -13
  72. package/src/components/Filters.tsx +0 -88
  73. package/src/components/Header/FilterModal.tsx +0 -506
  74. package/src/components/VisualizationsPanel.tsx +0 -72
  75. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -6,8 +6,7 @@ import { DashboardContext, DashboardDispatchContext } from '../DashboardContext'
6
6
 
7
7
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
8
8
  import Icon from '@cdc/core/components/ui/Icon'
9
- import Modal from '@cdc/core/components/ui/Modal'
10
- import { Visualization } from '@cdc/core/types/Visualization'
9
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
11
10
  import { iconHash } from '../helpers/iconHash'
12
11
  import _ from 'lodash'
13
12
  import { DataDesignerModal } from './DataDesignerModal'
@@ -25,19 +24,19 @@ const labelHash = {
25
24
  world: 'World',
26
25
  'single-state': 'U.S. State',
27
26
  'filtered-text': 'Filtered Text',
28
- 'filter-dropdowns': 'Filter Dropdowns',
27
+ dashboardFilters: 'Filter Dropdowns',
29
28
  Sankey: 'Sankey Chart',
30
29
  table: 'Table'
31
30
  }
32
31
 
33
- type WidgetData = Visualization & { rowIdx: number; colIdx: number }
32
+ type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
34
33
  type WidgetProps = {
35
- data?: WidgetData
34
+ widgetConfig?: WidgetConfig
36
35
  addVisualization?: Function
37
36
  type: string
38
37
  }
39
38
 
40
- const Widget = ({ data, addVisualization, type }: WidgetProps) => {
39
+ const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
41
40
  const { overlay } = useGlobalContext()
42
41
  const { config } = useContext(DashboardContext)
43
42
  const rows = config.rows
@@ -54,10 +53,10 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
54
53
 
55
54
  const { rowIdx, colIdx } = result
56
55
 
57
- if (undefined !== data?.rowIdx) {
58
- rows[data.rowIdx].columns[data.colIdx].widget = null // Wipe from old position
56
+ if (undefined !== widgetConfig?.rowIdx) {
57
+ rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null // Wipe from old position
59
58
 
60
- rows[rowIdx].columns[colIdx].widget = data.uid // Add to new row and col
59
+ rows[rowIdx].columns[colIdx].widget = widgetConfig.uid // Add to new row and col
61
60
  } else if (!!addVisualization) {
62
61
  // Item does not exist, instantiate a new one
63
62
  const newViz = addVisualization()
@@ -76,19 +75,19 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
76
75
  isDragging: monitor.isDragging()
77
76
  })
78
77
  },
79
- [config.activeDashboard, config.rows]
78
+ [config.activeDashboard, config.rows, config.dashboard.sharedFilters]
80
79
  )
81
80
 
82
81
  const deleteWidget = () => {
83
- if (!data) return
84
- rows[data.rowIdx].columns[data.colIdx].widget = null
82
+ if (!widgetConfig) return
83
+ rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null
85
84
 
86
- delete visualizations[data.uid]
85
+ delete visualizations[widgetConfig.uid]
87
86
 
88
87
  if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
89
88
  config.dashboard.sharedFilters.forEach(sharedFilter => {
90
- if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(data.uid) !== -1) {
91
- sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(data.uid), 1)
89
+ if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(widgetConfig.uid) !== -1) {
90
+ sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(widgetConfig.uid), 1)
92
91
  }
93
92
  })
94
93
  }
@@ -97,99 +96,36 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
97
96
  }
98
97
 
99
98
  const editWidget = () => {
100
- if (!data) return
101
- visualizations[data.uid].editing = true
99
+ if (!widgetConfig) return
100
+ visualizations[widgetConfig.uid].editing = true
102
101
 
103
102
  updateConfig({ ...config, visualizations })
104
103
  }
105
104
 
106
- const FilterHideModal = configureData => {
107
- const currentVizKey = Object.keys(visualizations).find(vizKey => vizKey === configureData.uid) || ''
108
- const currentViz = config.visualizations && config.visualizations[currentVizKey]
109
- const onFilterHideChange = (e, index) => {
110
- const visualizations = { ...config.visualizations }
111
-
112
- if (currentVizKey) {
113
- const currentVizConfig = visualizations[currentVizKey]
114
-
115
- if (currentVizConfig) {
116
- if (!currentVizConfig.hide) currentVizConfig.hide = []
117
- if (!e.target.checked && currentVizConfig.hide.indexOf(index) === -1) {
118
- visualizations[currentVizKey].hide.push(index)
119
- } else if (e.target.checked && currentVizConfig.hide.indexOf(index) !== -1) {
120
- visualizations[currentVizKey].hide.splice(currentVizConfig.hide.indexOf(index), 1)
121
- }
122
- }
123
- }
124
-
125
- updateConfig({ ...config, visualizations })
126
- }
127
-
128
- const vizWithAutoLoad = Object.keys(config.visualizations).find(vizKey => config.visualizations[vizKey].autoLoad)
129
- const onToggleAutoLoad = e => {
130
- if (currentViz) {
131
- currentViz.autoLoad = e.target.checked
132
- updateConfig({ ...config, visualizations: { ...visualizations, [currentVizKey]: currentViz } })
133
- }
134
- }
135
-
136
- const showAutoLoadCheckbox = !vizWithAutoLoad || vizWithAutoLoad === currentVizKey
137
- return (
138
- <Modal>
139
- <Modal.Content>
140
- <div>Choose which filters to display:</div>
141
-
142
- {config.dashboard.sharedFilters &&
143
- config.dashboard.sharedFilters.length > 0 &&
144
- config.dashboard.sharedFilters.map((sharedFilter, index) => (
145
- <label>
146
- <input type='checkbox' defaultChecked={!configureData.hide || configureData.hide.indexOf(index) === -1} onChange={e => onFilterHideChange(e, index)} />
147
- {sharedFilter.key}
148
- </label>
149
- ))}
150
-
151
- {(!config.dashboard.sharedFilters || config.dashboard.sharedFilters.length === 0) && <>No dashboard filters added yet.</>}
152
-
153
- {showAutoLoadCheckbox && (
154
- <label>
155
- Make Autoload:
156
- <input type='checkbox' defaultChecked={currentViz?.autoLoad} onChange={onToggleAutoLoad} />
157
- </label>
158
- )}
159
- <div>
160
- <button style={{ margin: '1em' }} className='cove-button' onClick={() => overlay?.actions.toggleOverlay()}>
161
- Continue
162
- </button>
163
- </div>
164
- </Modal.Content>
165
- </Modal>
166
- )
167
- }
168
-
169
105
  let isConfigurationReady = false
170
- const dataConfiguredForRow = !!rows[data?.rowIdx]?.dataKey
171
- if (dataConfiguredForRow || ['markup-include', 'filter-dropdowns'].includes(type)) {
106
+ const dataConfiguredForRow = !!rows[widgetConfig?.rowIdx]?.dataKey
107
+ if (dataConfiguredForRow || ['dashboardFilters', 'markup-include'].includes(type)) {
172
108
  isConfigurationReady = true
173
109
  } else {
174
- if (data?.formattedData) {
110
+ if (widgetConfig?.formattedData) {
175
111
  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)
112
+ } else if (widgetConfig?.dataKey && widgetConfig?.dataDescription && config.datasets[widgetConfig.dataKey]) {
113
+ const formattedDataAttempt = transform.autoStandardize(config.datasets[widgetConfig.dataKey].data)
114
+ const canFormatData = !!transform.developerStandardize(formattedDataAttempt, widgetConfig.dataDescription)
179
115
  if (canFormatData) {
180
116
  isConfigurationReady = true
181
117
  }
182
118
  }
183
119
  }
184
120
 
185
- const needsDataConfiguration = !dataConfiguredForRow && type !== 'markup-include'
121
+ const needsDataConfiguration = !dataConfiguredForRow && widgetConfig?.type !== 'dashboardFilters'
186
122
 
187
123
  return (
188
124
  <>
189
125
  <div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
190
126
  <Icon display='move' className='drag-icon' />
191
127
  <div className='widget__content'>
192
- {data?.rowIdx !== undefined && (
128
+ {widgetConfig?.rowIdx !== undefined && (
193
129
  <div className='widget-menu'>
194
130
  {isConfigurationReady && (
195
131
  <button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
@@ -201,7 +137,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
201
137
  title='Configure Data'
202
138
  className='btn btn-configure'
203
139
  onClick={() => {
204
- overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(data) : <DataDesignerModal rowIndex={data.rowIdx} vizKey={data.uid} />)
140
+ overlay?.actions.openOverlay(<DataDesignerModal rowIndex={widgetConfig.rowIdx} vizKey={widgetConfig.uid} />)
205
141
  }}
206
142
  >
207
143
  {iconHash['gear']}
@@ -214,7 +150,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
214
150
  )}
215
151
  {iconHash[type]}
216
152
  <span>{labelHash[type]}</span>
217
- {data?.newViz && type !== 'filter-dropdowns' && (
153
+ {widgetConfig?.newViz && type !== 'dashboardFilters' && (
218
154
  <span onClick={editWidget} className='config-needed'>
219
155
  Configuration needed
220
156
  </span>
@@ -0,0 +1,51 @@
1
+ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
+ import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
3
+ import { APIFilter } from '../types/APIFilter'
4
+ import { SharedFilter } from '../types/SharedFilter'
5
+
6
+ export const getLoadingFilterMemo = (sharedAPIFilters, apiFilterDropdowns): APIFilterDropdowns =>
7
+ sharedAPIFilters.reduce((acc, curr) => {
8
+ const _key = curr.apiFilter.apiEndpoint
9
+ if (apiFilterDropdowns[_key] != null) return acc // don't overwrite fetched data.
10
+ acc[_key] = null
11
+ return acc
12
+ }, {})
13
+
14
+ const getParentParams = (childFilter: SharedFilter, sharedAPIFilters: SharedFilter[]): Record<'key' | 'value', string>[] | null => {
15
+ const _parents = sharedAPIFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
16
+ if (!_parents.length) return null
17
+
18
+ return _parents.flatMap(filter => {
19
+ const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
20
+ const value = filter.queuedActive || filter.active || ''
21
+ if (Array.isArray(value)) {
22
+ return value.map(_value => ({ key, value: _value }))
23
+ }
24
+ return [{ key, value }]
25
+ })
26
+ }
27
+
28
+ export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
29
+ const { textSelector, valueSelector } = apiFilter
30
+ return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
31
+ }
32
+
33
+ export const getToFetch = (sharedAPIFilters: SharedFilter[], apiFilterDropdowns: APIFilterDropdowns): Record<string, [string, number]> => {
34
+ const toFetch = {}
35
+ sharedAPIFilters.forEach((filter, index) => {
36
+ const baseEndpoint = filter.apiFilter.apiEndpoint
37
+ const _key = baseEndpoint
38
+ const isAPIFilter = apiFilterDropdowns[_key]
39
+ const parentParams = getParentParams(filter, sharedAPIFilters)
40
+ const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
41
+
42
+ if (notAllParentsSelected) return // don't send request for dependent children filter options
43
+ if (isAPIFilter && !parentParams) return // don't reload filter unless it's a child
44
+ const topLevelDataAlreadyLoaded = isAPIFilter && !filter.parents
45
+ if (topLevelDataAlreadyLoaded) return // don't reload top level filters
46
+
47
+ const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
48
+ toFetch[endpoint] = [_key, index]
49
+ })
50
+ return toFetch
51
+ }
@@ -0,0 +1,30 @@
1
+ import _ from 'lodash'
2
+ import { FilterBehavior } from '../components/Header/Header'
3
+ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
4
+ import { SharedFilter } from '../types/SharedFilter'
5
+ import { DashboardFilters } from '../types/DashboardFilters'
6
+
7
+ const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
8
+ const parentKey = sharedFilters[parentIndex].key
9
+ const childIndex = sharedFilters.findIndex(filter => filter.parents?.includes(parentKey))
10
+ if (childIndex !== -1) {
11
+ sharedFilters[childIndex].active = ''
12
+ }
13
+ }
14
+
15
+ export const changeFilterActive = (filterIndex: number, value: string | string[], sharedFilters: SharedFilter[], vizConfig: DashboardFilters): SharedFilter[] => {
16
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
17
+ const currentFilter = sharedFilters[filterIndex]
18
+ if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
19
+ sharedFiltersCopy[filterIndex].active = value
20
+ handleChildren(sharedFiltersCopy, filterIndex)
21
+ const queryParams = getQueryParams()
22
+ if (currentFilter.setByQueryParameter && queryParams[currentFilter.setByQueryParameter] !== currentFilter.active) {
23
+ queryParams[currentFilter.setByQueryParameter] = currentFilter.active
24
+ updateQueryString(queryParams)
25
+ }
26
+ } else {
27
+ sharedFiltersCopy[filterIndex].queuedActive = value
28
+ }
29
+ return sharedFiltersCopy
30
+ }
@@ -22,60 +22,26 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
22
22
  return maxTier
23
23
  }
24
24
 
25
- function filter(data, filters, condition) {
26
- return data
27
- ? 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
- : []
41
- }
42
-
43
- function setFilterValuesAndActiveFilter(filters: SharedFilter[], filteredData: Object[], i: number) {
44
- filters.forEach(sharedFilter => {
45
- if (sharedFilter.pivot) {
46
- sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
47
- } else if (sharedFilter.tier === i + 2 && !Array.isArray(sharedFilter.active)) {
48
- sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
49
- const valueAlreadySelected = sharedFilter.values.includes(sharedFilter.active)
50
- if (!valueAlreadySelected && sharedFilter.values.length > 0) {
51
- sharedFilter.active = sharedFilter.values[0]
25
+ function filter(data = [], filters: SharedFilter[], condition) {
26
+ return data.filter(row => {
27
+ const foundMatchingFilter = filters.find(filter => {
28
+ const currentValue = row[filter.columnName]
29
+ const selectedValue = filter.queuedActive || filter.active
30
+ let isNotTheSelectedValue = true
31
+ if (Array.isArray(selectedValue)) {
32
+ isNotTheSelectedValue = !selectedValue.includes(currentValue)
33
+ } else {
34
+ isNotTheSelectedValue = selectedValue && currentValue != selectedValue
35
+ }
36
+ const isFirstOccurrenceOfTier = filter.tier === condition
37
+ if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
38
+ return true
52
39
  }
53
- }
54
- })
55
- }
56
-
57
- const pivotData = (data, pivotFilter: SharedFilter) => {
58
- const pivotActive = pivotFilter.active as string[]
59
- const inactive = pivotFilter.values.filter(value => !pivotActive.includes(value))
60
- const pivotColumn = pivotFilter.columnName
61
- const valueColumn = pivotFilter.pivot
62
- const grouped = _.groupBy(data, val => val[pivotColumn])
63
- const newData = []
64
- for (const key in grouped) {
65
- const group = grouped[key]
66
-
67
- group.forEach((val, index) => {
68
- const row = newData[index] || {}
69
- if (!inactive.includes(key)) row[key] = val[valueColumn]
70
- const toAdd = _.omit(val, [pivotColumn, valueColumn, ...inactive])
71
- newData[index] = { ...row, ...toAdd }
72
40
  })
73
- }
74
- return newData
41
+ return !foundMatchingFilter
42
+ })
75
43
  }
76
44
 
77
- /** This function returns filtered data.
78
- * It also manipulates the filters by adding: tiers, filterOptions, and default selections */
79
45
  export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
80
46
  const maxTier = getMaxTierAndSetFilterTiers(filters)
81
47
 
@@ -84,13 +50,7 @@ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] =
84
50
 
85
51
  const filteredData = filter(_data, filters, i + 1)
86
52
 
87
- setFilterValuesAndActiveFilter(_.cloneDeep(filters), filteredData, i)
88
-
89
53
  if (lastIteration) {
90
- const pivotFilter = filters.find(filter => filter.pivot)
91
- if (pivotFilter) {
92
- return pivotData(filteredData, pivotFilter)
93
- }
94
54
  // not sure if this last run of filter() function is necessary.
95
55
  return filter(filteredData, filters, maxTier - 1)
96
56
  }
@@ -5,7 +5,7 @@ export const generateValuesForFilter = (columnName, _data) => {
5
5
  Object.keys(_data).forEach(key => {
6
6
  _data[key]?.forEach(row => {
7
7
  const value = row[columnName]
8
- if (!values.includes(value)) {
8
+ if (value && !values.includes(value)) {
9
9
  values.push(value)
10
10
  }
11
11
  })
@@ -0,0 +1,11 @@
1
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
2
+ import { DashboardFilters } from '../types/DashboardFilters'
3
+
4
+ export const getAutoLoadVisualization = (visualizations: Record<string, AnyVisualization>): DashboardFilters | undefined => {
5
+ const autoLoadViz = Object.values(visualizations).filter(vis => {
6
+ return vis.type === 'dashboardFilters' && vis.autoLoad
7
+ }) as DashboardFilters[]
8
+ if (autoLoadViz.length === 0) return
9
+ if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
10
+ return autoLoadViz[0]
11
+ }
@@ -26,12 +26,14 @@ export const getFilteredData = (state: DashboardState, initialFilteredData = {},
26
26
  config.rows.forEach((row, index) => {
27
27
  if (row.dataKey) {
28
28
  const applicableFilters = getApplicableFilters(config.dashboard, index)
29
+ const { dataKey, data, dataDescription } = row
30
+ const _data = state.data[dataKey] || data
29
31
  if (applicableFilters) {
30
- const { dataKey, data, dataDescription } = row
31
- const _data = state.data[dataKey] || data
32
32
  const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
33
33
 
34
34
  newFilteredData[index] = filterData(applicableFilters, formattedData)
35
+ } else {
36
+ newFilteredData[index] = _data || []
35
37
  }
36
38
  }
37
39
  })
@@ -1,12 +1,33 @@
1
1
  import _ from 'lodash'
2
2
  import { MultiDashboardConfig } from '../types/MultiDashboard'
3
3
  import DataTransform from '@cdc/core/helpers/DataTransform'
4
+ import { getApplicableFilters } from './getFilteredData'
5
+ import { filterData } from './filterData'
6
+ import Footnotes from '@cdc/core/types/Footnotes'
4
7
 
5
8
  const transform = new DataTransform()
6
9
 
10
+ export const getFootnotesVizConfig = (vizKey: string, rowNumber: number, config: MultiDashboardConfig) => {
11
+ const visualizationConfig = _.cloneDeep(config.visualizations[vizKey])
12
+
13
+ const data = config.datasets[visualizationConfig.dataKey]?.data
14
+ const dataColumns = data?.length ? Object.keys(data[0]) : []
15
+ const filters = (getApplicableFilters(config.dashboard, rowNumber) || []).filter(filter => dataColumns.includes(filter.columnName))
16
+ if (filters.length) {
17
+ visualizationConfig.formattedData = filterData(filters, data)
18
+ }
19
+ visualizationConfig.data = data
20
+ return visualizationConfig as Footnotes
21
+ }
22
+
7
23
  export const getVizConfig = (visualizationKey: string, rowNumber: number, config: MultiDashboardConfig, data: Object, filteredData?: Object) => {
24
+ if (rowNumber === undefined) return {}
8
25
  const visualizationConfig = _.cloneDeep(config.visualizations[visualizationKey])
9
26
  const rowData = config.rows[rowNumber]
27
+ if (rowData.footnotesId && rowData.footnotesId === visualizationKey) {
28
+ // return the footnotes visualization config with filtered data
29
+ return getFootnotesVizConfig(visualizationKey, rowNumber, config)
30
+ }
10
31
  if (rowData?.dataKey) {
11
32
  // data configured on the row
12
33
  Object.assign(visualizationConfig, _.pick(rowData, ['dataKey', 'dataDescription', 'formattedData', 'data']))
@@ -16,13 +37,13 @@ export const getVizConfig = (visualizationKey: string, rowNumber: number, config
16
37
  const filteredVizData = filteredData?.[rowNumber] ?? filteredData?.[visualizationKey]
17
38
 
18
39
  if (filteredVizData) {
19
- visualizationConfig.data = filteredVizData
40
+ visualizationConfig.data = filteredVizData || []
20
41
  if (visualizationConfig.formattedData) {
21
42
  visualizationConfig.formattedData = visualizationConfig.data
22
43
  }
23
44
  } else {
24
45
  const dataKey = visualizationConfig.dataKey || 'backwards-compatibility'
25
- visualizationConfig.data = data[dataKey]
46
+ visualizationConfig.data = data[dataKey] || []
26
47
  if (visualizationConfig.formattedData) {
27
48
  visualizationConfig.formattedData = transform.developerStandardize(visualizationConfig.data, visualizationConfig.dataDescription) || visualizationConfig.data
28
49
  }
@@ -1,9 +1,10 @@
1
1
  import { ConfigRow } from '../types/ConfigRow'
2
2
 
3
- export const getVizRowColumnLocator = (rows: ConfigRow[]) =>
3
+ export const getVizRowColumnLocator = (rows: ConfigRow[]): Record<string, { row: number; column: number }> =>
4
4
  rows.reduce((acc, curr, index) => {
5
5
  curr.columns?.forEach((column, columnIndex) => {
6
6
  if (column.widget !== undefined) acc[column.widget] = { row: index, column: columnIndex }
7
7
  })
8
+ if (curr.footnotesId) acc[curr.footnotesId] = { row: index, column: 0 }
8
9
  return acc
9
10
  }, {})
@@ -0,0 +1,5 @@
1
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
2
+
3
+ export const hasDashboardApplyBehavior = (visualizations: Record<string, AnyVisualization>) => {
4
+ return Object.values(visualizations).some(v => v.filterBehavior === 'Apply Button' && v.type === 'dashboardFilters')
5
+ }
@@ -1,5 +1,5 @@
1
1
  import Icon from '@cdc/core/components/ui/Icon'
2
- import { Visualization } from '@cdc/core/types/Visualization'
2
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
3
3
 
4
4
  export const iconHash = {
5
5
  'data-bite': <Icon display='databite' base />,
@@ -16,12 +16,12 @@ export const iconHash = {
16
16
  gear: <Icon display='gear' base />,
17
17
  tools: <Icon display='tools' base />,
18
18
  'filtered-text': <Icon display='filtered-text' base />,
19
- 'filter-dropdowns': <Icon display='filter-dropdowns' base />,
19
+ dashboardFilters: <Icon display='dashboardFilters' base />,
20
20
  table: <Icon display='table' base />,
21
21
  Sankey: <Icon display='sankey' base />
22
22
  }
23
23
 
24
- export const getIcon = (visualization: Visualization) => {
24
+ export const getIcon = (visualization: AnyVisualization) => {
25
25
  const { type, visualizationType, general } = visualization
26
26
  if (visualizationType) return iconHash[visualizationType]
27
27
  if (general?.geoType) {
@@ -0,0 +1,29 @@
1
+ import { getFormattedData } from './getFormattedData'
2
+ import { DashboardConfig } from '../types/DashboardConfig'
3
+
4
+ const mapDataToVisualizations = (config: DashboardConfig) => {
5
+ Object.keys(config.visualizations).forEach((vizKey, i) => {
6
+ const viz = config.visualizations[vizKey]
7
+ if (viz.dataKey && !viz.data) {
8
+ const data = config.datasets[viz.dataKey].data
9
+ config.visualizations[vizKey].data = data
10
+ config.visualizations[vizKey].formattedData = getFormattedData(data, viz.dataDescription)
11
+ }
12
+ })
13
+ }
14
+
15
+ const mapDataToRows = (config: DashboardConfig) => {
16
+ config.rows.forEach((row, i) => {
17
+ if (row.dataKey && !row.data) {
18
+ const data = config.datasets[row.dataKey].data
19
+ config.rows[i].data = data
20
+ config.rows[i].formattedData = getFormattedData(data, row.dataDescription)
21
+ }
22
+ })
23
+ }
24
+
25
+ export const mapDataToConfig = (config: DashboardConfig) => {
26
+ mapDataToVisualizations(config)
27
+ mapDataToRows(config)
28
+ return config
29
+ }
@@ -1,10 +1,9 @@
1
- import { FilterBehavior } from '../components/Header/Header'
2
1
  import { DataSet } from '../types/DataSet'
3
2
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
4
3
  import { getFormattedData } from './getFormattedData'
5
4
 
6
- export const processData = async (dataSet: DataSet, filterBehavior) => {
7
- if (dataSet.dataUrl && filterBehavior !== FilterBehavior.Apply) {
5
+ export const processData = async (dataSet: DataSet, hasFilterChangeBehavior: boolean) => {
6
+ if (dataSet.dataUrl && hasFilterChangeBehavior) {
8
7
  const dataset = await fetchRemoteData(`${dataSet.dataUrl}`)
9
8
  return getFormattedData(dataset, dataSet.dataDescription)
10
9
  }
@@ -0,0 +1,68 @@
1
+ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
+ import { SharedFilter } from '../types/SharedFilter'
3
+ import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
4
+ import { Visualization } from '@cdc/core/types/Visualization'
5
+ import _ from 'lodash'
6
+
7
+ export const isUpdateNeeded = (filters: SharedFilter[], currentQueryParams: Record<string, string>, newQueryParams: Record<string, string>): boolean => {
8
+ let needsUpdate = false
9
+ filters.find(filter => {
10
+ if (filter.type === 'urlfilter' && !Array.isArray(filter.active) && filter.filterBy === 'File Name') {
11
+ needsUpdate = true
12
+ return true
13
+ }
14
+ })
15
+ Object.keys(newQueryParams).forEach(updatedParam => {
16
+ if (decodeURIComponent(newQueryParams[updatedParam]) !== currentQueryParams[updatedParam]) {
17
+ needsUpdate = true
18
+ }
19
+ })
20
+ return needsUpdate
21
+ }
22
+
23
+ export const getDataURL = (updatedQSParams: Record<string, string>, dataUrl: URL, newFileName: string) => {
24
+ const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
25
+ const baseURL = dataUrl.origin + dataUrl.pathname
26
+ let dataUrlFinal = `${baseURL}${gatherQueryParams(baseURL, _params)}`
27
+
28
+ if (newFileName !== '') {
29
+ const fileExtension = dataUrl.pathname.split('.').pop()
30
+ const pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
31
+ dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(baseURL, _params)}`
32
+ }
33
+ return dataUrlFinal
34
+ }
35
+
36
+ export const getNewFileName = (newFileName: string, filter: SharedFilter, datasetKey: string) => {
37
+ const replacements = {
38
+ 'Remove Spaces': '',
39
+ 'Keep Spaces': ' ',
40
+ 'Replace With Underscore': '_'
41
+ }
42
+ let fileName = newFileName
43
+ if (filter.datasetKey === datasetKey) {
44
+ if (filter.fileName) {
45
+ // if a file name is found, ie, state_${query}, use that, ie. state_activeFilter.json
46
+ fileName = capitalizeSplitAndJoin.call(String(filter.fileName), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces'])
47
+ } else {
48
+ // if no file name is entered use the default active filter. ie. /activeFilter.json
49
+ fileName = filter.active as string
50
+ }
51
+ }
52
+
53
+ if (fileName?.includes('${query}')) {
54
+ fileName = fileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
55
+ }
56
+
57
+ return fileName
58
+ }
59
+
60
+ export const getVisualizationsWithFormattedData = (visualizations: Record<string, Visualization>, newData: Object) => {
61
+ return Object.keys(visualizations).reduce((acc, vizKey) => {
62
+ const dataKey = visualizations[vizKey].dataKey
63
+ if (newData[dataKey]) {
64
+ acc[vizKey].formattedData = newData[dataKey]
65
+ }
66
+ return acc
67
+ }, _.cloneDeep(visualizations))
68
+ }