@cdc/dashboard 4.24.5 → 4.24.9-1

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 (86) hide show
  1. package/dist/cdcdashboard.js +147572 -128223
  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/examples/single-state-dashboard-filters.json +421 -0
  8. package/examples/state-level.json +90136 -0
  9. package/examples/state-points.json +10474 -0
  10. package/examples/test-file.json +147 -0
  11. package/examples/testing.json +94456 -0
  12. package/index.html +25 -4
  13. package/package.json +12 -11
  14. package/src/CdcDashboard.tsx +5 -1
  15. package/src/CdcDashboardComponent.tsx +250 -327
  16. package/src/DashboardContext.tsx +15 -1
  17. package/src/_stories/Dashboard.stories.tsx +158 -40
  18. package/src/_stories/_mock/api-filter-chart.json +11 -35
  19. package/src/_stories/_mock/api-filter-map.json +17 -31
  20. package/src/_stories/_mock/bump-chart.json +3554 -0
  21. package/src/_stories/_mock/methodology.json +412 -0
  22. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  23. package/src/_stories/_mock/multi-viz.json +3 -4
  24. package/src/_stories/_mock/pivot-filter.json +14 -12
  25. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  26. package/src/components/CollapsibleVisualizationRow.tsx +44 -0
  27. package/src/components/Column.tsx +1 -1
  28. package/src/components/DashboardFilters/DashboardFilters.tsx +102 -0
  29. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +218 -0
  30. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +48 -0
  31. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +477 -0
  32. package/src/components/DashboardFilters/DashboardFiltersEditor/index.ts +1 -0
  33. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +191 -0
  34. package/src/components/DashboardFilters/index.ts +3 -0
  35. package/src/components/DataDesignerModal.tsx +9 -9
  36. package/src/components/ExpandCollapseButtons.tsx +20 -0
  37. package/src/components/Header/Header.tsx +1 -102
  38. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +24 -12
  39. package/src/components/Row.tsx +52 -19
  40. package/src/components/Toggle/Toggle.tsx +2 -4
  41. package/src/components/VisualizationRow.tsx +169 -30
  42. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +116 -0
  43. package/src/components/VisualizationsPanel/index.ts +1 -0
  44. package/src/components/VisualizationsPanel/visualizations-panel-styles.css +12 -0
  45. package/src/components/Widget.tsx +27 -90
  46. package/src/helpers/FilterBehavior.ts +4 -0
  47. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  48. package/src/helpers/apiFilterHelpers.ts +103 -0
  49. package/src/helpers/changeFilterActive.ts +39 -0
  50. package/src/helpers/filterData.ts +10 -48
  51. package/src/helpers/generateValuesForFilter.ts +1 -1
  52. package/src/helpers/getAutoLoadVisualization.ts +11 -0
  53. package/src/helpers/getFilteredData.ts +7 -5
  54. package/src/helpers/getVizConfig.ts +23 -2
  55. package/src/helpers/getVizRowColumnLocator.ts +2 -1
  56. package/src/helpers/hasDashboardApplyBehavior.ts +5 -0
  57. package/src/helpers/iconHash.tsx +5 -3
  58. package/src/helpers/loadAPIFilters.ts +74 -0
  59. package/src/helpers/mapDataToConfig.ts +29 -0
  60. package/src/helpers/processData.ts +2 -3
  61. package/src/helpers/reloadURLHelpers.ts +102 -0
  62. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  63. package/src/helpers/tests/apiFilterHelpers.test.ts +155 -0
  64. package/src/helpers/tests/filterData.test.ts +1 -93
  65. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  66. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +220 -0
  67. package/src/helpers/tests/reloadURLHelpers.test.ts +232 -0
  68. package/src/scss/editor-panel.scss +1 -1
  69. package/src/scss/grid.scss +34 -27
  70. package/src/scss/main.scss +41 -3
  71. package/src/scss/variables.scss +4 -0
  72. package/src/store/dashboard.actions.ts +12 -4
  73. package/src/store/dashboard.reducer.ts +30 -4
  74. package/src/types/APIFilter.ts +1 -5
  75. package/src/types/ConfigRow.ts +2 -0
  76. package/src/types/Dashboard.ts +1 -1
  77. package/src/types/DashboardConfig.ts +2 -4
  78. package/src/types/DashboardFilters.ts +7 -0
  79. package/src/types/InitialState.ts +1 -1
  80. package/src/types/MultiDashboard.ts +2 -2
  81. package/src/types/SharedFilter.ts +4 -6
  82. package/src/types/Tab.ts +1 -1
  83. package/src/components/Filters.tsx +0 -88
  84. package/src/components/Header/FilterModal.tsx +0 -510
  85. package/src/components/VisualizationsPanel.tsx +0 -95
  86. package/src/helpers/getApiFilterKey.ts +0 -5
