@cdc/dashboard 4.24.2 → 4.24.3

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 (38) hide show
  1. package/dist/cdcdashboard.js +98192 -85200
  2. package/examples/sankey.json +5218 -0
  3. package/index.html +3 -2
  4. package/package.json +11 -10
  5. package/src/CdcDashboard.tsx +124 -124
  6. package/src/CdcDashboardComponent.tsx +173 -186
  7. package/src/DashboardContext.tsx +4 -1
  8. package/src/_stories/Dashboard.stories.tsx +27 -5
  9. package/src/_stories/_mock/pivot-filter.json +163 -0
  10. package/src/_stories/_mock/standalone-table.json +122 -0
  11. package/src/_stories/_mock/toggle-example.json +4035 -0
  12. package/src/components/EditorWrapper/EditorWrapper.tsx +52 -0
  13. package/src/components/EditorWrapper/editor-wrapper.style.css +13 -0
  14. package/src/components/Filters.tsx +88 -0
  15. package/src/components/Header/FilterModal.tsx +480 -0
  16. package/src/components/Header/Header.tsx +25 -465
  17. package/src/components/Row.tsx +28 -17
  18. package/src/components/Toggle/Toggle.tsx +37 -0
  19. package/src/components/Toggle/index.tsx +1 -0
  20. package/src/components/Toggle/toggle-style.css +34 -0
  21. package/src/components/VisualizationsPanel.tsx +13 -3
  22. package/src/components/Widget.tsx +14 -30
  23. package/src/helpers/filterData.ts +72 -49
  24. package/src/helpers/generateValuesForFilter.ts +2 -12
  25. package/src/helpers/getApiFilterKey.ts +5 -0
  26. package/src/helpers/getUpdateConfig.ts +24 -22
  27. package/src/helpers/iconHash.tsx +34 -0
  28. package/src/helpers/tests/filterData.test.ts +149 -0
  29. package/src/images/icon-toggle.svg +1 -0
  30. package/src/scss/grid.scss +1 -1
  31. package/src/scss/main.scss +6 -0
  32. package/src/store/dashboard.actions.ts +19 -2
  33. package/src/store/dashboard.reducer.ts +9 -1
  34. package/src/types/ConfigRow.ts +2 -0
  35. package/src/types/DataSet.ts +7 -7
  36. package/src/types/InitialState.ts +2 -1
  37. package/src/types/SharedFilter.ts +5 -2
  38. package/src/types/Tab.ts +1 -0
@@ -0,0 +1,34 @@
1
+ .cdc-open-viz-module {
2
+ --border: 1px solid var(--lightGray);
3
+ .toggle-component {
4
+ display: flex;
5
+ justify-content: right;
6
+ width: 100%;
7
+ margin-bottom: 15px;
8
+ :first-child:is(div) {
9
+ border: var(--border);
10
+ border-radius: 5px 0 0 5px;
11
+ }
12
+ :last-child:is(div) {
13
+ border: var(--border);
14
+ border-radius: 0 5px 5px 0;
15
+ }
16
+ :is(div) {
17
+ border-top: var(--border);
18
+ border-bottom: var(--border);
19
+ padding: 7px 15px;
20
+ display: inline;
21
+ float: right;
22
+ cursor: pointer;
23
+ &.selected {
24
+ background-color: var(--primary);
25
+ color: white;
26
+ }
27
+ background-color: var(--white);
28
+ color: var(--primary);
29
+ :is(svg) {
30
+ height: 25px;
31
+ }
32
+ }
33
+ }
34
+ }
@@ -2,11 +2,12 @@ import React from 'react'
2
2
  import type { Visualization } from '@cdc/core/types/Visualization'
3
3
  import Widget from './Widget'
4
4
  import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
5
+ import { Table } from '@cdc/core/types/Table'
5
6
 
