@cdc/dashboard 4.24.7 → 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 (41) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcdashboard.js +131733 -123191
  3. package/examples/single-state-dashboard-filters.json +421 -0
  4. package/examples/state-level.json +90136 -0
  5. package/examples/state-points.json +10474 -0
  6. package/examples/test-file.json +147 -0
  7. package/examples/testing.json +94456 -0
  8. package/index.html +18 -6
  9. package/package.json +9 -9
  10. package/src/CdcDashboardComponent.tsx +154 -90
  11. package/src/DashboardContext.tsx +7 -1
  12. package/src/_stories/Dashboard.stories.tsx +124 -10
  13. package/src/_stories/_mock/api-filter-map.json +1 -1
  14. package/src/_stories/_mock/bump-chart.json +3554 -0
  15. package/src/_stories/_mock/methodology.json +412 -0
  16. package/src/_stories/_mock/methodologyAPI.ts +90 -0
  17. package/src/_stories/_mock/multi-viz.json +1 -1
  18. package/src/_stories/_mock/single-state-dashboard-filters.json +390 -0
  19. package/src/components/DashboardFilters/DashboardFilters.tsx +39 -17
  20. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +2 -2
  21. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +141 -31
  22. package/src/components/DashboardFilters/DashboardFiltersWrapper.tsx +66 -18
  23. package/src/components/Header/Header.tsx +0 -5
  24. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +20 -8
  25. package/src/components/Row.tsx +1 -1
  26. package/src/components/VisualizationRow.tsx +98 -17
  27. package/src/components/Widget.tsx +1 -0
  28. package/src/helpers/FilterBehavior.ts +4 -0
  29. package/src/helpers/addValuesToDashboardFilters.ts +49 -0
  30. package/src/helpers/apiFilterHelpers.ts +70 -18
  31. package/src/helpers/changeFilterActive.ts +17 -8
  32. package/src/helpers/getFilteredData.ts +4 -4
  33. package/src/helpers/iconHash.tsx +2 -0
  34. package/src/helpers/loadAPIFilters.ts +74 -0
  35. package/src/helpers/reloadURLHelpers.ts +41 -7
  36. package/src/helpers/tests/addValuesToDashboardFilters.test.ts +44 -0
  37. package/src/helpers/tests/apiFilterHelpers.test.ts +155 -0
  38. package/src/helpers/tests/getFilteredData.test.ts +86 -0
  39. package/src/helpers/tests/loadAPIFiltersWrapper.test.ts +220 -0
  40. package/src/helpers/tests/reloadURLHelpers.test.ts +232 -0
  41. package/src/types/SharedFilter.ts +2 -1
@@ -27,10 +27,21 @@ type VisualizationWrapperProps = {
27
27
  row: ConfigRow
28
28
  }
29
29
 