@@ -0,0 +1,116 @@
1
+ import { useContext, useState } from 'react'
2
+ import type { AnyVisualization } from '@cdc/core/types/Visualization'
3
+ import Widget from '../Widget'
4
+ import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
5
+ import { Table } from '@cdc/core/types/Table'
6
+ import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
7
+ import { mapDataToConfig } from '../../helpers/mapDataToConfig'
8
+ import './visualizations-panel-styles.css'
9
+
10
+ const addVisualization = (type, subType) => {
11
+ const modalWillOpen = type !== 'markup-include'
12
+ const newVisualizationConfig: Partial<AnyVisualization> = {
13
+ filters: [],
14
+ filterBehavior: 'Filter Change',
15
+ newViz: type !== 'table',
16
+ openModal: modalWillOpen,
17
+ uid: type + Date.now(),
18
+ type
19
+ }
20
+
21
+ switch (type) {
22
+ case 'chart':
23
+ newVisualizationConfig.visualizationType = subType
24
+ break
25
+ case 'map':
26
+ newVisualizationConfig.general = {}
27
+ newVisualizationConfig.general.geoType = subType
28
+ break
29
+ case 'data-bite' || 'waffle-chart' || 'filtered-text':
30
+ newVisualizationConfig.visualizationType = type
31
+ break
32
+ case 'table':
33
+ const tableConfig: Table = { label: 'Data Table', show: true, showDownloadUrl: false, showVertical: true, expanded: true, collapsible: true }
34
+ newVisualizationConfig.table = tableConfig
35
+ newVisualizationConfig.columns = {}
36
+ newVisualizationConfig.dataFormat = {}
37
+ newVisualizationConfig.visualizationType = type
38
+ break
39
+ case 'markup-include':
40
+ newVisualizationConfig.contentEditor = {
41
+ inlineHTML: '<h2>Inline HTML</h2>',
42
+ markupVariables: [],
43
+ showHeader: true,
44
+ srcUrl: '#example',
45
+ title: 'Markup Include',
46
+ useInlineHTML: true
47
+ }
48
+ newVisualizationConfig.theme = 'theme-blue'
49
+ newVisualizationConfig.visual = {
50
+ border: false,
51
+ accent: false,
52
+ background: false,
53
+ hideBackgroundColor: false,
54
+ borderColorTheme: false
55
+ }
56
+ newVisualizationConfig.showEditorPanel = true
57
+ newVisualizationConfig.visualizationType = type
58
+
59
+ break
60
+ case 'dashboardFilters': {
61
+ newVisualizationConfig.sharedFilterIndexes = []
62
+ newVisualizationConfig.visualizationType = type
63
+ break
64
+ }
65
+ default:
66
+ newVisualizationConfig.visualizationType = type
67
+ break
68
+ }
69
+
70
+ return newVisualizationConfig
71
+ }
72
+
73
+ const VisualizationsPanel = () => {
74
+ const [advancedEditing, setAdvancedEditing] = useState(false)
75
+ const { config } = useContext(DashboardContext)
76
+ const dispatch = useContext(DashboardDispatchContext)
77
+ const loadConfig = newConfig => dispatch({ type: 'APPLY_CONFIG', payload: [mapDataToConfig(newConfig)] })
78
+ return (
79
+ <div className={`visualizations-panel${advancedEditing ? ' advanced-editor' : ''}`}>
80
+ <p style={{ fontSize: '14px' }}>Click and drag an item onto the grid to add it to your dashboard.</p>
81
+ <span className='subheading-3'>Chart</span>
82
+ <div className='drag-grid'>
83
+ <Widget addVisualization={() => addVisualization('chart', 'Bar')} type='Bar' />
84
+ <Widget addVisualization={() => addVisualization('chart', 'Line')} type='Line' />
85
+ <Widget addVisualization={() => addVisualization('chart', 'Pie')} type='Pie' />
86
+ <Widget addVisualization={() => addVisualization('chart', 'Sankey')} type='Sankey' />
87
+ </div>
88
+ <span className='subheading-3'>Map</span>
89
+ <div className='drag-grid'>
90
+ <Widget addVisualization={() => addVisualization('map', 'us')} type='us' />
91
+ <Widget addVisualization={() => addVisualization('map', 'world')} type='world' />
92
+ <Widget addVisualization={() => addVisualization('map', 'single-state')} type='single-state' />
93
+ </div>
94
+ <span className='subheading-3'>Misc.</span>
95
+ <div className='drag-grid'>
96
+ <Widget addVisualization={() => addVisualization('data-bite', '')} type='data-bite' />
97
+ <Widget addVisualization={() => addVisualization('waffle-chart', '')} type='waffle-chart' />
98
+ <Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
99
+ <Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
100
+ <Widget addVisualization={() => addVisualization('dashboardFilters', '')} type='dashboardFilters' />
101
+ <Widget addVisualization={() => addVisualization('table', '')} type='table' />
102
+ </div>
103
+ <span className='subheading-3'>Advanced</span>
104
+ <AdvancedEditor
105
+ loadConfig={loadConfig}
106
+ config={config}
107
+ convertStateToConfig={() => undefined}
108
+ onExpandCollapse={() => {
109
+ setAdvancedEditing(!advancedEditing)
110
+ }}
111
+ />
112
+ </div>
113
+ )
114
+ }
115
+
116
+ export default VisualizationsPanel
@@ -0,0 +1 @@
1
+ export { default } from './VisualizationsPanel'
@@ -0,0 +1,12 @@
1
+ .visualizations-panel {
2
+ background-color: #fff;
3
+ padding: 1em;
4
+ width: var(--editorWidth);
5
+ border-right: #c7c7c7 1px solid;
6
+ z-index: 1;
7
+ overflow-y: scroll;
8
+
9
+ &.advanced-editor {
10
+ width: 50vw;
11
+ }
12
+ }
@@ -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'
@@ -19,25 +18,26 @@ const labelHash = {
19
18
  Bar: 'Bar',
20
19
  Line: 'Line',
21
20
  'Spark Line': 'Spark Line',
21
+ 'Bump Chart': 'Bump Chart',
22
22
  Pie: 'Pie',
23
23
  us: 'United States (State- or County-Level)',
24
24
  'us-county': 'United States (State- or County-Level)',
25
25
  world: 'World',
26
26
  'single-state': 'U.S. State',
27
27
  'filtered-text': 'Filtered Text',
28
- 'filter-dropdowns': 'Filter Dropdowns',
28
+ dashboardFilters: 'Filter Dropdowns',
29
29
  Sankey: 'Sankey Chart',
30
30
  table: 'Table'
31
31
  }