6
7
  const addVisualization = (type, subType) => {
7
- let modalWillOpen = type !== 'markup-include'
8
- let newVisualizationConfig: Partial<Visualization> = {
9
- newViz: true,
8
+ const modalWillOpen = type !== 'markup-include'
9
+ const newVisualizationConfig: Partial<Visualization> = {
10
+ newViz: type !== 'table',
10
11
  openModal: modalWillOpen,
11
12
  uid: type + Date.now(),
12
13
  type
@@ -23,6 +24,13 @@ const addVisualization = (type, subType) => {
23
24
  case 'data-bite' || 'waffle-chart' || 'markup-include' || 'filtered-text':
24
25
  newVisualizationConfig.visualizationType = type
25
26
  break
27
+ case 'table':
28
+ const tableConfig: Table = { label: 'Data Table', show: true, showDownloadUrl: false, showVertical: true, expanded: true }
29
+ newVisualizationConfig.table = tableConfig
30
+ newVisualizationConfig.columns = {}
31
+ newVisualizationConfig.dataFormat = {}
32
+ newVisualizationConfig.visualizationType = type
33
+ break
26
34
  default:
27
35
  newVisualizationConfig.visualizationType = type
28
36
  break
@@ -39,6 +47,7 @@ const VisualizationsPanel = ({ loadConfig, config }) => (
39
47
  <Widget addVisualization={() => addVisualization('chart', 'Bar')} type='Bar' />
40
48
  <Widget addVisualization={() => addVisualization('chart', 'Line')} type='Line' />
41
49
  <Widget addVisualization={() => addVisualization('chart', 'Pie')} type='Pie' />
50
+ <Widget addVisualization={() => addVisualization('chart', 'Sankey')} type='Sankey' />
42
51
  </div>
43
52
  <span className='subheading-3'>Map</span>
44
53
  <div className='drag-grid'>
@@ -53,6 +62,7 @@ const VisualizationsPanel = ({ loadConfig, config }) => (
53
62
  <Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
54
63
  <Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
55
64
  <Widget addVisualization={() => addVisualization('filter-dropdowns', '')} type='filter-dropdowns' />
65
+ <Widget addVisualization={() => addVisualization('table', '')} type='table' />
56
66
  </div>
57
67
  <span className='subheading-3'>Advanced</span>
58
68
  <AdvancedEditor loadConfig={loadConfig} state={config} convertStateToConfig={undefined} />
@@ -11,24 +11,7 @@ import DataDesigner from '@cdc/core/components/managers/DataDesigner'
11
11
  import Icon from '@cdc/core/components/ui/Icon'
12
12
  import Modal from '@cdc/core/components/ui/Modal'
13
13
  import { Visualization } from '@cdc/core/types/Visualization'
14
-
15
- const iconHash = {
16
- 'data-bite': <Icon display='databite' base />,
17
- Bar: <Icon display='chartBar' base />,
18
- 'Spark Line': <Icon display='chartLine' />,
19
- 'waffle-chart': <Icon display='grid' base />,
20
- 'markup-include': <Icon display='code' base />,
21
- Line: <Icon display='chartLine' base />,
22
- Pie: <Icon display='chartPie' base />,
23
- us: <Icon display='mapUsa' base />,
24
- 'us-county': <Icon display='mapUsa' base />,
25
- world: <Icon display='mapWorld' base />,
26
- 'single-state': <Icon display='mapAl' base />,
27
- gear: <Icon display='gear' base />,
28
- tools: <Icon display='tools' base />,
29
- 'filtered-text': <Icon display='filtered-text' base />,
30
- 'filter-dropdowns': <Icon display='filter-dropdowns' base />
31
- }
14
+ import { iconHash } from '../helpers/iconHash'
32
15
 
33
16
  const labelHash = {
34
17
  'data-bite': 'Data Bite',
@@ -43,7 +26,9 @@ const labelHash = {
43
26
  world: 'World',
44
27
  'single-state': 'U.S. State',
45
28
  'filtered-text': 'Filtered Text',
46
- 'filter-dropdowns': 'Filter Dropdowns'
29
+ 'filter-dropdowns': 'Filter Dropdowns',
30
+ Sankey: 'Sankey Chart',
31
+ table: 'Table'
47
32
  }
48
33
 
49
34
  type WidgetData = Visualization & { rowIdx: number; colIdx: number }
@@ -56,7 +41,6 @@ type WidgetProps = {
56
41
  const Widget = ({ data, addVisualization, type }: WidgetProps) => {
57
42
  const { overlay } = useGlobalContext()
58
43
  const { config } = useContext(DashboardContext)
59
- if (!config) return null
60
44
  const rows = config.rows
61
45
  const visualizations = config.visualizations
62
46
  const dispatch = useContext(DashboardDispatchContext)
@@ -269,16 +253,16 @@ const Widget = ({ data, addVisualization, type }: WidgetProps) => {
269
253
  }
270
254
  }, [data?.openModal])
271
255
 
272
- let isConfigurationReady = false;
273
- if(type === 'markup-include' || type === 'filter-dropdowns'){
274
- isConfigurationReady = true;
275
- } else if(data && data.formattedData) {
276
- isConfigurationReady = true;
277
- } else if(data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]){
278
- let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data);
279
- formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription);
280
- if(formattedDataAttempt){
281
- isConfigurationReady = true;
256
+ let isConfigurationReady = false
257
+ if (type === 'markup-include' || type === 'filter-dropdowns') {
258
+ isConfigurationReady = true
259
+ } else if (data && data.formattedData) {
260
+ isConfigurationReady = true
261
+ } else if (data && data.dataKey && data.dataDescription && config.datasets[data.dataKey]) {
262
+ let formattedDataAttempt = transform.autoStandardize(config.datasets[data.dataKey].data)
263
+ formattedDataAttempt = transform.developerStandardize(formattedDataAttempt, data.dataDescription)
264
+ if (formattedDataAttempt) {
265
+ isConfigurationReady = true
282
266
  }
283
267
  }
284
268
 
@@ -1,73 +1,96 @@
1
+ import _ from 'lodash'
1
2
  import { SharedFilter } from '../types/SharedFilter'
2
- import { generateValuesForFilter } from './generateValuesForFilter'
3
3
 
4
4
  const findFilterTier = (filters: SharedFilter[], sharedFilter: SharedFilter) => {
5
5
  if (!sharedFilter.parents?.length) {
6
6
  return 1
7
7
  } else {
8
- let parent = filters.find(filter => sharedFilter.parents!.includes(filter.key))
8
+ const parent = filters.find(filter => sharedFilter.parents!.includes(filter.key))
9
9
  if (!parent) return 1
10
10
  return 1 + findFilterTier(filters, parent)
11
11
  }
12
12
  }
13
13
 
14
- export const filterData = (filters: SharedFilter[], _data: Object[], filterBehavior) => {
15
- if (_data) {
16
- let maxTier = 1
17
- filters.forEach(sharedFilter => {
18
- sharedFilter.tier = findFilterTier(filters, sharedFilter)
19
- })
14
+ function getMaxTierAndSetFilterTiers(filters: SharedFilter[]): number {
15
+ let maxTier = 1
16
+ filters.forEach(sharedFilter => {
17
+ sharedFilter.tier = findFilterTier(filters, sharedFilter)
18
+ if (sharedFilter.tier > maxTier) {
19
+ maxTier = sharedFilter.tier
20
+ }
21
+ })
22
+ return maxTier
23
+ }
20
24
 
21
- filters.forEach(sharedFilter => {
22
- if (sharedFilter.tier && sharedFilter.tier > maxTier) {
23
- maxTier = sharedFilter.tier
25
+ function filter(data, filters, condition) {
26
+ return data ? data.filter(row => {
27
+ const found = filters.find(filter => {
28
+ if (filter.pivot) return false
29
+ const currentValue = row[filter.columnName]
30
+ const selectedValue = filter.queuedActive || filter.active
31
+ const isNotTheSelectedValue = selectedValue && currentValue != selectedValue
32
+ const isFirstOccurrenceOfTier = filter.tier === condition
33
+ if (filter.type !== 'urlfilter' && isFirstOccurrenceOfTier && isNotTheSelectedValue) {
34
+ return true
24
35
  }
25
36
  })
37
+ return !found
38
+ }) : []
39
+ }
26
40
 
27
- let filteredData = _data
28
- // TODO triple loop??
29
- for (let i = 0; i < maxTier; i++) {
30
- let filteredDataSubTier: any[] = []
31
-
32
- filteredData.forEach(row => {
33
- let add = true
34
-
35
- filters.forEach(filter => {
36
- // eslint-disable-next-line eqeqeq
37
- if (filter.type !== 'urlfilter' && ((!filter.tier && i === 0) || filter.tier === i + 1) && (filter.queuedActive || filter.active) && row[filter.columnName!] != (filter.queuedActive || filter.active)) {
38
- add = false
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
+ }
52
+ })
53
+ }
41
54
 
42
- if (add) filteredDataSubTier.push(row)
43
- })
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]
44
64
 
45
- filters.forEach(sharedFilter => {
46
- if (sharedFilter.tier && sharedFilter.tier === i + 2) {
47
- sharedFilter.values = generateValuesForFilter(sharedFilter.columnName, { data: filteredDataSubTier }, filterBehavior)
48
- if (sharedFilter.values.length > 0 && (!sharedFilter.active || sharedFilter.values.indexOf(sharedFilter.active) === -1)) {
49
- sharedFilter.active = sharedFilter.values[0]
50
- }
51
- }
52
- })
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] = { ...row, ...toAdd }
70
+ })
71
+ }
72
+ return newData
73
+ }
53
74
 
54
- filteredData = filteredDataSubTier
55
- }
75
+ /** This function returns filtered data.
76
+ * It also manipulates the filters by adding: tiers, filterOptions, and default selections */
77
+ export const filterData = (filters: SharedFilter[], _data: Object[]): Object[] => {
78
+ const maxTier = getMaxTierAndSetFilterTiers(filters)
56
79
 
57
- let filteredDataSubTier: any[] = []
58
- filteredData.forEach(row => {
59
- let add = true
80
+ for (let i = 0; i < maxTier; i++) {
81
+ const lastIteration = i === maxTier - 1
60
82
 
61
- filters.forEach(filter => {
62
- // eslint-disable-next-line eqeqeq
63
- if (filter.type !== 'urlfilter' && filter.tier && filter.tier === maxTier - 1 && (filter.queuedActive || filter.active) && row[filter.columnName!] != (filter.queuedActive || filter.active)) {
64
- add = false
65
- }
66
- })
83
+ const filteredData = filter(_data, filters, i + 1)
67
84
 
68
- if (add) filteredDataSubTier.push(row)
69
- })
85
+ setFilterValuesAndActiveFilter(filters, filteredData, i)
70
86
 
71
- return filteredDataSubTier
87
+ if (lastIteration) {
88
+ const pivotFilter = filters.find(filter => filter.pivot)
89
+ if (pivotFilter) {
90
+ return pivotData(filteredData, pivotFilter)
91
+ }
92
+ // not sure if this last run of filter() function is necessary.
93
+ return filter(filteredData, filters, maxTier - 1)
94
+ }
72
95
  }
73
96
  }
@@ -1,21 +1,11 @@
1
- import { FilterBehavior } from '../components/Header/Header'
2
-
3
- // Gets filter values from API response
4
- export const generateValuesForAPIFilter = (columnName, _data): string[] => {
5
- type Row = { [key: string]: any }
6
- return Object.values(_data)
7
- .filter(row => row && !!(row as Row)[columnName])
8
- .map(row => (row as Row)[columnName])
9
- }
10
-
11
1
  // Gets filter values from dataset
12
- export const generateValuesForFilter = (columnName, _data, filterBehavior) => {
2
+ export const generateValuesForFilter = (columnName, _data) => {
13
3
  const values: string[] = []
14
4
 
15
5
  Object.keys(_data).forEach(key => {
16
6
  _data[key]?.forEach(row => {
17
7
  const value = row[columnName]
18
- if (value && false === values.includes(value)) {
8
+ if (!values.includes(value)) {
19
9
  values.push(value)
20
10
  }
21
11
  })
@@ -0,0 +1,5 @@
1
+ import { APIFilter } from '../types/APIFilter'
2
+
3
+ export const getApiFilterKey = ({ apiEndpoint, heirarchyLookup }: APIFilter) => {
4
+ return apiEndpoint + (heirarchyLookup || '')
5
+ }
@@ -5,6 +5,8 @@ import { generateValuesForFilter } from './generateValuesForFilter'
5
5
  import { getFormattedData } from './getFormattedData'
6
6
  import { getVizKeys } from './getVizKeys'
7
7
 
8
+ import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
9
+
8
10
  type UpdateState = Omit<DashboardState, 'config'> & {
9
11
  config?: DashboardConfig
10
12
  }
@@ -14,17 +16,28 @@ export const getUpdateConfig =
14
16
  (newConfig, dataOverride?: Object): [Config, Object] => {
15
17
  let newFilteredData = {}
16
18
  let visualizationKeys = getVizKeys(newConfig)
17
- const setFilter = (filterIndex: number, key: string, value: any) => {
18
- newConfig.dashboard.sharedFilters[filterIndex][key] = value
19
- }
19
+
20
20
  if (newConfig.dashboard.sharedFilters) {
21
21
  newConfig.dashboard.sharedFilters.forEach((filter, i) => {
22
22
  const filterIsSetByVizData = !!visualizationKeys.find(key => key === filter.setBy)
23
- let _filter = newConfig.dashboard.sharedFilters[i]
23
+ const _filter = newConfig.dashboard.sharedFilters[i]
24
24
 
25
- if (filterIsSetByVizData) {
26
- const filterValues = generateValuesForFilter(filter.columnName, dataOverride || state.data, state.config?.filterBehavior)
25
+ const setValuesAndActive = filterValues => {
26
+ _filter.values = filterValues
27
+ if (filterValues.length > 0) {
28
+ const defaultValues = _filter.pivot ? _filter.values : _filter.values[0]
29
+
30
+ const queryStringFilterValue = getQueryStringFilterValue(_filter)
31
+ if(queryStringFilterValue){
32
+ _filter.active = queryStringFilterValue
33
+ } else {
34
+ _filter.active = _filter.active || defaultValues
35
+ }
36
+ }
37
+ }
27
38
 
39
+ const filterValues = generateValuesForFilter(filter.columnName, dataOverride || state.data)
40
+ if (filterIsSetByVizData) {
28
41
  if (_filter.order === 'asc') {
29
42
  filterValues.sort()
30
43
  }
@@ -32,32 +45,21 @@ export const getUpdateConfig =
32
45
  filterValues.sort().reverse()
33
46
  }
34
47
 
35
- setFilter(i, 'values', filterValues)
36
- _filter = newConfig.dashboard.sharedFilters[i]
37
- if (filterValues.length > 0) {
38
- setFilter(i, 'active', _filter.active || _filter.values[0])
39
- }
40
- }
41
-
42
- if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
43
- const generatedValues = generateValuesForFilter(filter.columnName, dataOverride || state.data, state.config?.filterBehavior)
44
- setFilter(i, 'values', generatedValues)
45
- const _filter = newConfig.dashboard.sharedFilters[i]
46
- if (_filter.values.length > 0) {
47
- setFilter(i, 'active', filter.active || _filter.values[0])
48
- }
48
+ setValuesAndActive(filterValues)
49
+ } else if ((!filter.values || filter.values.length === 0) && filter.showDropdown) {
50
+ setValuesAndActive(filterValues)
49
51
  }
50
52
  })
51
53
 
52
54
  visualizationKeys.forEach(visualizationKey => {
53
- let applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
55
+ const applicableFilters = newConfig.dashboard.sharedFilters.filter(sharedFilter => sharedFilter.usedBy && sharedFilter.usedBy.indexOf(visualizationKey) !== -1)
54
56
 
55
57
  if (applicableFilters.length > 0) {
56
58
  const visualization = newConfig.visualizations[visualizationKey]
57
59
  const _newConfigDataSet = newConfig.datasets[visualization.dataKey]
58
60
  const formattedData = getFormattedData(_newConfigDataSet?.data || visualization.data, visualization.dataDescription)
59
61
  const _data = formattedData || (dataOverride || state.data)[visualization.dataKey]
60
- newFilteredData[visualizationKey] = filterData(applicableFilters, _data, state.config?.filterBehavior)
62
+ newFilteredData[visualizationKey] = filterData(applicableFilters, _data)
61
63
  }
62
64
  })
63
65
  }
@@ -0,0 +1,34 @@
1
+ import Icon from '@cdc/core/components/ui/Icon'
2
+ import { Visualization } from '@cdc/core/types/Visualization'
3
+
4
+ export const iconHash = {
5
+ 'data-bite': <Icon display='databite' base />,
6
+ Bar: <Icon display='chartBar' base />,
7
+ 'Spark Line': <Icon display='chartLine' />,
8
+ 'waffle-chart': <Icon display='grid' base />,
9
+ 'markup-include': <Icon display='code' base />,
10
+ Line: <Icon display='chartLine' base />,
11
+ Pie: <Icon display='chartPie' base />,
12
+ us: <Icon display='mapUsa' base />,
13
+ 'us-county': <Icon display='mapUsa' base />,
14
+ world: <Icon display='mapWorld' base />,
15
+ 'single-state': <Icon display='mapAl' base />,
16
+ gear: <Icon display='gear' base />,
17
+ tools: <Icon display='tools' base />,
18
+ 'filtered-text': <Icon display='filtered-text' base />,
19
+ 'filter-dropdowns': <Icon display='filter-dropdowns' base />,
20
+ table: <Icon display='table' base />,
21
+ Sankey: <Icon display='sankey' base />
22
+ }
23
+
24
+ export const getIcon = (visualization: Visualization) => {
25
+ const { type, visualizationType, general } = visualization
26
+ if (visualizationType) return iconHash[visualizationType]
27
+ if (general?.geoType) {
28
+ // for visualizations, mismatching state and state icon is not desired
29
+ // so instead of showing alabama as the default state icon we show the US icon.
30
+ if (general.geoType === 'single-state') return iconHash['us']
31
+ return iconHash[general.geoType]
32
+ }
33
+ return iconHash[type]
34
+ }
@@ -0,0 +1,149 @@
1
+ import { SharedFilter } from '../../types/SharedFilter'
2
+ import { filterData } from '../filterData'
3
+
4
+ describe('filterData', () => {
5
+ it('should filter data based on the provided filters', () => {
6
+ const filters = [
7
+ { tier: 1, columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'abc' },
8
+ { tier: 2, columnName: 'age', active: 30, queuedActive: 30, fileName: 'abc', key: 'abc' }
9
+ ] as SharedFilter[]
10
+ const data = [
11
+ { name: 'John', age: 30 },
12
+ { name: 'Jane', age: 25 },
13
+ { name: 'John', age: 35 },
14
+ { name: 'Jane', age: 30 }
15
+ ]
16
+
17
+ const result = filterData(filters, data)
18
+
19
+ expect(result).toEqual([{ name: 'John', age: 30 }])
20
+ })
21
+
22
+ it('filters with parents', () => {
23
+ const filters = [
24
+ { columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'abc' },
25
+ { columnName: 'age', active: 30, queuedActive: 30, fileName: 'abc', key: 'abc', parents: ['name'] }
26
+ ] as SharedFilter[]
27
+ const data = [
28
+ { name: 'John', age: 30 },
29
+ { name: 'Jane', age: 25 },
30
+ { name: 'John', age: 35 },
31
+ { name: 'Jane', age: 30 }
32
+ ]
33
+
34
+ const result = filterData(filters, data)
35
+
36
+ expect(result).toEqual([{ name: 'John', age: 30 }])
37
+ })
38
+
39
+ it('causes sideEffects to filters', () => {
40
+ // the side effect is not desired, but current functionality depends on the sideEffect.
41
+ // hopefully the side effect will be refactored in the future to be a returned value.
42
+ const filters = [
43
+ { columnName: 'name', active: 'John', queuedActive: 'John', fileName: 'abc', key: 'name' },
44
+ { columnName: 'age', fileName: 'abc', key: 'age' },
45
+ { columnName: 'color', fileName: 'abc', key: 'color', parents: ['age'] }
46
+ ] as SharedFilter[]
47
+ const data = [
48
+ { name: 'John', age: 30, color: 'blue' },
49
+ { name: 'Jane', age: 25, color: 'red' },
50
+ { name: 'John', age: 35, color: 'yellow' },
51
+ { name: 'Jane', age: 30, color: 'green' }
52
+ ]
53
+
54
+ const result = filterData(filters, data)
55
+
56
+ expect(result).toEqual([{ name: 'John', age: 30, color: 'blue' }])
57
+
58
+ const sideEffectOfFiltering = [
59
+ {
60
+ columnName: 'name',
61
+ active: 'John',
62
+ queuedActive: 'John',
63
+ fileName: 'abc',
64
+ key: 'name',
65
+ tier: 1
66
+ },
67
+ { columnName: 'age', fileName: 'abc', key: 'age', tier: 1 },
68
+ {
69
+ columnName: 'color',
70
+ fileName: 'abc',
71
+ key: 'color',
72
+ parents: ['age'],
73
+ tier: 2,
74
+ values: ['blue', 'yellow'],
75
+ active: 'blue'
76
+ }
77
+ ]
78
+ expect(filters).toEqual(sideEffectOfFiltering)
79
+ })
80
+
81
+ it('should not include data that does not meet the filter criteria', () => {
82
+ const filters = [
83
+ //{ columnName: 'apple', fileName: 'abc', key: 'banana' },
84
+ { columnName: 'color', active: 'red', queuedActive: 'red', fileName: 'abc', key: 'color' },
85
+ { columnName: 'name', fileName: 'abc', key: 'name' },
86
+ { columnName: 'age', fileName: 'abc', key: 'age', parents: ['name'] }
87
+ ] as SharedFilter[]
88
+ const data = [
89
+ { name: 'Jane', age: 30, color: 'blue' },
90
+ { name: 'John', age: 25, color: 'red' },
91
+ { name: 'John', age: 25, color: 'green' }
92
+ //{ name: 'John', age: 25, color: 'red', apple: 'banana' }
93
+ ]
94
+
95
+ const result = filterData(filters, data)
96
+ expect(result).toEqual([{ name: 'John', age: 25, color: 'red' }])
97
+ })
98
+
99
+ it('should pivot data based on the provided filters', () => {
100
+ const filters = [{ key: 'Race', type: 'datafilter', showDropdown: true, columnName: 'Race', pivot: 'Age-adjusted rate', usedBy: ['table1707935263149'] }] as SharedFilter[]
101
+ const data = [
102
+ {
103
+ Race: 'Hispanic or Latino',
104
+ 'Age-adjusted rate': '644.2',
105
+ Year: '2016'
106
+ },
107
+ {
108
+ Race: 'Non-Hispanic American Indian',
109
+ 'Age-adjusted rate': '636.1',
110
+ Year: '2016'
111
+ },
112
+ {
113
+ Race: 'Non-Hispanic Black',
114
+ 'Age-adjusted rate': '563.7',
115
+ Year: '2016'
116
+ },
117
+ {
118
+ Race: 'Hispanic or Latino',
119
+ 'Age-adjusted rate': '644.2',
120
+ Year: '2017'
121
+ },
122
+ {
123
+ Race: 'Non-Hispanic American Indian',
124
+ 'Age-adjusted rate': '636.1',
125
+ Year: '2017'
126
+ },
127
+ {
128
+ Race: 'Non-Hispanic Black',
129
+ 'Age-adjusted rate': '563.7',
130
+ Year: '2017'
131
+ }
132
+ ]
133
+
134
+ expect(filterData(filters, data)).toEqual([
135
+ {
136
+ 'Hispanic or Latino': '644.2',
137
+ 'Non-Hispanic American Indian': '636.1',
138
+ 'Non-Hispanic Black': '563.7',
139
+ Year: '2016'
140
+ },
141
+ {
142
+ 'Hispanic or Latino': '644.2',
143
+ 'Non-Hispanic American Indian': '636.1',
144
+ 'Non-Hispanic Black': '563.7',
145
+ Year: '2017'
146
+ }
147
+ ])
148
+ })
149
+ })
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M192 64C86 64 0 150 0 256S86 448 192 448H384c106 0 192-86 192-192s-86-192-192-192H192zm192 96a96 96 0 1 1 0 192 96 96 0 1 1 0-192z"/></svg>
@@ -77,7 +77,7 @@ $red: #f74242;
77
77
 
78
78
  .row-menu__btn:hover .row-menu__flyout {
79
79
  transition: width 0.2s cubic-bezier(0.16, 1, 0.3, 1);
80
- width: 144px;
80
+ width: 180px;
81
81
 
82
82
  li {
83
83
  display: flex;
@@ -187,6 +187,9 @@
187
187
  .dashboard-row {
188
188
  display: flex;
189
189
  flex-direction: column;
190
+ &.toggle {
191
+ display: block;
192
+ }
190
193
  }
191
194
 
192
195
  .dashboard-col {
@@ -197,6 +200,9 @@
197
200
  margin-left: 0;
198
201
  margin-right: 0;
199
202
  }
203
+ &.hidden-toggle {
204
+ display: none;
205
+ }
200
206
  }
201
207
 
202
208
  .dashboard-col-12 {