30
- const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({ allExpanded, currentViewport, groupName, row, children }) => {
30
+ const VisualizationWrapper: React.FC<VisualizationWrapperProps> = ({
31
+ allExpanded,
32
+ currentViewport,
33
+ groupName,
34
+ row,
35
+ children
36
+ }) => {
31
37
  return row.expandCollapseAllButtons ? (
32
38
  <div className='collapsable-multiviz-container'>
33
- <CollapsibleVisualizationRow allExpanded={allExpanded} fontSize={'26px'} groupName={groupName} currentViewport={currentViewport}>
39
+ <CollapsibleVisualizationRow
40
+ allExpanded={allExpanded}
41
+ fontSize={'26px'}
42
+ groupName={groupName}
43
+ currentViewport={currentViewport}
44
+ >
34
45
  {children}
35
46
  </CollapsibleVisualizationRow>
36
47
  </div>
@@ -54,7 +65,17 @@ type VizRowProps = {
54
65
  currentViewport: ViewPort
55
66
  }
56
67
 
57
- const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOverride, groupName, row, rowIndex: index, setSharedFilter, updateChildConfig, applyFilters, apiFilterDropdowns, handleOnChange, currentViewport }) => {
68
+ const VisualizationRow: React.FC<VizRowProps> = ({
69
+ allExpanded,
70
+ filteredDataOverride,
71
+ groupName,
72
+ row,
73
+ rowIndex: index,
74
+ setSharedFilter,
75
+ updateChildConfig,
76
+ apiFilterDropdowns,
77
+ currentViewport
78
+ }) => {
58
79
  const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
59
80
  const [show, setShow] = React.useState(row.columns.map((col, i) => i === 0))
60
81
  const setToggled = (colIndex: number) => {
@@ -81,7 +102,9 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
81
102
  }, [config, row, rawData, dashboardFilteredData])
82
103
 
83
104
  const applyButtonNotClicked = (vizConfig: DashboardFilters): boolean => {
84
- const dashboardFilters = Object.values(config.visualizations).filter(v => v.type === 'dashboardFilters') as DashboardFilters[]
105
+ const dashboardFilters = Object.values(config.visualizations).filter(
106
+ v => v.type === 'dashboardFilters'
107
+ ) as DashboardFilters[]
85
108
  const applyFilters = dashboardFilters.filter(v => !v.autoLoad).flatMap(v => v.sharedFilterIndexes)
86
109
  if (hasDashboardApplyBehavior(config.visualizations) && vizConfig.autoLoad) {
87
110
  return applyFilters.some(index => {
@@ -94,11 +117,16 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
94
117
  return false
95
118
  }
96
119
  return (
97
- <div className={`row mb-5 ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`} key={`row__${index}`}>
98
- {row.toggle && <Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />}
120
+ <div
121
+ className={`row mb-5 ${row.equalHeight ? 'equal-height' : ''} ${row.toggle ? 'toggle' : ''}`}
122
+ key={`row__${index}`}
123
+ >
124
+ {row.toggle && (
125
+ <Toggle row={row} visualizations={config.visualizations} active={show.indexOf(true)} setToggled={setToggled} />
126
+ )}
99
127
  {row.columns.map((col, colIndex) => {
100
128
  if (col.width) {
101
- if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col-md-${col.width}`}></div>
129
+ if (!col.widget) return <div key={`row__${index}__col__${colIndex}`} className={`col col-${col.width}`}></div>
102
130
 
103
131
  const visualizationConfig = getVizConfig(col.widget, index, config, rawData, dashboardFilteredData)
104
132
  if (filteredDataOverride) {
@@ -108,22 +136,37 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
108
136
  }
109
137
  }
110
138
 
111
- const setsSharedFilter = config.dashboard.sharedFilters && config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
112
- const setSharedFilterValue = setsSharedFilter ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active : undefined
139
+ const setsSharedFilter =
140
+ config.dashboard.sharedFilters &&
141
+ config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget).length > 0
142
+ const setSharedFilterValue = setsSharedFilter
143
+ ? config.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.setBy === col.widget)[0].active
144
+ : undefined
113
145
  const tableLink = (
114
146
  <a href={`#data-table-${visualizationConfig.dataKey}`} className='margin-left-href'>
115
147
  {visualizationConfig.dataKey} (Go to Table)
116
148
  </a>
117
149
  )
118
- const hideFilter = inNoDataState && visualizationConfig.type === 'dashboardFilters' && applyButtonNotClicked(visualizationConfig)
150
+ const hideFilter =
151
+ inNoDataState &&
152
+ visualizationConfig.type === 'dashboardFilters' &&
153
+ applyButtonNotClicked(visualizationConfig)
119
154
 
120
155
  const shouldShow = row.toggle === undefined || (row.toggle && show[colIndex])
121
156
 
122
157
  const body = <></>
123
158
 
124
159
  return (
125
- <div key={`vis__${index}__${colIndex}`} className={`col-${col.width} ${!shouldShow ? 'd-none' : ''}`}>
126
- <VisualizationWrapper allExpanded={allExpanded} currentViewport={currentViewport} groupName={groupName} row={row}>
160
+ <div
161
+ key={`vis__${index}__${colIndex}`}
162
+ className={`p-1 col-12 col-md-${col.width} ${!shouldShow ? 'd-none' : ''}`}
163
+ >
164
+ <VisualizationWrapper
165
+ allExpanded={allExpanded}
166
+ currentViewport={currentViewport}
167
+ groupName={groupName}
168
+ row={row}
169
+ >
127
170
  {visualizationConfig.type === 'chart' && (
128
171
  <CdcChart
129
172
  key={col.widget}
@@ -135,7 +178,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
135
178
  }}
136
179
  setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
137
180
  isDashboard={true}
138
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
181
+ link={
182
+ config.table &&
183
+ config.table.show &&
184
+ config.datasets &&
185
+ visualizationConfig.table &&
186
+ visualizationConfig.table.showDataTableLink
187
+ ? tableLink
188
+ : undefined
189
+ }
139
190
  configUrl={undefined}
140
191
  setEditing={undefined}
141
192
  hostname={undefined}
@@ -154,7 +205,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
154
205
  setSharedFilter={setsSharedFilter ? setSharedFilter : undefined}
155
206
  setSharedFilterValue={setSharedFilterValue}
156
207
  isDashboard={true}
157
- link={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
208
+ link={
209
+ config.table &&
210
+ config.table.show &&
211
+ config.datasets &&
212
+ visualizationConfig.table &&
213
+ visualizationConfig.table.showDataTableLink
214
+ ? tableLink
215
+ : undefined
216
+ }
158
217
  />
159
218
  )}
160
219
  {visualizationConfig.type === 'data-bite' && (
@@ -177,7 +236,15 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
177
236
  updateChildConfig(col.widget, newConfig)
178
237
  }}
179
238
  isDashboard={true}
180
- configUrl={config.table && config.table.show && config.datasets && visualizationConfig.table && visualizationConfig.table.showDataTableLink ? tableLink : undefined}
239
+ configUrl={
240
+ config.table &&
241
+ config.table.show &&
242
+ config.datasets &&
243
+ visualizationConfig.table &&
244
+ visualizationConfig.table.showDataTableLink
245
+ ? tableLink
246
+ : undefined
247
+ }
181
248
  />
182
249
  )}
183
250
  {visualizationConfig.type === 'markup-include' && (
@@ -226,14 +293,28 @@ const VisualizationRow: React.FC<VizRowProps> = ({ allExpanded, filteredDataOver
226
293
  viewport={currentViewport}
227
294
  />
228
295
  )}
229
- {visualizationConfig.type === 'footnotes' && <FootnotesStandAlone key={col.widget} visualizationKey={col.widget} config={visualizationConfig} viewport={currentViewport} />}
296
+ {visualizationConfig.type === 'footnotes' && (
297
+ <FootnotesStandAlone
298
+ key={col.widget}
299
+ visualizationKey={col.widget}
300
+ config={visualizationConfig}
301
+ viewport={currentViewport}
302
+ />
303
+ )}
230
304
  </VisualizationWrapper>
231
305
  </div>
232
306
  )
233
307
  }
234
308
  return <React.Fragment key={`vis__${index}__${colIndex}`}></React.Fragment>
235
309
  })}
236
- {row.footnotesId ? <FootnotesStandAlone isEditor={false} visualizationKey={row.footnotesId} config={footnotesConfig} viewport={currentViewport} /> : null}
310
+ {row.footnotesId ? (
311
+ <FootnotesStandAlone
312
+ isEditor={false}
313
+ visualizationKey={row.footnotesId}
314
+ config={footnotesConfig}
315
+ viewport={currentViewport}
316
+ />
317
+ ) : null}
237
318
  </div>
238
319
  )
239
320
  }
@@ -18,6 +18,7 @@ const labelHash = {
18
18
  Bar: 'Bar',
19
19
  Line: 'Line',
20
20
  'Spark Line': 'Spark Line',
21
+ 'Bump Chart': 'Bump Chart',
21
22
  Pie: 'Pie',
22
23
  us: 'United States (State- or County-Level)',
23
24
  'us-county': 'United States (State- or County-Level)',
@@ -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
+ }
@@ -2,26 +2,42 @@ import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
2
  import { APIFilterDropdowns, DropdownOptions } from '../components/DashboardFilters'
3
3
  import { APIFilter } from '../types/APIFilter'
4
4
  import { SharedFilter } from '../types/SharedFilter'
5
+ import _ from 'lodash'
6
+ import { getQueryParams } from '@cdc/core/helpers/queryStringUtils'
5
7
 
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
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
+ }
11
24
  return acc
12
25
  }, {})