32
32
 
33
- type WidgetData = Visualization & { rowIdx: number; colIdx: number }
33
+ type WidgetConfig = AnyVisualization & { rowIdx: number; colIdx: number }
34
34
  type WidgetProps = {
35
- data?: WidgetData
35
+ widgetConfig?: WidgetConfig
36
36
  addVisualization?: Function
37
37
  type: string
38
38
  }
39
39
 
40
- const Widget = ({ data, addVisualization, type }: WidgetProps) => {
40
+ const Widget = ({ widgetConfig, addVisualization, type }: WidgetProps) => {
41
41
  const { overlay } = useGlobalContext()
42
42
  const { config } = useContext(DashboardContext)
43
43
  const rows = config.rows
@@ -54,10 +54,10 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
54
54
 
55
55
  const { rowIdx, colIdx } = result
56
56
 
57
- if (undefined !== data?.rowIdx) {
58
- rows[data.rowIdx].columns[data.colIdx].widget = null // Wipe from old position
57
+ if (undefined !== widgetConfig?.rowIdx) {
58
+ rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null // Wipe from old position
59
59
 
60
- rows[rowIdx].columns[colIdx].widget = data.uid // Add to new row and col
60
+ rows[rowIdx].columns[colIdx].widget = widgetConfig.uid // Add to new row and col
61
61
  } else if (!!addVisualization) {
62
62
  // Item does not exist, instantiate a new one
63
63
  const newViz = addVisualization()
@@ -76,19 +76,19 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
76
76
  isDragging: monitor.isDragging()
77
77
  })
78
78
  },
79
- [config.activeDashboard, config.rows]
79
+ [config.activeDashboard, config.rows, config.dashboard.sharedFilters]
80
80
  )
