@cdc/core 4.24.5 → 4.24.9
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/assets/icon-gear-multi.svg +23 -0
- 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/Alert/components/Alert.styles.css +15 -0
- package/components/Alert/components/Alert.tsx +39 -0
- package/components/Alert/index.tsx +3 -0
- package/components/DataTable/DataTable.tsx +127 -32
- 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 +6 -12
- package/components/DataTable/helpers/getChartCellValue.ts +9 -5
- package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -7
- package/components/DataTable/helpers/getRowType.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +3 -3
- package/components/DataTable/types/TableConfig.ts +2 -1
- 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/Inputs.tsx +13 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +268 -0
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +306 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +40 -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.tsx +520 -0
- 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/Responsive.tsx +14 -4
- package/components/Layout/components/Sidebar/components/Sidebar.tsx +14 -5
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +23 -20
- package/components/Layout/components/Visualization/index.tsx +19 -6
- package/components/Layout/components/Visualization/visualizations.scss +32 -26
- package/components/Layout/styles/editor.scss +0 -8
- package/components/Legend/Legend.Gradient.tsx +133 -0
- package/components/LegendShape.tsx +28 -0
- package/components/MultiSelect/MultiSelect.tsx +41 -11
- package/components/MultiSelect/multiselect.styles.css +0 -3
- package/components/NestedDropdown/NestedDropdown.tsx +47 -52
- package/components/NestedDropdown/nesteddropdown.styles.css +19 -25
- package/components/Table/Table.tsx +8 -5
- package/components/Table/components/Cell.tsx +2 -2
- package/components/Table/components/Row.tsx +25 -7
- package/components/_stories/Footnotes.stories.tsx +17 -0
- package/components/_stories/Layout.Debug.stories.tsx +91 -0
- package/components/_stories/_mocks/bar-chart-suppressed.json +474 -0
- package/components/_stories/styles.scss +14 -1
- package/components/createBarElement.jsx +4 -4
- package/components/inputs/InputSelect.tsx +17 -6
- package/components/ui/Icon.tsx +22 -16
- package/components/ui/Title/Title.scss +0 -8
- package/helpers/DataTransform.ts +2 -2
- package/helpers/addValuesToFilters.ts +135 -0
- package/helpers/cove/accessibility.ts +17 -4
- package/helpers/cove/fontSettings.ts +2 -0
- package/helpers/coveUpdateWorker.ts +30 -9
- package/helpers/filterVizData.ts +49 -0
- package/helpers/formatConfigBeforeSave.ts +95 -0
- package/helpers/gatherQueryParams.ts +14 -7
- package/helpers/getGradientLegendWidth.ts +15 -0
- package/helpers/getTextWidth.ts +18 -0
- package/helpers/lineChartHelpers.js +2 -1
- package/helpers/pivotData.ts +18 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/scaling.ts +7 -0
- package/helpers/tests/addValuesToFilters.test.ts +55 -0
- package/helpers/tests/filterVizData.test.ts +31 -0
- package/helpers/tests/invertValue.test.ts +35 -0
- package/helpers/tests/updateFieldFactory.test.ts +1 -0
- package/helpers/updateFieldFactory.ts +1 -1
- package/helpers/updatePaletteNames.ts +19 -0
- package/helpers/{useDataVizClasses.js → useDataVizClasses.ts} +3 -2
- package/helpers/ver/4.24.5.ts +3 -3
- package/helpers/ver/4.24.7.ts +123 -0
- package/helpers/ver/4.24.9.ts +63 -0
- package/helpers/ver/tests/4.24.9.test.ts +22 -0
- package/helpers/ver/versionNeedsUpdate.ts +9 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +7 -2
- package/styles/_data-table.scss +0 -1
- package/styles/_global.scss +6 -2
- package/styles/base.scss +4 -0
- package/styles/filters.scss +4 -0
- package/styles/v2/themes/_color-definitions.scss +1 -0
- package/types/Annotation.ts +46 -0
- package/types/Axis.ts +3 -2
- package/types/ConfigureData.ts +1 -1
- package/types/Dimensions.ts +1 -0
- 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 +39 -7
- package/LICENSE +0 -201
- package/components/AdvancedEditor.jsx +0 -74
- package/components/EditorPanel/VizFilterEditor.tsx +0 -234
- package/components/Filters.jsx +0 -461
- package/components/LegendCircle.jsx +0 -17
- package/helpers/queryStringUtils.js +0 -26
- package/helpers/updatePaletteNames.js +0 -16
- package/types/BaseVisualizationType.ts +0 -1
- /package/components/{Waiting.jsx → Waiting.tsx} +0 -0
- /package/helpers/ver/{4.23.4.ts → 4.24.4.ts} +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
|
|
3
|
+
import { VizFilter } from '../types/VizFilter'
|
|
4
|
+
|
|
5
|
+
type Filter = {
|
|
6
|
+
columnName: string
|
|
7
|
+
values: (string | number)[]
|
|
8
|
+
filterStyle?: string
|
|
9
|
+
active?: string | number | (string | number)[]
|
|
10
|
+
parents?: (string | number)[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** MapData is an object */
|
|
14
|
+
type MapData = Record<string, any[]>
|
|
15
|
+
|
|
16
|
+
const cleanLookup = (lookup: Record<string, { values: string[]; orderedValues?: string[] }>) => {
|
|
17
|
+
// for nested-dropdown
|
|
18
|
+
// removes values from subGrouping.valuesLookup
|
|
19
|
+
// keeps orderedValues
|
|
20
|
+
return Object.fromEntries(
|
|
21
|
+
Object.entries(lookup || {}).map(([key, { orderedValues }]) => {
|
|
22
|
+
if (!orderedValues) return [key, { values: [] }]
|
|
23
|
+
return [key, { orderedValues, values: [] }]
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Gets filter values from dataset
|
|
29
|
+
const generateValuesForFilter = (filter: VizFilter, data: any[] | MapData) => {
|
|
30
|
+
const columnName = filter.columnName
|
|
31
|
+
const values: string[] = []
|
|
32
|
+
const subGroupingColumn = filter.subGrouping?.columnName
|
|
33
|
+
const subValues = cleanLookup(filter.subGrouping?.valuesLookup)
|
|
34
|
+
if (Array.isArray(data)) {
|
|
35
|
+
data.forEach(row => {
|
|
36
|
+
const value = row[columnName]
|
|
37
|
+
if (value !== undefined && !values.includes(value)) {
|
|
38
|
+
values.push(value)
|
|
39
|
+
}
|
|
40
|
+
if (subGroupingColumn) {
|
|
41
|
+
const dataValue = row[subGroupingColumn]
|
|
42
|
+
if (value === undefined) return
|
|
43
|
+
if (!subValues[value]) {
|
|
44
|
+
subValues[value] = { values: [] }
|
|
45
|
+
}
|
|
46
|
+
if (!subValues[value].values.includes(dataValue)) {
|
|
47
|
+
subValues[value].values.push(dataValue)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
} else {
|
|
52
|
+
// data is a dataset this loops through ALL datasets to find matching values
|
|
53
|
+
// not sure if this is desired behavior
|
|
54
|
+
// Maps Only
|
|
55
|
+
if (!data) return values
|
|
56
|
+
Object.values(data).forEach((rows: any[]) => {
|
|
57
|
+
rows.forEach(row => {
|
|
58
|
+
const value = row[columnName]
|
|
59
|
+
if (value !== undefined && !values.includes(value)) {
|
|
60
|
+
values.push(value)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
filter.values = values
|
|
66
|
+
if (subGroupingColumn) {
|
|
67
|
+
filter.subGrouping.valuesLookup = subValues
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handleVizParents = (filter: VizFilter, data: any[] | MapData, filtersLookup: Record<string, Filter>) => {
|
|
72
|
+
let filteredData = Array.isArray(data) ? data : Object.values(data).flat(1)
|
|
73
|
+
filter.parents.forEach(parentKey => {
|
|
74
|
+
const parent = filtersLookup[parentKey]
|
|
75
|
+
if (parent.filterStyle === 'nested-dropdown') {
|
|
76
|
+
const { subGrouping } = parent as VizFilter
|
|
77
|
+
if (subGrouping.active) {
|
|
78
|
+
filteredData = filteredData.filter(d => {
|
|
79
|
+
const matchingParentGroup = parent.active == d[parent.columnName]
|
|
80
|
+
const matchingSubGroup = subGrouping.active == d[subGrouping.columnName]
|
|
81
|
+
return matchingParentGroup && matchingSubGroup
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
} else if (parent?.active) {
|
|
85
|
+
filteredData = filteredData.filter(d => {
|
|
86
|
+
if (Array.isArray(parent.active)) {
|
|
87
|
+
return parent.active.includes(d[parent.columnName])
|
|
88
|
+
}
|
|
89
|
+
return parent.active == d[parent.columnName]
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
return filteredData
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const includes = (arr: any[], val: any): boolean => {
|
|
97
|
+
return arr.map(val => String(val)).includes(String(val))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const addValuesToFilters = (filters: VizFilter[], data: any[] | MapData): Array<VizFilter> => {
|
|
101
|
+
const filtersLookup = _.keyBy(filters, 'id')
|
|
102
|
+
return filters?.map(filter => {
|
|
103
|
+
const filterCopy = _.cloneDeep(filter)
|
|
104
|
+
let filteredData = data
|
|
105
|
+
const isMapData = !Array.isArray(data)
|
|
106
|
+
if (filter.parents?.length && !isMapData) {
|
|
107
|
+
filteredData = handleVizParents(filter as VizFilter, data, filtersLookup)
|
|
108
|
+
}
|
|
109
|
+
generateValuesForFilter(filterCopy, filteredData)
|
|
110
|
+
if (filterCopy.values.length > 0) {
|
|
111
|
+
const queryStringFilterValue = getQueryStringFilterValue(filterCopy)
|
|
112
|
+
if (queryStringFilterValue) {
|
|
113
|
+
filterCopy.active = queryStringFilterValue
|
|
114
|
+
} else if (filterCopy.filterStyle === 'multi-select') {
|
|
115
|
+
const defaultValues = filterCopy.values
|
|
116
|
+
const active = Array.isArray(filterCopy.active) ? filterCopy.active : [filterCopy.active]
|
|
117
|
+
filterCopy.active = active.filter(val => includes(defaultValues, val))
|
|
118
|
+
} else {
|
|
119
|
+
const defaultValue = filterCopy.values[0]
|
|
120
|
+
const active = Array.isArray(filterCopy.active) ? filterCopy.active[0] : filterCopy.active
|
|
121
|
+
filterCopy.active = includes(filterCopy.values, active) ? active : defaultValue
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (filterCopy.subGrouping) {
|
|
125
|
+
const queryStringFilterValue = getQueryStringFilterValue(filterCopy.subGrouping)
|
|
126
|
+
const groupActive = filterCopy.active || filterCopy.values[0]
|
|
127
|
+
const defaultValue = filterCopy.subGrouping.valuesLookup[groupActive as string].values[0]
|
|
128
|
+
// if the value doesn't exist in the subGrouping then return the default
|
|
129
|
+
const filteredLookupValues = Object.values(filterCopy.subGrouping.valuesLookup).flatMap(v => v.values)
|
|
130
|
+
const activeValue = queryStringFilterValue || filterCopy.subGrouping.active
|
|
131
|
+
filterCopy.subGrouping.active = filteredLookupValues.includes(activeValue) ? activeValue : defaultValue
|
|
132
|
+
}
|
|
133
|
+
return filterCopy
|
|
134
|
+
})
|
|
135
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import chroma from 'chroma-js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* WCAG 2.
|
|
5
|
-
* https://www.w3.org/TR/WCAG20-TECHS/G18.html
|
|
4
|
+
* WCAG 2.1 CONSTANTS
|
|
5
|
+
* TEXT (4.5 Contrast Ratio): https://www.w3.org/TR/WCAG20-TECHS/G18.html
|
|
6
|
+
* NON-TEXT (3.5 Contrast Ratio): https://www.w3.org/TR/WCAG20-TECHS/G145.html
|
|
6
7
|
*
|
|
7
8
|
* !important - DO NOT CHANGE.
|
|
8
9
|
*/
|
|
9
|
-
export const
|
|
10
|
+
export const WCAG_TEXT_CONTRAST_RATIO = 4.5
|
|
11
|
+
export const WCAG_NON_TEXT_CONTRAST_RATIO = 3.5
|
|
10
12
|
|
|
11
13
|
export const getContrastColor = (textColor: string, bgColor: string) => {
|
|
12
|
-
if (
|
|
14
|
+
if (!bgColor) return
|
|
15
|
+
if (chroma.contrast(textColor, bgColor) < WCAG_TEXT_CONTRAST_RATIO) {
|
|
13
16
|
switch (textColor) {
|
|
14
17
|
case '#FFF':
|
|
15
18
|
return '#000'
|
|
@@ -21,3 +24,13 @@ export const getContrastColor = (textColor: string, bgColor: string) => {
|
|
|
21
24
|
}
|
|
22
25
|
return textColor
|
|
23
26
|
}
|
|
27
|
+
|
|
28
|
+
export const checkColorContrast = (color1: string, color2: string) => {
|
|
29
|
+
if (!chroma.valid(color1) || !chroma.valid(color2)) return false
|
|
30
|
+
return chroma.contrast(color1, color2) >= WCAG_NON_TEXT_CONTRAST_RATIO
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const getColorContrast = (color1: string, color2: string) => {
|
|
34
|
+
if (!chroma.valid(color1) || !chroma.valid(color2)) return false
|
|
35
|
+
return chroma.contrast(color1, color2)
|
|
36
|
+
}
|
|
@@ -1,21 +1,42 @@
|
|
|
1
1
|
// If config key names or position in the config have been changed with a version change,
|
|
2
2
|
// process those config entries and format old values into new
|
|
3
|
-
import update_4_24_4 from './ver/4.
|
|
3
|
+
import update_4_24_4 from './ver/4.24.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'
|
|
7
|
+
import update_4_24_9 from './ver/4.24.9'
|
|
8
|
+
import versionNeedsUpdate from './ver/versionNeedsUpdate'
|
|
9
|
+
import { UpdateFunction } from 'json-edit-react'
|
|
6
10
|
|
|
7
11
|
export const coveUpdateWorker = config => {
|
|
12
|
+
if (config.multiDashboards) {
|
|
13
|
+
config.multiDashboards.forEach((dashboard, index) => {
|
|
14
|
+
config.multiDashboards[index] = coveUpdateWorker(dashboard)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
8
17
|
let genConfig = config
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
const versions = [
|
|
20
|
+
['4.24.3', update_4_24_3],
|
|
21
|
+
['4.24.4', update_4_24_4],
|
|
22
|
+
['4.24.5', update_4_24_5],
|
|
23
|
+
['4.24.7', update_4_24_7, true],
|
|
24
|
+
['4.24.9', update_4_24_9]
|
|
25
|
+
]
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
versions.forEach(([version, updateFunction, alwaysRun]: [string, UpdateFunction, boolean?]) => {
|
|
28
|
+
if (versionNeedsUpdate(genConfig.version, version) || alwaysRun) {
|
|
29
|
+
genConfig = updateFunction(genConfig)
|
|
30
|
+
}
|
|
31
|
+
if (genConfig.multiDashboards) {
|
|
32
|
+
genConfig.multiDashboards.forEach((dashboard, index) => {
|
|
33
|
+
dashboard.type = 'dashboard'
|
|
34
|
+
genConfig.multiDashboards[index] = coveUpdateWorker(dashboard)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
return await coveUpdateWorker(config)
|
|
39
|
+
return genConfig
|
|
19
40
|
}
|
|
20
41
|
|
|
21
|
-
export default
|
|
42
|
+
export default coveUpdateWorker
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SubGrouping } from '../types/VizFilter'
|
|
2
|
+
|
|
3
|
+
type Filter = {
|
|
4
|
+
columnName: string
|
|
5
|
+
type?: string
|
|
6
|
+
values: (string | number)[]
|
|
7
|
+
filterStyle?: string
|
|
8
|
+
active?: string | number | (string | number)[]
|
|
9
|
+
parents?: (string | number)[]
|
|
10
|
+
subGrouping?: SubGrouping
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const filterVizData = (filters: Filter[], data) => {
|
|
14
|
+
if (!data) {
|
|
15
|
+
console.warn('COVE: No data to filter')
|
|
16
|
+
return []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!filters) return data
|
|
20
|
+
const filteredData: any[] = []
|
|
21
|
+
|
|
22
|
+
data?.forEach(row => {
|
|
23
|
+
let add = true
|
|
24
|
+
filters
|
|
25
|
+
.filter(filter => filter.type !== 'url')
|
|
26
|
+
.forEach(filter => {
|
|
27
|
+
if (filter.active === undefined) return
|
|
28
|
+
const value = row[filter.columnName]
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(filter.active)) {
|
|
31
|
+
if (!filter.active.includes(value)) {
|
|
32
|
+
add = false
|
|
33
|
+
}
|
|
34
|
+
} else if (value != filter.active) {
|
|
35
|
+
add = false
|
|
36
|
+
}
|
|
37
|
+
if (filter.filterStyle === 'nested-dropdown' && filter.subGrouping && add === true) {
|
|
38
|
+
const subGroupActive = filter.subGrouping.active
|
|
39
|
+
const value = row[filter.subGrouping.columnName]
|
|
40
|
+
if (subGroupActive === undefined) return
|
|
41
|
+
if (value != subGroupActive) {
|
|
42
|
+
add = false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
if (add) filteredData.push(row)
|
|
47
|
+
})
|
|
48
|
+
return filteredData
|
|
49
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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', 'editing', 'originalFormattedData'])
|
|
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
|
+
const 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
|
+
delete strippedConfig.dashboard
|
|
78
|
+
delete strippedConfig.rows
|
|
79
|
+
delete strippedConfig.visualizations
|
|
80
|
+
delete strippedConfig.label
|
|
81
|
+
delete strippedConfig.activeDashboard
|
|
82
|
+
}
|
|
83
|
+
cleanDashboardData(strippedConfig)
|
|
84
|
+
cleanSharedFilters(strippedConfig)
|
|
85
|
+
cleanDashboardFootnotes(strippedConfig)
|
|
86
|
+
} else {
|
|
87
|
+
delete strippedConfig.runtime
|
|
88
|
+
delete strippedConfig.formattedData
|
|
89
|
+
if (strippedConfig.dataUrl) {
|
|
90
|
+
delete strippedConfig.data
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return strippedConfig
|
|
95
|
+
}
|
|
@@ -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,15 @@
|
|
|
1
|
+
export const getGradientLegendWidth = (containerWidth: number, currentViewport: string): number => {
|
|
2
|
+
const newWidth = Number(containerWidth)
|
|
3
|
+
switch (currentViewport) {
|
|
4
|
+
case 'lg':
|
|
5
|
+
return newWidth / 3
|
|
6
|
+
case 'md':
|
|
7
|
+
return newWidth / 2
|
|
8
|
+
case 'sm':
|
|
9
|
+
case 'xs':
|
|
10
|
+
case 'xxs':
|
|
11
|
+
return newWidth / 1.4
|
|
12
|
+
default:
|
|
13
|
+
return newWidth
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets the width of the specified text using a given font.
|
|
3
|
+
* @param text The text to measure.
|
|
4
|
+
* @param font The CSS font declaration to use, defaults to the body's font if not provided.
|
|
5
|
+
* @returns The width of the text in pixels, or undefined if the context could not be obtained.
|
|
6
|
+
*/
|
|
7
|
+
export const getTextWidth = (text: string, font?: string): number | undefined => {
|
|
8
|
+
// Create a canvas element
|
|
9
|
+
const canvas = document.createElement('canvas')
|
|
10
|
+
const context = canvas.getContext('2d')
|
|
11
|
+
if (!context) {
|
|
12
|
+
console.error('2D context not found')
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
context.font = font || getComputedStyle(document.body).font
|
|
17
|
+
return Math.ceil(context.measureText(text).width)
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { VizFilter } from '../../types/VizFilter'
|
|
3
|
+
import { addValuesToFilters } from '../addValuesToFilters'
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
describe('addValuesToFilters', () => {
|
|
7
|
+
const parentFilter = { columnName: 'parentColumn', id: 11, active: 'apple', values: [] } as VizFilter
|
|
8
|
+
const parentFilter2 = { columnName: 'parentColumn2', id: 22, active: '1', values: [] } as VizFilter
|
|
9
|
+
const childFilter = { columnName: 'childColumn', id: 33, parents: [11, 22], values: [] } as VizFilter
|
|
10
|
+
const data = [
|
|
11
|
+
{ parentColumn: 'apple', parentColumn2: 3, childColumn: 'a' },
|
|
12
|
+
{ parentColumn: 'apple', parentColumn2: 1, childColumn: 'b' },
|
|
13
|
+
{ parentColumn: 'pear', parentColumn2: 4, childColumn: 'c' }
|
|
14
|
+
]
|
|
15
|
+
const filters: VizFilter[] = [parentFilter, childFilter, parentFilter2]
|
|
16
|
+
it('adds filter values based on parent active values', () => {
|
|
17
|
+
const filtersCopy = _.cloneDeep(filters)
|
|
18
|
+
const newFilters = addValuesToFilters(filtersCopy, data)
|
|
19
|
+
expect(newFilters[0].values).toEqual(['apple', 'pear'])
|
|
20
|
+
expect(newFilters[2].values).toEqual([3, 1, 4])
|
|
21
|
+
expect(newFilters[1].values).toEqual(['b'])
|
|
22
|
+
|
|
23
|
+
filtersCopy[0].active = 'pear'
|
|
24
|
+
const newFilters2 = addValuesToFilters(filtersCopy, data)
|
|
25
|
+
expect(newFilters2[0].values).toEqual(['apple', 'pear'])
|
|
26
|
+
expect(newFilters2[2].values).toEqual([3, 1, 4])
|
|
27
|
+
expect(newFilters2[1].values).toEqual([])
|
|
28
|
+
})
|
|
29
|
+
it('works when data is an object', () => {
|
|
30
|
+
const filtersCopy = _.cloneDeep(filters)
|
|
31
|
+
const newFilters = addValuesToFilters(filtersCopy, { '0': data })
|
|
32
|
+
expect(newFilters[0].values).toEqual(['apple', 'pear'])
|
|
33
|
+
expect(newFilters[2].values).toEqual([3, 1, 4])
|
|
34
|
+
// This test is failing
|
|
35
|
+
// data is only an object when using map data according to Adam Doe.
|
|
36
|
+
//expect(newFilters[1].values).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
it('works for nested dropdowns', () => {
|
|
39
|
+
const nestedParentFilter = { ...parentFilter, subGrouping: { columnName: 'childColumn' } }
|
|
40
|
+
const newFilters = addValuesToFilters([nestedParentFilter], data)
|
|
41
|
+
expect(newFilters[0].values).toEqual(['apple', 'pear'])
|
|
42
|
+
expect(newFilters[0].subGrouping.valuesLookup).toEqual({ apple: { values: ['a', 'b'] }, pear: { values: ['c'] } })
|
|
43
|
+
nestedParentFilter.active = 'apple'
|
|
44
|
+
expect(newFilters[0].subGrouping.active).toEqual('a')
|
|
45
|
+
})
|
|
46
|
+
it('maintains nested custom order', () => {
|
|
47
|
+
const nestedParentFilter = {
|
|
48
|
+
...parentFilter,
|
|
49
|
+
subGrouping: { columnName: 'childColumn', valuesLookup: { apple: { orderedValues: ['b', 'a'] } } }
|
|
50
|
+
}
|
|
51
|
+
const newFilters = addValuesToFilters([nestedParentFilter], data)
|
|
52
|
+
expect(newFilters[0].values).toEqual(['apple', 'pear'])
|
|
53
|
+
expect(newFilters[0].subGrouping.valuesLookup.apple.orderedValues).toEqual(['b', 'a'])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { filterVizData } from '../filterVizData'
|
|
3
|
+
describe('filterVizData', () => {
|
|
4
|
+
it('filters data when there is a nested filter', () => {
|
|
5
|
+
const nestedFilter = {
|
|
6
|
+
columnName: 'nestedColumn',
|
|
7
|
+
id: 1,
|
|
8
|
+
active: 'nestedValue',
|
|
9
|
+
values: [],
|
|
10
|
+
filterStyle: 'nested-dropdown',
|
|
11
|
+
subGrouping: {
|
|
12
|
+
active: 'subGroupValue',
|
|
13
|
+
columnName: 'subGroupColumn',
|
|
14
|
+
valuesLookup: { nestedValue: { values: ['subGroupValue'] } }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const normalFilter = { columnName: 'normalColumn', id: 2, active: 'normalValue', values: [] }
|
|
18
|
+
|
|
19
|
+
const data = [
|
|
20
|
+
{ nestedColumn: 'nestedValue', subGroupColumn: 'subGroupValue', normalColumn: 'normalValue' },
|
|
21
|
+
{ nestedColumn: 'X', subGroupColumn: 'subGroupValue', normalColumn: 'normalValue' },
|
|
22
|
+
{ nestedColumn: 'nestedValue', subGroupColumn: 'X', normalColumn: 'normalValue' },
|
|
23
|
+
{ nestedColumn: 'nestedValue', subGroupColumn: 'subGroupValue', normalColumn: 'X' },
|
|
24
|
+
{ nestedColumn: 'nestedValue', subGroupColumn: 'subGroupValue', normalColumn: 'normalValue' }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const filters = [nestedFilter, normalFilter]
|
|
28
|
+
const filteredData = filterVizData(filters, data)
|
|
29
|
+
expect(filteredData.length).toEqual(2)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// invertValue.test.js
|
|
2
|
+
import { invertValue } from '../scaling'
|
|
3
|
+
import { expect, describe, it, vi } from 'vitest'
|
|
4
|
+
describe('invertValue', () => {
|
|
5
|
+
it('should return the inverted value if xScale has an invert function', () => {
|
|
6
|
+
const xScale = {
|
|
7
|
+
invert: vi.fn().mockReturnValue(50)
|
|
8
|
+
}
|
|
9
|
+
const value = 100
|
|
10
|
+
|
|
11
|
+
const result = invertValue(xScale, value)
|
|
12
|
+
|
|
13
|
+
expect(xScale.invert).toHaveBeenCalledWith(value)
|
|
14
|
+
expect(result).toBe(50)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should return null if xScale does not have an invert function', () => {
|
|
18
|
+
const xScale = {}
|
|
19
|
+
const value = 100
|
|
20
|
+
|
|
21
|
+
const result = invertValue(xScale, value)
|
|
22
|
+
|
|
23
|
+
expect(result).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should return null if xScale is null or undefined', () => {
|
|
27
|
+
const value = 100
|
|
28
|
+
|
|
29
|
+
const resultWithNull = invertValue(null, value)
|
|
30
|
+
const resultWithUndefined = invertValue(undefined, value)
|
|
31
|
+
|
|
32
|
+
expect(resultWithNull).toBeNull()
|
|
33
|
+
expect(resultWithUndefined).toBeNull()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -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,19 @@
|
|
|
1
|
+
interface ColorPalettes {
|
|
2
|
+
[paletteName: string]: string[]
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const updatePaletteNames = (colorPalettes: ColorPalettes): ColorPalettes => {
|
|
6
|
+
// This function adds REVERSE keyword to each palette
|
|
7
|
+
delete colorPalettes.qualitative9 // Delete palette before reversing
|
|
8
|
+
|
|
9
|
+
let paletteReversed: ColorPalettes = {}
|
|
10
|
+
for (const [paletteName, hexCodeArr] of Object.entries(colorPalettes)) {
|
|
11
|
+
const paletteStr = String(paletteName)
|
|
12
|
+
|
|
13
|
+
if (!paletteStr.endsWith('reverse')) {
|
|
14
|
+
let palette = paletteStr.concat('reverse') // Add to the end of the string "reverse"
|
|
15
|
+
paletteReversed[palette] = [...hexCodeArr].reverse() // Reverses array elements and create new keys on object
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { ...paletteReversed, ...colorPalettes }
|
|
19
|
+
}
|