13
26
 
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
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
17
33
 
18
34
  return _parents.flatMap(filter => {
19
35
  const key = filter.queryParameter || filter.apiFilter.valueSelector || ''
20
36
  const value = filter.queuedActive || filter.active || ''
21
37
  if (Array.isArray(value)) {
22
- return value.map(_value => ({ key, value: _value }))
38
+ return value.map(_value => ({ key, value: _value.toString() }))
23
39
  }
24
- return [{ key, value }]
40
+ return [{ key, value: value.toString() }]
25
41
  })
26
42
  }
27
43
 
@@ -30,22 +46,58 @@ export const getFilterValues = (data: Array<Object>, apiFilter: APIFilter): Drop
30
46
  return data.map(v => ({ text: v[textSelector || valueSelector], value: v[valueSelector] }))
31
47
  }
32
48
 
33
- export const getToFetch = (sharedAPIFilters: SharedFilter[], apiFilterDropdowns: APIFilterDropdowns): Record<string, [string, number]> => {
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]> => {
34
56
  const toFetch = {}
35
- sharedAPIFilters.forEach((filter, index) => {
36
- const baseEndpoint = filter.apiFilter.apiEndpoint
57
+ sharedFilters.forEach((filter, index) => {
58
+ const baseEndpoint = filter.apiFilter?.apiEndpoint
59
+ if (!baseEndpoint) return
37
60
  const _key = baseEndpoint
38
- const isAPIFilter = apiFilterDropdowns[_key]
39
- const parentParams = getParentParams(filter, sharedAPIFilters)
61
+ if (apiFilterDropdowns[_key]) return // don't reload cached filter
62
+ const parentParams = getParentParams(filter, sharedFilters)
40
63
  const notAllParentsSelected = parentParams?.some(({ value }) => value === '')
41
64
 
42
65
  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
66
 
47
67
  const endpoint = baseEndpoint + (parentParams ? gatherQueryParams(baseEndpoint, parentParams) : '')
48
68
  toFetch[endpoint] = [_key, index]
49
69
  })
50
70
  return toFetch
51
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
+ }
@@ -1,23 +1,32 @@
1
1
  import _ from 'lodash'
2
- import { FilterBehavior } from '../components/Header/Header'
2
+ import { FilterBehavior } from '../helpers/FilterBehavior'
3
3
  import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
4
4
  import { SharedFilter } from '../types/SharedFilter'
5
5
  import { DashboardFilters } from '../types/DashboardFilters'
6
6
 
7
7
  const handleChildren = (sharedFilters: SharedFilter[], parentIndex: number) => {
8
8
  const parentKey = sharedFilters[parentIndex].key
9
- const childIndex = sharedFilters.findIndex(filter => filter.parents?.includes(parentKey))
10
- if (childIndex !== -1) {
11
- sharedFilters[childIndex].active = ''
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
+ })
12
16
  }
17
+ return childFilterIndexes
13
18
  }