81
81
 
82
82
  const deleteWidget = () => {
83
- if (!data) return
84
- rows[data.rowIdx].columns[data.colIdx].widget = null
83
+ if (!widgetConfig) return
84
+ rows[widgetConfig.rowIdx].columns[widgetConfig.colIdx].widget = null
85
85
 
86
- delete visualizations[data.uid]
86
+ delete visualizations[widgetConfig.uid]
87
87
 
88
88
  if (config.dashboard.sharedFilters && config.dashboard.sharedFilters.length > 0) {
89
89
  config.dashboard.sharedFilters.forEach(sharedFilter => {
90
- if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(data.uid) !== -1) {
91
- sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(data.uid), 1)
90
+ if (sharedFilter.usedBy && sharedFilter.usedBy.indexOf(widgetConfig.uid) !== -1) {
91
+ sharedFilter.usedBy.splice(sharedFilter.usedBy.indexOf(widgetConfig.uid), 1)
92
92
  }
93
93
  })
94
94
  }
@@ -97,99 +97,36 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
97
97
  }
98
98
 
99
99
  const editWidget = () => {
100
- if (!data) return
101
- visualizations[data.uid].editing = true
100
+ if (!widgetConfig) return
101
+ visualizations[widgetConfig.uid].editing = true
102
102
 
103
103
  updateConfig({ ...config, visualizations })
104
104
  }
105
105
 
106
- const FilterHideModal = configureData => {
107
- const currentVizKey = Object.keys(visualizations).find(vizKey => vizKey === configureData.uid) || ''
108
- const currentViz = config.visualizations && config.visualizations[currentVizKey]
109
- const onFilterHideChange = (e, index) => {
110
- const visualizations = { ...config.visualizations }
111
-
112
- if (currentVizKey) {
113
- const currentVizConfig = visualizations[currentVizKey]
114
-
115
- if (currentVizConfig) {
116
- if (!currentVizConfig.hide) currentVizConfig.hide = []
117
- if (!e.target.checked && currentVizConfig.hide.indexOf(index) === -1) {
118
- visualizations[currentVizKey].hide.push(index)
119
- } else if (e.target.checked && currentVizConfig.hide.indexOf(index) !== -1) {
120
- visualizations[currentVizKey].hide.splice(currentVizConfig.hide.indexOf(index), 1)
121
- }
122
- }
123
- }
124
-
125
- updateConfig({ ...config, visualizations })
126
- }
127
-
128
- const vizWithAutoLoad = Object.keys(config.visualizations).find(vizKey => config.visualizations[vizKey].autoLoad)
129
- const onToggleAutoLoad = e => {
130
- if (currentViz) {
131
- currentViz.autoLoad = e.target.checked
132
- updateConfig({ ...config, visualizations: { ...visualizations, [currentVizKey]: currentViz } })
133
- }
134
- }
135
-
136
- const showAutoLoadCheckbox = !vizWithAutoLoad || vizWithAutoLoad === currentVizKey
137
- return (
138
- <Modal>
139
- <Modal.Content>
140
- <div>Choose which filters to display:</div>
141
-
142
- {config.dashboard.sharedFilters &&
143
- config.dashboard.sharedFilters.length > 0 &&
144
- config.dashboard.sharedFilters.map((sharedFilter, index) => (
145
- <label>
146
- <input type='checkbox' defaultChecked={!configureData.hide || configureData.hide.indexOf(index) === -1} onChange={e => onFilterHideChange(e, index)} />
147
- {sharedFilter.key}
148
- </label>
149
- ))}
150
-
151
- {(!config.dashboard.sharedFilters || config.dashboard.sharedFilters.length === 0) && <>No dashboard filters added yet.</>}
152
-
153
- {showAutoLoadCheckbox && (
154
- <label>
155
- Make Autoload:
156
- <input type='checkbox' defaultChecked={currentViz?.autoLoad} onChange={onToggleAutoLoad} />
157
- </label>
158
- )}
159
- <div>
160
- <button style={{ margin: '1em' }} className='cove-button' onClick={() => overlay?.actions.toggleOverlay()}>
161
- Continue
162
- </button>
163
- </div>
164
- </Modal.Content>
165
- </Modal>
166
- )
167
- }
168
-
169
106
  let isConfigurationReady = false
