@cdc/core 4.24.5 → 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.
- package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
- package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
- package/components/AdvancedEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +21 -2
- package/components/DataTable/DataTableStandAlone.tsx +4 -25
- package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
- package/components/DataTable/components/ExpandCollapse.tsx +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +3 -9
- package/components/DataTable/helpers/getChartCellValue.ts +8 -4
- package/components/DataTable/helpers/getDataSeriesColumns.ts +8 -5
- package/components/DataTable/helpers/getRowType.ts +6 -0
- package/components/DataTable/types/TableConfig.ts +1 -0
- package/components/EditorPanel/ColumnsEditor.tsx +3 -30
- package/components/EditorPanel/DataTableEditor.tsx +66 -22
- package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
- package/components/EditorPanel/FootnotesEditor.tsx +77 -0
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +227 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +54 -0
- package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
- package/components/EditorWrapper/EditorWrapper.tsx +3 -4
- package/components/EditorWrapper/index.ts +1 -0
- package/components/{Filters.jsx → Filters.tsx} +40 -24
- package/components/Footnotes/Footnotes.tsx +25 -0
- package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
- package/components/Footnotes/footnotes.css +5 -0
- package/components/Footnotes/index.ts +1 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +8 -4
- package/components/Layout/components/Visualization/index.tsx +12 -5
- package/components/MultiSelect/MultiSelect.tsx +36 -9
- package/components/MultiSelect/multiselect.styles.css +0 -3
- package/components/_stories/Footnotes.stories.tsx +17 -0
- package/components/_stories/styles.scss +1 -0
- package/components/inputs/InputSelect.tsx +17 -6
- package/components/ui/Icon.tsx +1 -2
- package/helpers/addValuesToFilters.ts +56 -0
- package/helpers/cove/accessibility.ts +1 -0
- package/helpers/cove/fontSettings.ts +2 -0
- package/helpers/coveUpdateWorker.ts +7 -0
- package/helpers/filterVizData.ts +30 -0
- package/helpers/formatConfigBeforeSave.ts +90 -0
- package/helpers/gatherQueryParams.ts +14 -7
- package/helpers/lineChartHelpers.js +2 -1
- package/helpers/pivotData.ts +18 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/tests/updateFieldFactory.test.ts +1 -0
- package/helpers/updateFieldFactory.ts +1 -1
- package/helpers/ver/4.24.7.ts +92 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +6 -1
- package/styles/_data-table.scss +0 -1
- package/styles/base.scss +4 -0
- package/styles/v2/themes/_color-definitions.scss +1 -0
- package/types/Annotation.ts +46 -0
- package/types/Axis.ts +0 -2
- package/types/ConfigureData.ts +1 -1
- package/types/Footnotes.ts +17 -0
- package/types/General.ts +5 -0
- package/types/Runtime.ts +2 -7
- package/types/Table.ts +6 -0
- package/types/Visualization.ts +31 -9
- package/types/VizFilter.ts +16 -5
- package/LICENSE +0 -201
- package/components/AdvancedEditor.jsx +0 -74
- package/components/EditorPanel/VizFilterEditor.tsx +0 -234
- package/helpers/queryStringUtils.js +0 -26
- package/types/BaseVisualizationType.ts +0 -1
|
@@ -601,7 +601,7 @@
|
|
|
601
601
|
/* clicking anywhere will focus the input */
|
|
602
602
|
cursor: text;
|
|
603
603
|
|
|
604
|
-
span {
|
|
604
|
+
span:not(.cove-tooltip, .cove-icon) {
|
|
605
605
|
display: inline;
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -723,6 +723,10 @@
|
|
|
723
723
|
margin-right: 5px;
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
.cove-tooltip {
|
|
727
|
+
position: relative;
|
|
728
|
+
}
|
|
729
|
+
|
|
726
730
|
// tooltips
|
|
727
731
|
.cove-label + .cove-tooltip {
|
|
728
732
|
top: 1px;
|
|
@@ -731,9 +735,9 @@
|
|
|
731
735
|
}
|
|
732
736
|
|
|
733
737
|
.cove-accordion__button .cove-tooltip {
|
|
734
|
-
display: inline-flex;
|
|
735
|
-
right: 1.5rem;
|
|
736
|
-
line-height: inherit;
|
|
738
|
+
// display: inline-flex;
|
|
739
|
+
// right: 1.5rem;
|
|
740
|
+
// line-height: inherit;
|
|
737
741
|
}
|
|
738
742
|
|
|
739
743
|
.cove-list-group__item .cove-tooltip {
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
// main visualization wrapper
|
|
2
2
|
import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
3
|
-
import React, { forwardRef
|
|
3
|
+
import React, { forwardRef } from 'react'
|
|
4
4
|
import { Config as DataBiteConfig } from '@cdc/data-bite/src/types/Config'
|
|
5
5
|
import './visualizations.scss'
|
|
6
6
|
import { Config as WaffleChartConfig } from '@cdc/waffle-chart/src/types/Config'
|
|
7
7
|
import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
|
|
8
|
+
import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
|
|
8
9
|
|
|
9
10
|
type VisualizationWrapper = {
|
|
10
11
|
children: React.ReactNode
|
|
11
|
-
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig
|
|
12
|
-
currentViewport
|
|
13
|
-
imageId
|
|
12
|
+
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters
|
|
13
|
+
currentViewport?: string
|
|
14
|
+
imageId?: string
|
|
14
15
|
isEditor: boolean
|
|
15
|
-
showEditorPanel
|
|
16
|
+
showEditorPanel?: boolean
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) => {
|
|
@@ -33,6 +34,12 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
|
|
|
33
34
|
classes.push('editor-panel--hidden')
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
if (config.type === 'filtered-text') {
|
|
38
|
+
classes.push('type-filtered-text')
|
|
39
|
+
classes = classes.filter(item => item !== 'cove-component__content')
|
|
40
|
+
return classes
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
if (config.type === 'chart') {
|
|
37
44
|
classes.push('type-chart')
|
|
38
45
|
config?.visualizationType === 'Spark Line' && classes.push(`type-sparkline`)
|
|
@@ -21,8 +21,8 @@ interface MultiSelectProps {
|
|
|
21
21
|
limit?: number
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected, limit }) => {
|
|
25
|
-
const preselectedItems = options.filter(opt => selected
|
|
24
|
+
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected = [], limit }) => {
|
|
25
|
+
const preselectedItems = options.filter(opt => selected.includes(opt.value)).slice(0, limit)
|
|
26
26
|
const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
|
|
27
27
|
const [expanded, setExpanded] = useState(false)
|
|
28
28
|
const multiSelectRef = useRef(null)
|
|
@@ -68,22 +68,39 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
|
|
|
68
68
|
return (
|
|
69
69
|
<div ref={multiSelectRef} className='cove-multiselect'>
|
|
70
70
|
{label && (
|
|
71
|
-
<
|
|
71
|
+
<span id={multiID} className='edit-label column-heading'>
|
|
72
72
|
{label}
|
|
73
|
-
</
|
|
73
|
+
</span>
|
|
74
74
|
)}
|
|
75
75
|
|
|
76
76
|
<div className='wrapper'>
|
|
77
77
|
<div className='selected'>
|
|
78
78
|
{selectedItems.map(item => (
|
|
79
|
-
<div key={item.value} aria-labelledby={label ? multiID : undefined}
|
|
79
|
+
<div key={item.value} aria-labelledby={label ? multiID : undefined}>
|
|
80
80
|
{item.label}
|
|
81
|
-
<button
|
|
81
|
+
<button
|
|
82
|
+
aria-label='Remove'
|
|
83
|
+
onClick={e => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
handleItemRemove(item)
|
|
86
|
+
}}
|
|
87
|
+
onKeyUp={e => {
|
|
88
|
+
handleItemRemove(item, e)
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
82
91
|
x
|
|
83
92
|
</button>
|
|
84
93
|
</div>
|
|
85
94
|
))}
|
|
86
|
-
<button
|
|
95
|
+
<button
|
|
96
|
+
aria-label={expanded ? 'Collapse' : 'Expand'}
|
|
97
|
+
aria-labelledby={label ? multiID : undefined}
|
|
98
|
+
className='expand'
|
|
99
|
+
onClick={e => {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
setExpanded(!expanded)
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
87
104
|
<Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
|
|
88
105
|
</button>
|
|
89
106
|
</div>
|
|
@@ -98,11 +115,21 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
|
|
|
98
115
|
</Tooltip>
|
|
99
116
|
)}
|
|
100
117
|
</div>
|
|
101
|
-
<ul className={'dropdown' + (expanded ? '' : '
|
|
118
|
+
<ul className={'dropdown' + (expanded ? '' : ' d-none')}>
|
|
102
119
|
{options
|
|
103
120
|
.filter(option => !selectedItems.find(item => item.value === option.value))
|
|
104
121
|
.map(option => (
|
|
105
|
-
<li
|
|
122
|
+
<li
|
|
123
|
+
className='cove-multiselect-li'
|
|
124
|
+
key={option.value}
|
|
125
|
+
role='option'
|
|
126
|
+
tabIndex={0}
|
|
127
|
+
onClick={e => {
|
|
128
|
+
e.preventDefault()
|
|
129
|
+
handleItemSelect(option, e)
|
|
130
|
+
}}
|
|
131
|
+
onKeyUp={e => handleItemSelect(option, e)}
|
|
132
|
+
>
|
|
106
133
|
{option.label}
|
|
107
134
|
</li>
|
|
108
135
|
))}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import Footnotes from '../Footnotes'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Footnotes> = {
|
|
5
|
+
title: 'Components/Organisms/Footnotes',
|
|
6
|
+
component: Footnotes
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default meta
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Footnotes>
|
|
12
|
+
|
|
13
|
+
export const Primary: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
footnotes: [{ symbol: '*', text: 'This is a footnote' }, { symbol: '†', text: 'This is another footnote' }, { text: 'This is a third footnote' }]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -3,7 +3,7 @@ import '../../styles/v2/components/input/index.scss'
|
|
|
3
3
|
interface InputProps {
|
|
4
4
|
label?
|
|
5
5
|
value?
|
|
6
|
-
options: string[] |
|
|
6
|
+
options: string[] | Record<string, any> | [any, string][]
|
|
7
7
|
fieldName
|
|
8
8
|
section?
|
|
9
9
|
subsection?
|
|
@@ -17,11 +17,22 @@ const InputSelect = ({ label, value, options, fieldName, section = null, subsect
|
|
|
17
17
|
|
|
18
18
|
if (Array.isArray(options)) {
|
|
19
19
|
//Handle basic array
|
|
20
|
-
optionsJsx = options.map(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
optionsJsx = options.map(option => {
|
|
21
|
+
if (typeof option === 'string') {
|
|
22
|
+
return (
|
|
23
|
+
<option value={option} key={option}>
|
|
24
|
+
{option}
|
|
25
|
+
</option>
|
|
26
|
+
)
|
|
27
|
+
} else {
|
|
28
|
+
const [value, name] = option
|
|
29
|
+
return (
|
|
30
|
+
<option value={value} key={name}>
|
|
31
|
+
{name}
|
|
32
|
+
</option>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
25
36
|
} else {
|
|
26
37
|
//Handle object with value/name pairs
|
|
27
38
|
optionsJsx = []
|
package/components/ui/Icon.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
1
|
import PropTypes from 'prop-types'
|
|
3
2
|
|
|
4
3
|
import iconCaretUp from '../../assets/icon-caret-up.svg'
|
|
@@ -68,7 +67,7 @@ const iconHash = {
|
|
|
68
67
|
plus: iconPlus,
|
|
69
68
|
minus: iconMinus,
|
|
70
69
|
'filtered-text': iconText,
|
|
71
|
-
|
|
70
|
+
dashboardFilters: iconDropdowns,
|
|
72
71
|
table: iconTable,
|
|
73
72
|
sankey: iconSankey,
|
|
74
73
|
rotateLeft: iconRotateLeft,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
|
+
|
|
4
|
+
type Filter = {
|
|
5
|
+
columnName: string
|
|
6
|
+
values: string[]
|
|
7
|
+
filterStyle?: string
|
|
8
|
+
active?: string | string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Gets filter values from dataset
|
|
12
|
+
const generateValuesForFilter = (columnName, data: any[] | Record<string, any[]>) => {
|
|
13
|
+
const values: string[] = []
|
|
14
|
+
|
|
15
|
+
if (Array.isArray(data)) {
|
|
16
|
+
data.forEach(row => {
|
|
17
|
+
const value = row[columnName]
|
|
18
|
+
if (!values.includes(value)) {
|
|
19
|
+
values.push(value)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
} else {
|
|
23
|
+
// data is a dataset this loops through ALL datasets to find matching values
|
|
24
|
+
// not sure if this is desired behavior
|
|
25
|
+
Object.values(data).forEach((rows: any[]) => {
|
|
26
|
+
rows.forEach(row => {
|
|
27
|
+
const value = row[columnName]
|
|
28
|
+
if (!values.includes(value)) {
|
|
29
|
+
values.push(value)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return values
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const addValuesToFilters = <T>(filters: Filter[], data: any[] | Record<string, any[]>): Array<T> => {
|
|
39
|
+
return filters?.map(filter => {
|
|
40
|
+
const filterCopy = _.cloneDeep(filter)
|
|
41
|
+
|
|
42
|
+
const filterValues = generateValuesForFilter(filter.columnName, data)
|
|
43
|
+
filterCopy.values = filterValues
|
|
44
|
+
if (filterValues.length > 0) {
|
|
45
|
+
const defaultValues = filterCopy.filterStyle === 'multi-select' ? filterCopy.values : filterCopy.values[0]
|
|
46
|
+
|
|
47
|
+
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
48
|
+
if (queryStringFilterValue) {
|
|
49
|
+
filterCopy.active = queryStringFilterValue
|
|
50
|
+
} else {
|
|
51
|
+
filterCopy.active = filterCopy.active || defaultValues
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return filterCopy
|
|
55
|
+
}) as Array<T>
|
|
56
|
+
}
|
|
@@ -9,6 +9,7 @@ import chroma from 'chroma-js'
|
|
|
9
9
|
export const WCAG_CONTRAST_RATIO = 4.5
|
|
10
10
|
|
|
11
11
|
export const getContrastColor = (textColor: string, bgColor: string) => {
|
|
12
|
+
if (!bgColor) return
|
|
12
13
|
if (chroma.contrast(textColor, bgColor) < WCAG_CONTRAST_RATIO) {
|
|
13
14
|
switch (textColor) {
|
|
14
15
|
case '#FFF':
|
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
import update_4_24_4 from './ver/4.23.4'
|
|
4
4
|
import update_4_24_3 from './ver/4.24.3'
|
|
5
5
|
import update_4_24_5 from './ver/4.24.5'
|
|
6
|
+
import update_4_24_7 from './ver/4.24.7'
|
|
6
7
|
|
|
7
8
|
export const coveUpdateWorker = config => {
|
|
9
|
+
if (config.multiDashboards) {
|
|
10
|
+
config.multiDashboards.forEach((dashboard, index) => {
|
|
11
|
+
config.multiDashboards[index] = coveUpdateWorker(dashboard)
|
|
12
|
+
})
|
|
13
|
+
}
|
|
8
14
|
let genConfig = config
|
|
9
15
|
|
|
10
16
|
genConfig = update_4_24_3(genConfig)
|
|
11
17
|
genConfig = update_4_24_4(genConfig)
|
|
12
18
|
genConfig = update_4_24_5(genConfig)
|
|
19
|
+
genConfig = update_4_24_7(genConfig)
|
|
13
20
|
|
|
14
21
|
return genConfig
|
|
15
22
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const filterVizData = (filters, data) => {
|
|
2
|
+
if (!data) {
|
|
3
|
+
console.warn('COVE: No data to filter')
|
|
4
|
+
return []
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (!filters) return data
|
|
8
|
+
const filteredData: any[] = []
|
|
9
|
+
|
|
10
|
+
data?.forEach(row => {
|
|
11
|
+
let add = true
|
|
12
|
+
filters
|
|
13
|
+
.filter(filter => filter.type !== 'url')
|
|
14
|
+
.forEach(filter => {
|
|
15
|
+
const value = row[filter.columnName]
|
|
16
|
+
if (filter.active === undefined) return
|
|
17
|
+
if (Array.isArray(filter.active)) {
|
|
18
|
+
if (!filter.active.includes(value)) {
|
|
19
|
+
add = false
|
|
20
|
+
}
|
|
21
|
+
} else if (value != filter.active) {
|
|
22
|
+
add = false
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (add) filteredData.push(row)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return filteredData
|
|
30
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import Footnotes from '@cdc/core/types/Footnotes'
|
|
2
|
+
import { Visualization } from '@cdc/core/types/Visualization'
|
|
3
|
+
import { DashboardConfig } from '@cdc/dashboard/src/types/DashboardConfig'
|
|
4
|
+
import _ from 'lodash'
|
|
5
|
+
|
|
6
|
+
const cleanDashboardFootnotes = (config: DashboardConfig) => {
|
|
7
|
+
// strip any blank footnote visualizations
|
|
8
|
+
const footnoteIds: string[] = []
|
|
9
|
+
|
|
10
|
+
if (config.rows) {
|
|
11
|
+
config.rows.forEach(row => {
|
|
12
|
+
if (row.footnotesId) {
|
|
13
|
+
const { dataKey, staticFootnotes } = config.visualizations[row.footnotesId] as Footnotes
|
|
14
|
+
if (!dataKey && !staticFootnotes?.length) {
|
|
15
|
+
delete config.visualizations[row.footnotesId]
|
|
16
|
+
} else {
|
|
17
|
+
footnoteIds.push(row.footnotesId)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (config.visualizations) {
|
|
24
|
+
Object.keys(config.visualizations).forEach(vizKey => {
|
|
25
|
+
const viz: Visualization = config.visualizations[vizKey]
|
|
26
|
+
if (viz.type === 'footnotes' && !footnoteIds.includes(vizKey)) {
|
|
27
|
+
// if footnote isn't being used by any rows, remove it
|
|
28
|
+
delete config.visualizations[vizKey]
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cleanDashboardData = (config: DashboardConfig) => {
|
|
35
|
+
if (config.datasets) {
|
|
36
|
+
Object.keys(config.datasets).forEach(datasetKey => {
|
|
37
|
+
delete config.datasets[datasetKey].formattedData
|
|
38
|
+
if (config.datasets[datasetKey].dataUrl) {
|
|
39
|
+
delete config.datasets[datasetKey].data
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
if (config.visualizations) {
|
|
44
|
+
Object.keys(config.visualizations).forEach(vizKey => {
|
|
45
|
+
config.visualizations[vizKey] = _.omit(config.visualizations[vizKey], ['runtime', 'formattedData', 'data'])
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
if (config.rows) {
|
|
49
|
+
config.rows.forEach((row, i) => {
|
|
50
|
+
if (row.dataKey) {
|
|
51
|
+
config.rows[i] = _.omit(row, ['data', 'formattedData'])
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cleanSharedFilters = (config: DashboardConfig) => {
|
|
58
|
+
if (config.dashboard?.sharedFilters) {
|
|
59
|
+
config.dashboard.sharedFilters.forEach((filter, index) => {
|
|
60
|
+
delete config.dashboard.sharedFilters[index].active
|
|
61
|
+
if (filter.type === 'urlfilter') {
|
|
62
|
+
delete config.dashboard.sharedFilters[index].values
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const formatConfigBeforeSave = configToStrip => {
|
|
69
|
+
let strippedConfig = _.cloneDeep(configToStrip)
|
|
70
|
+
if (strippedConfig.type === 'dashboard') {
|
|
71
|
+
if (strippedConfig.multiDashboards) {
|
|
72
|
+
strippedConfig.multiDashboards.forEach((multiDashboard, i) => {
|
|
73
|
+
cleanDashboardData(strippedConfig.multiDashboards[i])
|
|
74
|
+
cleanSharedFilters(strippedConfig.multiDashboards[i])
|
|
75
|
+
cleanDashboardFootnotes(strippedConfig.multiDashboards[i])
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
cleanDashboardData(strippedConfig)
|
|
79
|
+
cleanSharedFilters(strippedConfig)
|
|
80
|
+
cleanDashboardFootnotes(strippedConfig)
|
|
81
|
+
} else {
|
|
82
|
+
delete strippedConfig.runtime
|
|
83
|
+
delete strippedConfig.formattedData
|
|
84
|
+
if (strippedConfig.dataUrl) {
|
|
85
|
+
delete strippedConfig.data
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return strippedConfig
|
|
90
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const gatherQueryParams = (baseEndpoint: string, params: { key: string; value: string }[]) => {
|
|
4
|
+
const baseEndpointHasQueryParams = baseEndpoint.includes('?')
|
|
5
|
+
return params
|
|
6
|
+
.filter(({ value }) => value !== '')
|
|
7
|
+
.map(({ key, value }, i) => {
|
|
8
|
+
const leadingCharacter = i === 0 && !baseEndpointHasQueryParams ? '?' : '&'
|
|
9
|
+
const isStatementParam = key.match(/\$.*/)
|
|
10
|
+
if (!_.isNaN(parseInt(value)) || isStatementParam) return leadingCharacter + key + '=' + value
|
|
11
|
+
return leadingCharacter + key + '=' + `"${value}"`
|
|
12
|
+
})
|
|
13
|
+
.join('')
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
/** columnName is the column you'd like to select data values from to show as column headers.
|
|
4
|
+
* Pivot is the value column who's data you'd like to show under those respective columns*/
|
|
5
|
+
export const pivotData = (data: Record<string, any>[], columnName: string, pivot: string) => {
|
|
6
|
+
const grouped = _.groupBy(data, val => val[columnName])
|
|
7
|
+
const newData = []
|
|
8
|
+
for (const key in grouped) {
|
|
9
|
+
const group = grouped[key]
|
|
10
|
+
group.forEach((val, index) => {
|
|
11
|
+
const row = newData[index] || {}
|
|
12
|
+
row[key] = val[pivot]
|
|
13
|
+
const toAdd = _.omit(val, [columnName, pivot])
|
|
14
|
+
newData[index] = { ...toAdd, ...row }
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
return newData
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function getQueryStringFilterValue(filter) {
|
|
2
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
3
|
+
if (filter.setByQueryParameter) {
|
|
4
|
+
// Only check the query string if the filter is supposed to be set by QS param
|
|
5
|
+
const filterValue = urlParams.get(filter.setByQueryParameter)
|
|
6
|
+
if (filterValue && filter.values) {
|
|
7
|
+
for (let i = 0; i < filter.values.length; i++) {
|
|
8
|
+
if (filter.values[i] && filter.values[i].toLowerCase() === filterValue.toLowerCase()) {
|
|
9
|
+
return filter.values[i]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getQueryParams() {
|
|
17
|
+
const queryParams = {}
|
|
18
|
+
for (const [key, value] of Array.from(new URLSearchParams(window.location.search).entries())) {
|
|
19
|
+
queryParams[key] = value
|
|
20
|
+
}
|
|
21
|
+
return queryParams
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function updateQueryString(queryParams) {
|
|
25
|
+
const updateUrl = `${window.location.origin}${window.location.pathname}?${Object.keys(queryParams)
|
|
26
|
+
.map(queryParam => `${queryParam}=${encodeURIComponent(queryParams[queryParam])}`)
|
|
27
|
+
.join('&')}`
|
|
28
|
+
window.history.pushState({ path: updateUrl }, '', updateUrl)
|
|
29
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UpdateFieldFunc } from '../types/UpdateFieldFunc'
|
|
2
2
|
|
|
3
3
|
export const updateFieldFactory =
|
|
4
|
-
(config, updateConfig, legacy = false): UpdateFieldFunc<
|
|
4
|
+
<T>(config, updateConfig, legacy = false): UpdateFieldFunc<T> =>
|
|
5
5
|
(section, subsection, fieldName, newValue) => {
|
|
6
6
|
// Top level
|
|
7
7
|
if (null === section && null === subsection) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
|
|
3
|
+
import { MultiDashboardConfig } from '@cdc/dashboard/src/types/MultiDashboard'
|
|
4
|
+
import { AnyVisualization } from '../../types/Visualization'
|
|
5
|
+
|
|
6
|
+
export const dashboardFiltersMigrate = config => {
|
|
7
|
+
if (!config.dashboard) return config
|
|
8
|
+
const dashboardConfig = config as MultiDashboardConfig
|
|
9
|
+
const newVisualizations = {}
|
|
10
|
+
// autoload was removed from APIFilter type
|
|
11
|
+
const newSharedFilters = (dashboardConfig.dashboard.sharedFilters || []).map(sf => {
|
|
12
|
+
if (sf.apiFilter?.autoLoad !== undefined) {
|
|
13
|
+
delete sf.apiFilter.autoLoad
|
|
14
|
+
}
|
|
15
|
+
if (sf.apiFilter?.defaultValue !== undefined) {
|
|
16
|
+
delete sf.apiFilter.defaultValue
|
|
17
|
+
}
|
|
18
|
+
return sf
|
|
19
|
+
})
|
|
20
|
+
config.dashboard.sharedFilters = newSharedFilters
|
|
21
|
+
|
|
22
|
+
Object.keys(dashboardConfig.visualizations).forEach(vizKey => {
|
|
23
|
+
const viz = dashboardConfig.visualizations[vizKey] as DashboardFilters
|
|
24
|
+
// hide was removed from visualizations
|
|
25
|
+
if (viz.hide !== undefined) {
|
|
26
|
+
viz.sharedFilterIndexes = newSharedFilters.map((_sf, i) => i).filter(i => !viz.hide.includes(i))
|
|
27
|
+
viz.type = 'dashboardFilters'
|
|
28
|
+
if (viz.autoLoad) {
|
|
29
|
+
viz.filterBehavior = 'Filter Change'
|
|
30
|
+
} else {
|
|
31
|
+
viz.filterBehavior = 'Apply Button'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
delete viz.hide
|
|
35
|
+
}
|
|
36
|
+
// 'filter-dropdowns' was renamed to 'dashboardFilters' for clarity
|
|
37
|
+
if (viz.type === 'filter-dropdowns') viz.type = 'dashboardFilters'
|
|
38
|
+
if (viz.visualizationType === 'filter-dropdowns') viz.visualizationType = 'dashboardFilters'
|
|
39
|
+
newVisualizations[vizKey] = viz
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (config.dashboard.sharedFilters.length && !Object.values(newVisualizations).find((v: AnyVisualization) => v.type === 'dashboardFilters')) {
|
|
43
|
+
const newViz = {
|
|
44
|
+
type: 'dashboardFilters',
|
|
45
|
+
visualizationType: 'dashboardFilters',
|
|
46
|
+
sharedFilterIndexes: config.dashboard.sharedFilters.map((_sf, i) => i),
|
|
47
|
+
filterBehavior: config.filterBehavior || 'Filter Change'
|
|
48
|
+
}
|
|
49
|
+
const key = 'legacySharedFilters'
|
|
50
|
+
newVisualizations[key] = newViz
|
|
51
|
+
const newRow = {
|
|
52
|
+
columns: [
|
|
53
|
+
{
|
|
54
|
+
width: 12,
|
|
55
|
+
widget: key
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
config.rows = [newRow, ...config.rows]
|
|
60
|
+
}
|
|
61
|
+
// if there's no dashboardFilters visualization but there are sharedFilters create a visualization and update rows.
|
|
62
|
+
|
|
63
|
+
config.visualizations = newVisualizations
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mapUpdates = newConfig => {
|
|
67
|
+
// When switching between old version of equal number, and the revised equal number opt in, roundToPlace needs to be set.
|
|
68
|
+
// There wasn't an initial value set for this, and legends would return NaN if it wasn't set. ie. 0 - NAN instead of 0 - 1
|
|
69
|
+
const equalNumberRoundingPatch = newConfig => {
|
|
70
|
+
if (newConfig.type === 'map') {
|
|
71
|
+
if (newConfig.columns.primary.roundToPlace === undefined) {
|
|
72
|
+
newConfig.columns.primary.roundToPlace = 0
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
equalNumberRoundingPatch(newConfig)
|
|
78
|
+
|
|
79
|
+
return newConfig
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const update_4_24_7 = config => {
|
|
83
|
+
const ver = '4.24.7'
|
|
84
|
+
|
|
85
|
+
const newConfig = _.cloneDeep(config)
|
|
86
|
+
|
|
87
|
+
mapUpdates(newConfig)
|
|
88
|
+
dashboardFiltersMigrate(newConfig)
|
|
89
|
+
newConfig.version = ver
|
|
90
|
+
return newConfig
|
|
91
|
+
}
|
|
92
|
+
export default update_4_24_7
|