14
19
 
15
- export const changeFilterActive = (filterIndex: number, value: string | string[], sharedFilters: SharedFilter[], vizConfig: DashboardFilters): SharedFilter[] => {
20
+ export const changeFilterActive = (
21
+ filterIndex: number,
22
+ value: string | string[],
23
+ sharedFilters: SharedFilter[],
24
+ vizConfig: DashboardFilters
25
+ ): [SharedFilter[], number[]] => {
16
26
  const sharedFiltersCopy = _.cloneDeep(sharedFilters)
17
- const currentFilter = sharedFilters[filterIndex]
27
+ const currentFilter = sharedFiltersCopy[filterIndex]
18
28
  if (vizConfig.filterBehavior !== FilterBehavior.Apply || vizConfig.autoLoad) {
19
29
  sharedFiltersCopy[filterIndex].active = value
20
- handleChildren(sharedFiltersCopy, filterIndex)
21
30
  const queryParams = getQueryParams()
22
31
  if (currentFilter.setByQueryParameter && queryParams[currentFilter.setByQueryParameter] !== currentFilter.active) {
23
32
  queryParams[currentFilter.setByQueryParameter] = currentFilter.active
@@ -26,5 +35,5 @@ export const changeFilterActive = (filterIndex: number, value: string | string[]
26
35
  } else {
27
36
  sharedFiltersCopy[filterIndex].queuedActive = value
28
37
  }
29
- return sharedFiltersCopy
38
+ return [sharedFiltersCopy, handleChildren(sharedFiltersCopy, filterIndex)]
30
39
  }
@@ -10,14 +10,14 @@ export const getApplicableFilters = (dashboard: Dashboard, key: string | number)
10
10
  return c?.length > 0 ? c : false
11
11
  }
12
12
 
13
- export const getFilteredData = (state: DashboardState, initialFilteredData = {}, dataOverride?: Object) => {
14
- const newFilteredData = initialFilteredData
13
+ export const getFilteredData = (state: DashboardState, initialFilteredData?: Record<string, any>, dataOverride?: Object) => {
14
+ const newFilteredData = initialFilteredData || {}
15
15
  const { config } = state
16
16
  getVizKeys(config).forEach(key => {
17
17
  const applicableFilters = getApplicableFilters(config.dashboard, key)
18
18
  if (applicableFilters) {
19
19
  const { dataKey, data, dataDescription } = config.visualizations[key]
20
- const _data = state.data[dataKey] || data
20
+ const _data = (dataOverride || state.data)[dataKey] || data
21
21
  const formattedData = dataOverride?.[dataKey] || (dataDescription ? getFormattedData(_data, dataDescription) : _data)
22
22
 
23
23
  newFilteredData[key] = filterData(applicableFilters, formattedData)
@@ -27,7 +27,7 @@ export const getFilteredData = (state: DashboardState, initialFilteredData = {},
27
27
  if (row.dataKey) {
28
28
  const applicableFilters = getApplicableFilters(config.dashboard, index)
29
29
  const { dataKey, data, dataDescription } = row
30
- const _data = state.data[dataKey] || data
30
+ const _data = (dataOverride || state.data)[dataKey] || data
31
31
  if (applicableFilters) {
32
32
  const formattedData = dataOverride?.[dataKey] ?? dataDescription ? getFormattedData(_data, dataDescription) : _data
33
33
 
@@ -5,6 +5,7 @@ export const iconHash = {
5
5
  'data-bite': <Icon display='databite' base />,
6
6
  Bar: <Icon display='chartBar' base />,
7
7
  'Spark Line': <Icon display='chartLine' />,
8
+ 'Bump Chart': <Icon display='chartLine' />,
8
9
  'waffle-chart': <Icon display='grid' base />,
9
10
  'markup-include': <Icon display='code' base />,
10
11
  Line: <Icon display='chartLine' base />,
@@ -14,6 +15,7 @@ export const iconHash = {
14
15
  world: <Icon display='mapWorld' base />,
15
16
  'single-state': <Icon display='mapAl' base />,
16
17
  gear: <Icon display='gear' base />,
18
+ gearMulti: <Icon display='gearMulti' base />,
17
19
  tools: <Icon display='tools' base />,
18
20
  'filtered-text': <Icon display='filtered-text' base />,
19
21
  dashboardFilters: <Icon display='dashboardFilters' base />,
@@ -0,0 +1,74 @@
1
+ import _ from 'lodash'
2
+ import { APIFilterDropdowns } from '../components/DashboardFilters'
3
+ import { SharedFilter } from '../types/SharedFilter'
4
+ import * as apiFilterHelpers from './apiFilterHelpers'
5
+ import { APIFilter } from '../types/APIFilter'
6
+
7
+ export const loadAPIFiltersFactory = (
8
+ dispatch: Function,
9
+ setAPIFilterDropdowns: Function,
10
+ autoLoadFilterIndexes: number[]
11
+ ) => {
12
+ const loadAPIFilters = (
13
+ sharedFilters: SharedFilter[],
14
+ dropdowns: APIFilterDropdowns,
15
+ recursiveLimit = 3
16
+ ): Promise<SharedFilter[]> => {
17
+ if (!sharedFilters) return
18
+ sharedFilters = sharedFilters.map((filter, index) =>
19
+ apiFilterHelpers.setAutoLoadDefaultValue(
20
+ index,
21
+ dropdowns[filter.apiFilter?.apiEndpoint],
22
+ sharedFilters,
23
+ autoLoadFilterIndexes
24
+ )
25
+ )
26
+ const sharedAPIFilters = sharedFilters.filter(f => f.apiFilter)
27
+ const filterLookup = new Map(sharedAPIFilters.map(filter => [filter.apiFilter.apiEndpoint, filter.apiFilter]))
28
+ const toFetch = apiFilterHelpers.getToFetch(sharedFilters, dropdowns)
29
+ const newDropdowns = _.cloneDeep(dropdowns)
30
+ return Promise.all(
31
+ Object.keys(toFetch).map(
32
+ endpoint =>
33
+ new Promise<void>(resolve => {
34
+ fetch(endpoint)
35
+ .then(resp => resp.json())
36
+ .then(data => {
37
+ if (!Array.isArray(data)) {
38
+ console.error('COVE only supports response data in the shape Array<Object>')
39
+ return
40
+ }
41
+ const [_key, index] = toFetch[endpoint]
42
+ const apiFilter = filterLookup.get(_key) as APIFilter
43
+ const _filterValues = apiFilterHelpers.getFilterValues(data, apiFilter)
44
+ newDropdowns[_key] = _filterValues
45
+ const newDefaultSelectedFilter = apiFilterHelpers.setAutoLoadDefaultValue(
46
+ index,
47
+ _filterValues,
48
+ sharedFilters,
49
+ autoLoadFilterIndexes
50
+ )
51
+ sharedFilters[index] = newDefaultSelectedFilter
52
+ })
53
+ .catch(console.error)
54
+ .finally(() => {
55
+ resolve()
56
+ })
57
+ })
58
+ )
59
+ ).then(() => {
60
+ const finishedLoading = sharedFilters.reduce((acc, curr, index) => {
61
+ if (autoLoadFilterIndexes.includes(index) && !curr.active) return false
62
+ return acc
63
+ }, true)
64
+ if (finishedLoading || recursiveLimit === 0) {
65
+ setAPIFilterDropdowns(newDropdowns)
66
+ dispatch({ type: 'SET_SHARED_FILTERS', payload: sharedFilters })
67
+ return sharedFilters
68
+ } else {
69
+ return loadAPIFilters(sharedFilters, newDropdowns, recursiveLimit - 1)
70
+ }
71
+ })
72
+ }
73
+ return loadAPIFilters
74
+ }
@@ -1,10 +1,14 @@
1
1
  import { gatherQueryParams } from '@cdc/core/helpers/gatherQueryParams'
2
2
  import { SharedFilter } from '../types/SharedFilter'
3
3
  import { capitalizeSplitAndJoin } from '@cdc/core/helpers/cove/string'
4
- import { Visualization } from '@cdc/core/types/Visualization'
4
+ import { AnyVisualization, Visualization } from '@cdc/core/types/Visualization'
5
5
  import _ from 'lodash'
6
6
 
7
- export const isUpdateNeeded = (filters: SharedFilter[], currentQueryParams: Record<string, string>, newQueryParams: Record<string, string>): boolean => {
7
+ export const isUpdateNeeded = (
8
+ filters: SharedFilter[],
9
+ currentQueryParams: Record<string, string>,
10
+ newQueryParams: Record<string, string>
11
+ ): boolean => {
8
12
  let needsUpdate = false
9
13
  filters.find(filter => {
10
14
  if (filter.type === 'urlfilter' && !Array.isArray(filter.active) && filter.filterBy === 'File Name') {
@@ -20,15 +24,24 @@ export const isUpdateNeeded = (filters: SharedFilter[], currentQueryParams: Reco
20
24
  return needsUpdate
21
25
  }
22
26
 
23
- export const getDataURL = (updatedQSParams: Record<string, string>, dataUrl: URL, newFileName: string) => {
24
- const _params = Object.keys(updatedQSParams).map(key => ({ key, value: updatedQSParams[key] }))
27
+ export const getDataURL = (updatedQSParams: Record<string, string | string[]>, dataUrl: URL, newFileName: string) => {
28
+ const _params = Object.keys(updatedQSParams).flatMap(key => {
29
+ const value = updatedQSParams[key]
30
+ if (value === undefined) return []
31
+ if (typeof value === 'string' && (value as String).match(/undefined/)) return []
32
+ if (Array.isArray(value)) return value.map(v => ({ key, value: v }))
33
+ return { key, value }
34
+ })
25
35
  const baseURL = dataUrl.origin + dataUrl.pathname
26
36
  let dataUrlFinal = `${baseURL}${gatherQueryParams(baseURL, _params)}`
27
37
 
28
38
  if (newFileName !== '') {
29
39
  const fileExtension = dataUrl.pathname.split('.').pop()
30
40
  const pathWithoutFilename = dataUrl.pathname.substring(0, dataUrl.pathname.lastIndexOf('/'))
31
- dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(baseURL, _params)}`
41
+ dataUrlFinal = `${dataUrl.origin}${pathWithoutFilename}/${newFileName}.${fileExtension}${gatherQueryParams(
42
+ baseURL,
43
+ _params
44
+ )}`
32
45
  }
33
46
  return dataUrlFinal
34
47
  }
@@ -43,7 +56,11 @@ export const getNewFileName = (newFileName: string, filter: SharedFilter, datase
43
56
  if (filter.datasetKey === datasetKey) {
44
57
  if (filter.fileName) {
45
58
  // 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'])
59
+ fileName = capitalizeSplitAndJoin.call(
60
+ String(filter.fileName),
61
+ ' ',
62
+ replacements[filter.whitespaceReplacement ?? 'Keep Spaces']
63
+ )
47
64
  } else {
48
65
  // if no file name is entered use the default active filter. ie. /activeFilter.json
49
66
  fileName = filter.active as string
@@ -51,7 +68,14 @@ export const getNewFileName = (newFileName: string, filter: SharedFilter, datase
51
68
  }
52
69
 
53
70
  if (fileName?.includes('${query}')) {
54
- fileName = fileName.replace('${query}', capitalizeSplitAndJoin.call(String(filter.active), ' ', replacements[filter.whitespaceReplacement ?? 'Keep Spaces']))
71
+ fileName = fileName.replace(
72
+ '${query}',
73
+ capitalizeSplitAndJoin.call(
74
+ String(filter.active),
75
+ ' ',
76
+ replacements[filter.whitespaceReplacement ?? 'Keep Spaces']
77
+ )
78
+ )
55
79
  }
56
80
 
57
81
  return fileName
@@ -66,3 +90,13 @@ export const getVisualizationsWithFormattedData = (visualizations: Record<string
66
90
  return acc
67
91
  }, _.cloneDeep(visualizations))
68
92
  }
93
+
94
+ export const filterUsedByDataUrl = (
95
+ filter: SharedFilter,
96
+ datasetKey: string,
97
+ visualizations: Record<string, AnyVisualization>
98
+ ) => {
99
+ if (!filter.usedBy || !filter.usedBy.length) return true
100
+ const vizUsingFilters = filter.usedBy?.map(vizKey => visualizations[vizKey])
101
+ return vizUsingFilters?.some(viz => viz?.dataKey === datasetKey)
102
+ }