170
- const dataConfiguredForRow = !!rows[data?.rowIdx]?.dataKey
171
- if (dataConfiguredForRow || ['filter-dropdowns', 'markup-include'].includes(type)) {
107
+ const dataConfiguredForRow = !!rows[widgetConfig?.rowIdx]?.dataKey
108
+ if (dataConfiguredForRow || ['dashboardFilters', 'markup-include'].includes(type)) {
172
109
  isConfigurationReady = true
173
110
  } else {
174
- if (data?.formattedData) {
111
+ if (widgetConfig?.formattedData) {
175
112
  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)
113
+ } else if (widgetConfig?.dataKey && widgetConfig?.dataDescription && config.datasets[widgetConfig.dataKey]) {
114
+ const formattedDataAttempt = transform.autoStandardize(config.datasets[widgetConfig.dataKey].data)
115
+ const canFormatData = !!transform.developerStandardize(formattedDataAttempt, widgetConfig.dataDescription)
179
116
  if (canFormatData) {
180
117
  isConfigurationReady = true
181
118
  }
182
119
  }
183
120
  }
184
121
 
185
- const needsDataConfiguration = !dataConfiguredForRow
122
+ const needsDataConfiguration = !dataConfiguredForRow && widgetConfig?.type !== 'dashboardFilters'
186
123
 
187
124
  return (
188
125
  <>
189
126
  <div className='widget' ref={drag} style={{ opacity: isDragging ? 0.5 : 1 }} {...collected}>
190
127
  <Icon display='move' className='drag-icon' />
191
128
  <div className='widget__content'>
192
- {data?.rowIdx !== undefined && (
129
+ {widgetConfig?.rowIdx !== undefined && (
193
130
  <div className='widget-menu'>
194
131
  {isConfigurationReady && (
195
132
  <button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
@@ -201,7 +138,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
201
138
  title='Configure Data'
202
139
  className='btn btn-configure'
203
140
  onClick={() => {
204
- overlay?.actions.openOverlay(type === 'filter-dropdowns' ? FilterHideModal(data) : <DataDesignerModal rowIndex={data.rowIdx} vizKey={data.uid} />)
141
+ overlay?.actions.openOverlay(<DataDesignerModal rowIndex={widgetConfig.rowIdx} vizKey={widgetConfig.uid} />)
205
142
  }}
206
143
  >
207
144
  {iconHash['gear']}
@@ -214,7 +151,7 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
214
151
  )}
215
152
  {iconHash[type]}
216
153
  <span>{labelHash[type]}</span>
217
- {data?.newViz && type !== 'filter-dropdowns' && (
154
+ {widgetConfig?.newViz && type !== 'dashboardFilters' && (
218
155
  <span onClick={editWidget} className='config-needed'>
219
156
  Configuration needed
220
157
  </span>
@@ -0,0 +1,4 @@
1
+ export const FilterBehavior = {
2
+ Apply: 'Apply Button',
3
+ OnChange: 'Filter Change'
4
+ }
@@ -0,0 +1,49 @@
1
+ import _ from 'lodash'
2
+ import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
3
+ import { SharedFilter } from '../types/SharedFilter'
4
+
5
+ // Gets filter values from dataset
6
+ const generateValuesForFilter = (columnName, data: Record<string, any[]>) => {
7
+ const values: string[] = []
8
+ // data is a dataset this loops through ALL datasets to find matching values
9
+ // not sure if this is desired behavior
10
+
11
+ const d = Object.values(data) || []
12
+ d.forEach((rows: any[]) => {
13
+ rows?.forEach(row => {
14
+ const value = row[columnName]
15
+ if (value !== undefined && !values.includes(value)) {
16
+ values.push(String(value))
17
+ }
18
+ })
19
+ })
20
+ return values
21
+ }
22
+
23
+ const getSelector = (filter: SharedFilter) => {
24
+ return filter.type === 'urlfilter' ? filter.apiFilter?.valueSelector : filter.columnName
25
+ }
26
+
27
+ export const addValuesToDashboardFilters = (filters: SharedFilter[], data: Record<string, any[]>): Array<SharedFilter> => {
28
+ return filters?.map(filter => {
29
+ if (filter.type === 'urlfilter') return filter
30
+ const filterCopy = _.cloneDeep(filter)
31
+ const filterValues = generateValuesForFilter(getSelector(filter), data)
32
+ filterCopy.values = filterValues
33
+ if (filterValues.length > 0) {
34
+ const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
35
+ if (queryStringFilterValue) {
36
+ filterCopy.active = queryStringFilterValue
37
+ } else if (filter.multiSelect) {
38
+ const defaultValues = filterCopy.values
39
+ const active: string[] = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
40
+ filterCopy.active = active.filter(val => defaultValues.includes(val))
41
+ } else {
42
+ const defaultValue = filterCopy.values[0] || filterCopy.active
43
+ const active = Array.isArray(filterCopy.active) ? filterCopy.active[0] : filterCopy.active
44
+ filterCopy.active = filterCopy.values.includes(active) ? active : defaultValue
45
+ }
46
+ }
47
+ return filterCopy
48
+ })
49
+ }
@@ -0,0 +1,103 @@
1
+ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
+ import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
3
+ import { APIFilter } from '../types/APIFilter'
4
+ import { SharedFilter } from '../types/SharedFilter'
5
+ import _ from 'lodash'
6
+ import { getQueryParams } from '@cdc/core/helpers/queryStringUtils'
7
+
8
+ /** key for the dropdowns object */
9
+ type DropdownsKey = string
10
+
11
+ export const getLoadingFilterMemo = (
12
+ apiFiltersEndpoints: string[],
13
+ apiFilterDropdowns,
14
+ changedChildFilterIndexes = []
15
+ ): APIFilterDropdowns =>
16
+ apiFiltersEndpoints.reduce((acc, endpoint, currIndex) => {
17
+ const _key: DropdownsKey = endpoint
18
+ const hasChanged = changedChildFilterIndexes.includes(currIndex)
19
+ if (apiFilterDropdowns[_key] != null && !hasChanged) {
20
+ acc[_key] = apiFilterDropdowns[_key]
21
+ } else {
22
+ acc[_key] = null
23
+ }
24
+ return acc
25
+ }, {})
26
+
27
+ const getParentParams = (
28
+ childFilter: SharedFilter,
29
+ sharedFilters: SharedFilter[]
30
+ ): Record<'key' | 'value', string>[] | null => {
31
+ const _parents = sharedFilters.filter(parentFilter => childFilter.parents?.includes(parentFilter.key))
32
+ if (!(_parents || []).length) return null
33
+
34
+ return _parents.flatMap(filter => {
35
+ const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
36
+ const value = filter.queuedActive || filter.active || ''
37
+ if (Array.isArray(value)) {
38
+ return value.map(_value => ({ key, value: _value.toString() }))
39
+ }
40
+ return [{ key, value: value.toString() }]
41
+ })
42
+ }
43
+
44
+ export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): DropdownOptions => {
45
+ const { textSelector, valueSelector } = apiFilter
46
+ return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
47
+ }
48
+
49
+ /** API endpoint to fetch */
50
+ type Endpoint = string
51
+ type SharedFilterIndex = number
52
+ export const getToFetch = (
53
+ sharedFilters: SharedFilter[],
54
+ apiFilterDropdowns: APIFilterDropdowns
55
+ ): Record<Endpoint, [DropdownsKey, SharedFilterIndex]> => {
56
+ const toFetch = {}
57
+ sharedFilters.forEach((filter, index) => {
58
+ const baseEndpoint = filter.apiFilter?.apiEndpoint
59
+ if (!baseEndpoint) return
60
+ const _key = baseEndpoint
61
+ if (apiFilterDropdowns[_key]) return // don't reload cached filter
62
+ const parentParams = getParentParams(filter, sharedFilters)
63
+ const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
64
+
65
+ if (notAllParentsSelected) return // don't send request for dependent children filter options
66
+
67
+ const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
68
+ toFetch[endpoint] = [_key, index]
69
+ })
70
+ return toFetch
71
+ }
72
+
73
+ export const setAutoLoadDefaultValue = (
74
+ sharedFilterIndex: number,
75
+ dropdownOptions: DropdownOptions,
76
+ sharedFilters: SharedFilter[],
77
+ autoLoadFilterIndexes: number[]
78
+ ): SharedFilter => {
79
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
80
+ const sharedFilter = _.cloneDeep(sharedFiltersCopy[sharedFilterIndex])
81
+ if (!autoLoadFilterIndexes.length || !dropdownOptions?.length) return sharedFilter // no autoLoading happening
82
+ if (autoLoadFilterIndexes.includes(sharedFilterIndex)) {
83
+ const filterParents = sharedFiltersCopy.filter(f => sharedFilter.parents?.includes(f.key))
84
+ const notAllParentFiltersSelected = filterParents.some(p => !(p.active || p.queuedActive))
85
+ if (filterParents && notAllParentFiltersSelected) return sharedFilter
86
+ const defaultValue =
87
+ sharedFilter.filterStyle === 'multi-select' ? [dropdownOptions[0]?.value] : dropdownOptions[0]?.value
88
+ if (!sharedFilter.active) {
89
+ const queryParams = getQueryParams()
90
+ const defaultQueryParamValue = queryParams[sharedFilter?.queryParameter]
91
+ sharedFilter.active = defaultQueryParamValue || defaultValue
92
+ } else if (sharedFilter.filterStyle === 'multi-select') {
93
+ const currentOption = (sharedFilter.active as string[]).filter(activeVal =>
94
+ dropdownOptions.find(option => option.value === activeVal)
95
+ )
96
+ sharedFilter.active = currentOption.length ? currentOption : defaultValue
97
+ } else {
98
+ const currentOption = dropdownOptions.find(option => option.value === sharedFilter.active)
99
+ sharedFilter.active = currentOption ? currentOption.value : defaultValue
100
+ }
101
+ }
102
+ return sharedFilter
103
+ }
@@ -0,0 +1,39 @@
1
+ import _ from 'lodash'
2
+ import { FilterBehavior } from '../helpers/FilterBehavior'
3
+ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
4
+ import { SharedFilter } from '../types/SharedFilter'
5
+ import { DashboardFilters } from '../types/DashboardFilters'
6
+
7
+ const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
8
+ const parentKey = sharedFilters[parentIndex].key
9
+ const childFilterIndexes = sharedFilters
10
+ .map((filter, index) => (filter.parents?.includes(parentKey) ? index : null))
11
+ .filter(i => i !== null)
12
+ if (childFilterIndexes.length) {
13
+ childFilterIndexes.forEach(filterIndex => {
14
+ sharedFilters[filterIndex].active = ''
15
+ })
16
+ }
17
+ return childFilterIndexes
18
+ }
19
+
20
+ export const changeFilterActive = (
21
+ filterIndex: number,
22
+ value: string | string[],
23
+ sharedFilters: SharedFilter[],
24
+ vizConfig: DashboardFilters
25
+ ): [SharedFilter[], number[]] => {
26
+ const sharedFiltersCopy = _.cloneDeep(sharedFilters)
27
+ const currentFilter = sharedFiltersCopy[filterIndex]
28
+ if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
29
+ sharedFiltersCopy[filterIndex].active = value
30
+ const queryParams = getQueryParams()
31
+ if (currentFilter.setByQueryParameter && queryParams[currentFilter.setByQueryParameter] !== currentFilter.active) {
32
+ queryParams[currentFilter.setByQueryParameter] = currentFilter.active
33
+ updateQueryString(queryParams)
34
+ }
35
+ } else {
36
+ sharedFiltersCopy[filterIndex].queuedActive = value
37
+ }
38
+ return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
39
+ }
@@ -22,58 +22,26 @@ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
22
22
  return maxTier
23
23
  }
24
24
 
25
- function filter(data, filters, condition) {
26
- return data ? data.filter(row => {
27
- const found = filters.find(filter => {
28
- if (filter.pivot) return false
25
+ function filter(data = [], filters: SharedFilter[], condition) {
26
+ return data.filter(row => {
27
+ const foundMatchingFilter = filters.find(filter => {
29
28
  const currentValue = row[filter.columnName]
30
29
  const selectedValue = filter.queuedActive || filter.active
31
- const isNotTheSelectedValue = selectedValue && currentValue != selectedValue
30
+ let isNotTheSelectedValue = true
31
+ if (Array.isArray(selectedValue)) {
32
+ isNotTheSelectedValue = !selectedValue.includes(currentValue)
33
+ } else {
34
+ isNotTheSelectedValue = selectedValue && currentValue != selectedValue
35
+ }
32
36
  const isFirstOccurrenceOfTier = filter.tier === condition
33
37
  if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
34
38
  return true
35
39
  }
36
40
  })
37
- return !found
38
- }) : []
39
- }
40
-
41
- function setFilterValuesAndActiveFilter(filters: SharedFilter[], filteredData: Object[], i: number) {
42
- filters.forEach(sharedFilter => {
43
- if (sharedFilter.pivot) {
44
- sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
45
- } else if (sharedFilter.tier === i + 2 && !Array.isArray(sharedFilter.active)) {
46
- sharedFilter.values = _.uniq(filteredData.map(row => row[sharedFilter.columnName]))
47
- const valueAlreadySelected = sharedFilter.values.includes(sharedFilter.active)
48
- if (!valueAlreadySelected && sharedFilter.values.length > 0) {
49
- sharedFilter.active = sharedFilter.values[0]
50
- }
51
- }
41
+ return !foundMatchingFilter
52
42
  })
53
43
  }
54
44
 
55
- const pivotData = (data, pivotFilter: SharedFilter) => {
56
- const pivotActive = pivotFilter.active as string[]
57
- const inactive = pivotFilter.values.filter(value => !pivotActive.includes(value))
58
- const pivotColumn = pivotFilter.columnName
59
- const valueColumn = pivotFilter.pivot
60
- const grouped = _.groupBy(data, val => val[pivotColumn])
61
- const newData = []
62
- for (const key in grouped) {
63
- const group = grouped[key]
64
-
65
- group.forEach((val, index) => {
66
- const row = newData[index] || {}
67
- if (!inactive.includes(key)) row[key] = val[valueColumn]
68
- const toAdd = _.omit(val, [pivotColumn, valueColumn, ...inactive])
69
- newData[index] = { ...toAdd, ...row }
70
- })
71
- }
72
- return newData
73
- }
74
-
75
- /** This function returns filtered data.
76
- * It also manipulates the filters by adding: tiers, filterOptions, and default selections */
77
45
  export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
78
46
  const maxTier = getMaxTierAndSetFilterTiers(filters)
79
47
 
@@ -82,13 +50,7 @@ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] =
82
50
 
83
51
  const filteredData = filter(_data, filters, i + 1)
84
52
 
85
- setFilterValuesAndActiveFilter(filters, filteredData, i)
86
-
87
53
  if (lastIteration) {
88
- const pivotFilter = filters.find(filter => filter.pivot)
89
- if (pivotFilter) {
90
- return pivotData(filteredData, pivotFilter)
91
- }
92
54
  // not sure if this last run of filter() function is necessary.
93
55
  return filter(filteredData, filters, maxTier - 1)
94
56
  }
@@ -5,7 +5,7 @@ export const generateValuesForFilter = (columnName, _data) => {
5
5
  Object.keys(_data).forEach(key => {
6
6
  _data[key]?.forEach(row => {
7
7
  const value = row[columnName]
8
- if (!values.includes(value)) {
8
+ if (value && !values.includes(value)) {
9
9
  values.push(value)
10
10
  }
11
11
  })
@@ -0,0 +1,11 @@
1
+ import { AnyVisualization } from '@cdc/core/types/Visualization'
2
+ import { DashboardFilters } from '../types/DashboardFilters'
3
+
4
+ export const getAutoLoadVisualization = (visualizations: Record<string, AnyVisualization>): DashboardFilters | undefined => {
5
+ const autoLoadViz = Object.values(visualizations).filter(vis => {
6
+ return vis.type === 'dashboardFilters' && vis.autoLoad
7
+ }) as DashboardFilters[]
8
+ if (autoLoadViz.length === 0) return
9
+ if (autoLoadViz.length > 1) throw new Error('Only one filter row can be autoloaded')
10
+ return autoLoadViz[0]
11
+ }