@cdc/core 4.25.3 → 4.25.5-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.
- package/assets/icon-close.svg +1 -1
- package/components/DataTable/DataTable.tsx +18 -13
- package/components/DataTable/components/CellAnchor.tsx +1 -1
- package/components/DataTable/components/ChartHeader.tsx +2 -1
- package/components/DataTable/components/MapHeader.tsx +1 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +2 -1
- package/components/DataTable/helpers/mapCellMatrix.tsx +17 -7
- package/components/DownloadButton.tsx +17 -2
- package/components/EditorPanel/DataTableEditor.tsx +1 -1
- package/components/EditorPanel/Inputs.tsx +12 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +2 -1
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +3 -1
- package/components/Filters/Filters.tsx +168 -429
- package/components/Filters/components/Dropdown.tsx +39 -0
- package/components/Filters/components/Tabs.tsx +82 -0
- package/components/Filters/helpers/getChangedFilters.ts +31 -0
- package/components/Filters/helpers/getNestedOptions.ts +2 -2
- package/components/Filters/helpers/getNewRuntime.ts +35 -0
- package/components/Filters/helpers/handleSorting.ts +2 -2
- package/components/Filters/helpers/tests/getChangedFilters.test.ts +92 -0
- package/components/Filters/helpers/tests/getNestedOptions.test.ts +31 -0
- package/components/Filters/helpers/tests/getNewRuntime.test.ts +82 -0
- package/components/Filters/index.ts +1 -1
- package/components/Layout/components/Visualization/index.tsx +3 -3
- package/components/Legend/Legend.Gradient.tsx +66 -23
- package/components/MultiSelect/multiselect.styles.css +2 -0
- package/components/NestedDropdown/NestedDropdown.tsx +2 -2
- package/components/RichTooltip/RichTooltip.tsx +37 -0
- package/components/RichTooltip/richTooltip.css +16 -0
- package/components/Table/Table.tsx +142 -142
- package/components/Table/components/Row.tsx +1 -1
- package/components/Table/table.styles.css +10 -0
- package/components/_stories/DataTable.stories.tsx +9 -2
- package/components/_stories/Table.stories.tsx +1 -1
- package/components/_stories/styles.scss +0 -4
- package/components/ui/Accordion.jsx +8 -1
- package/components/ui/Title/index.tsx +4 -1
- package/components/ui/Title/{Title.scss → title.styles.css} +0 -2
- package/components/ui/_stories/Colors.stories.mdx +220 -0
- package/components/ui/_stories/IconGallery.stories.mdx +14 -0
- package/components/ui/_stories/Title.stories.tsx +29 -4
- package/components/ui/accordion.styles.css +3 -0
- package/data/colorPalettes.js +0 -1
- package/dist/cove-main.css +3 -8
- package/dist/cove-main.css.map +1 -1
- package/helpers/constants.ts +6 -0
- package/helpers/cove/accessibility.ts +7 -8
- package/helpers/coveUpdateWorker.ts +5 -1
- package/helpers/filterOrderOptions.ts +17 -0
- package/helpers/isNumber.ts +20 -0
- package/helpers/isRightAlignedTableValue.js +1 -1
- package/helpers/pivotData.ts +16 -11
- package/helpers/tests/pivotData.test.ts +74 -0
- package/helpers/ver/4.25.3.ts +25 -2
- package/helpers/ver/4.25.4.ts +33 -0
- package/helpers/ver/tests/4.25.4.test.ts +24 -0
- package/helpers/viewports.ts +4 -0
- package/package.json +2 -3
- package/styles/_global-variables.scss +3 -0
- package/styles/_reset.scss +0 -6
- package/styles/v2/main.scss +0 -5
- package/types/General.ts +1 -0
- package/types/Legend.ts +1 -0
- package/LICENSE +0 -201
- package/components/ui/_stories/Colors.stories.tsx +0 -92
- package/components/ui/_stories/Icon.stories.tsx +0 -29
- package/helpers/cove/fontSettings.ts +0 -2
- package/helpers/isNumber.js +0 -24
- package/helpers/isNumberLog.js +0 -18
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { VizFilter } from '../../../types/VizFilter'
|
|
2
|
+
|
|
3
|
+
export const DROPDOWN_STYLES = 'py-2 ps-2 w-100 d-block'
|
|
4
|
+
|
|
5
|
+
type DropdownProps = {
|
|
6
|
+
index: number
|
|
7
|
+
label: string
|
|
8
|
+
filter: VizFilter
|
|
9
|
+
changeFilterActive: (index: number, value: string) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Dropdown: React.FC<DropdownProps> = ({ index: outerIndex, label, filter, changeFilterActive }) => {
|
|
13
|
+
const { active, queuedActive } = filter
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<select
|
|
17
|
+
id={`filter-${outerIndex}`}
|
|
18
|
+
name={label}
|
|
19
|
+
aria-label={`Filter by ${label}`}
|
|
20
|
+
className={`cove-form-select ${DROPDOWN_STYLES}`}
|
|
21
|
+
style={{ backgroundColor: 'white !important' }}
|
|
22
|
+
data-index='0'
|
|
23
|
+
value={queuedActive || active}
|
|
24
|
+
onChange={e => {
|
|
25
|
+
changeFilterActive(outerIndex, e.target.value)
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{filter.values?.map((value, index) => {
|
|
29
|
+
return (
|
|
30
|
+
<option key={index} value={value} aria-label={value}>
|
|
31
|
+
{filter.labels && filter.labels[value] ? filter.labels[value] : value}
|
|
32
|
+
</option>
|
|
33
|
+
)
|
|
34
|
+
})}
|
|
35
|
+
</select>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default Dropdown
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useId, useState } from 'react'
|
|
2
|
+
import { VizFilter } from '../../../types/VizFilter'
|
|
3
|
+
|
|
4
|
+
type TabsProps = {
|
|
5
|
+
filter: VizFilter
|
|
6
|
+
index: number
|
|
7
|
+
changeFilterActive: Function
|
|
8
|
+
theme: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Tabs: React.FC<TabsProps> = ({ filter, index: outerIndex, changeFilterActive, theme }) => {
|
|
12
|
+
const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
|
|
13
|
+
|
|
14
|
+
const id = useId()
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (selectedFilter) {
|
|
18
|
+
const el = document.getElementById(selectedFilter.id)
|
|
19
|
+
if (el) el.focus()
|
|
20
|
+
}
|
|
21
|
+
}, [selectedFilter])
|
|
22
|
+
|
|
23
|
+
const getClassList = value => {
|
|
24
|
+
const isActive = filter.active === value
|
|
25
|
+
let classList = []
|
|
26
|
+
switch (filter.filterStyle) {
|
|
27
|
+
case 'tab bar':
|
|
28
|
+
classList = ['button__tab-bar', isActive && 'button__tab-bar--active']
|
|
29
|
+
break
|
|
30
|
+
case 'pill':
|
|
31
|
+
classList = ['pill', isActive && 'pill--active', theme && theme]
|
|
32
|
+
break
|
|
33
|
+
default:
|
|
34
|
+
const tabSimple = filter.filterStyle === 'tab-simple' && 'tab--simple'
|
|
35
|
+
classList = ['tab', isActive && 'tab--active', theme && theme, tabSimple]
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
return classList.filter(Boolean).join(' ')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Tabs = filter.values.map((value, index) => {
|
|
42
|
+
return (
|
|
43
|
+
<button
|
|
44
|
+
id={`${value}-${outerIndex}-${index}-${id}`}
|
|
45
|
+
className={getClassList(value)}
|
|
46
|
+
onClick={e => {
|
|
47
|
+
changeFilterActive(outerIndex, value)
|
|
48
|
+
setSelectedFilter(e.target)
|
|
49
|
+
}}
|
|
50
|
+
onKeyDown={e => {
|
|
51
|
+
if (e.keyCode === 13) {
|
|
52
|
+
changeFilterActive(outerIndex, value)
|
|
53
|
+
setSelectedFilter(e.target)
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{value}
|
|
58
|
+
</button>
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
switch (filter.filterStyle) {
|
|
63
|
+
case 'tab bar':
|
|
64
|
+
return <section className='single-filters__tab-bar'>{Tabs}</section>
|
|
65
|
+
case 'tab-simple':
|
|
66
|
+
return <div className='tab-simple-container d-flex w-100'>{Tabs}</div>
|
|
67
|
+
case 'pill':
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
{Tabs.map((Tab, index) => (
|
|
71
|
+
<div className='pill__wrapper' key={`pill-${index}`}>
|
|
72
|
+
{Tab}
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
default:
|
|
78
|
+
return <>{Tabs}</>
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default Tabs
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
import { getQueryParams, updateQueryString } from '../../../helpers/queryStringUtils'
|
|
3
|
+
|
|
4
|
+
export const getChangedFilters = (filters, index, value, filterBehavior) => {
|
|
5
|
+
const newFilters = _.cloneDeep(filters)
|
|
6
|
+
const newFilter = newFilters[index]
|
|
7
|
+
if (filterBehavior === 'Apply Button') {
|
|
8
|
+
newFilter.queuedActive = value
|
|
9
|
+
} else {
|
|
10
|
+
if (newFilter.filterStyle !== 'nested-dropdown') {
|
|
11
|
+
newFilter.active = value
|
|
12
|
+
} else {
|
|
13
|
+
newFilter.active = value[0]
|
|
14
|
+
newFilter.subGrouping.active = value[1]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const queryParams = getQueryParams()
|
|
18
|
+
if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
|
|
19
|
+
queryParams[newFilter.setByQueryParameter] = newFilter.active
|
|
20
|
+
updateQueryString(queryParams)
|
|
21
|
+
}
|
|
22
|
+
if (
|
|
23
|
+
newFilter?.subGrouping?.setByQueryParameter &&
|
|
24
|
+
queryParams[newFilter?.subGrouping?.setByQueryParameter] !== newFilter?.subGrouping.active
|
|
25
|
+
) {
|
|
26
|
+
queryParams[newFilter?.subGrouping?.setByQueryParameter] = newFilter.subGrouping.active
|
|
27
|
+
updateQueryString(queryParams)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return newFilters
|
|
31
|
+
}
|
|
@@ -12,7 +12,7 @@ export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOpti
|
|
|
12
12
|
const filteredValues = orderedValues?.length
|
|
13
13
|
? orderedValues.filter(orderedValue => values.includes(orderedValue))
|
|
14
14
|
: values
|
|
15
|
-
const
|
|
15
|
+
const options: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
|
|
16
16
|
if (!subGrouping) return [[value], []]
|
|
17
17
|
const { orderedValues, values: filteredSubValues } = subGrouping.valuesLookup[value]
|
|
18
18
|
// keep custom subFilter order
|
|
@@ -25,5 +25,5 @@ export const getNestedOptions = ({ orderedValues, values, subGrouping }: GetOpti
|
|
|
25
25
|
return structuredNestedDropdownData
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
return
|
|
28
|
+
return options
|
|
29
29
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const getNewRuntime = (visualizationConfig, newFilteredData) => {
|
|
4
|
+
const runtime = _.cloneDeep(visualizationConfig.runtime) || {}
|
|
5
|
+
runtime.series = []
|
|
6
|
+
runtime.seriesLabels = {}
|
|
7
|
+
runtime.seriesLabelsAll = []
|
|
8
|
+
const { filters, columns, dynamicSeriesType, dynamicSeriesLineType, xAxis } = visualizationConfig
|
|
9
|
+
if (newFilteredData?.length) {
|
|
10
|
+
const columnNames = Object.keys(newFilteredData[0])
|
|
11
|
+
columnNames.forEach(colName => {
|
|
12
|
+
const isNotXAxis = xAxis.dataKey !== colName
|
|
13
|
+
const isNotFiltered = !filters || !filters?.find(filter => filter.columnName === colName)
|
|
14
|
+
const noColConfiguration = !columns || Object.keys(columns).indexOf(colName) === -1
|
|
15
|
+
if (isNotXAxis && isNotFiltered && noColConfiguration) {
|
|
16
|
+
runtime.series.push({
|
|
17
|
+
dataKey: colName,
|
|
18
|
+
type: dynamicSeriesType,
|
|
19
|
+
lineType: dynamicSeriesLineType,
|
|
20
|
+
tooltip: true
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
runtime.seriesKeys = runtime.series
|
|
27
|
+
? runtime.series.map(series => {
|
|
28
|
+
runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
|
|
29
|
+
runtime.seriesLabelsAll.push(series.name || series.dataKey)
|
|
30
|
+
return series.dataKey
|
|
31
|
+
})
|
|
32
|
+
: []
|
|
33
|
+
|
|
34
|
+
return runtime
|
|
35
|
+
}
|
|
@@ -17,8 +17,8 @@ export const handleSorting = singleFilter => {
|
|
|
17
17
|
return String(asc ? a : b).localeCompare(String(asc ? b : a), 'en', { numeric: true })
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
singleFilter.values = singleFilterValues
|
|
21
|
-
singleFilter.orderedValues = singleFilterValues
|
|
20
|
+
singleFilter.values = singleFilterValues?.sort(sort)
|
|
21
|
+
singleFilter.orderedValues = singleFilterValues?.sort(sort)
|
|
22
22
|
|
|
23
23
|
return singleFilter
|
|
24
24
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, vi, type Mock } from 'vitest'
|
|
2
|
+
import { getChangedFilters } from '../getChangedFilters'
|
|
3
|
+
import { getQueryParams, updateQueryString } from '../../../../helpers/queryStringUtils'
|
|
4
|
+
import _ from 'lodash'
|
|
5
|
+
|
|
6
|
+
vi.mock('../../../../helpers/queryStringUtils', () => ({
|
|
7
|
+
getQueryParams: vi.fn(),
|
|
8
|
+
updateQueryString: vi.fn()
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe('getChangedFilters', () => {
|
|
12
|
+
it('should update queuedActive when filterBehavior is "Apply Button"', () => {
|
|
13
|
+
const filters = [{ queuedActive: false }]
|
|
14
|
+
const index = 0
|
|
15
|
+
const value = true
|
|
16
|
+
const filterBehavior = 'Apply Button'
|
|
17
|
+
|
|
18
|
+
const result = getChangedFilters(filters, index, value, filterBehavior)
|
|
19
|
+
|
|
20
|
+
expect(result[index].queuedActive).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should update active for non-nested-dropdown filters', () => {
|
|
24
|
+
const filters = [{ filterStyle: 'dropdown', active: false }]
|
|
25
|
+
const index = 0
|
|
26
|
+
const value = true
|
|
27
|
+
const filterBehavior = 'Immediate'
|
|
28
|
+
|
|
29
|
+
const result = getChangedFilters(filters, index, value, filterBehavior)
|
|
30
|
+
|
|
31
|
+
expect(result[index].active).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should update active and subGrouping.active for nested-dropdown filters', () => {
|
|
35
|
+
const filters = [
|
|
36
|
+
{
|
|
37
|
+
filterStyle: 'nested-dropdown',
|
|
38
|
+
active: null,
|
|
39
|
+
subGrouping: { active: null }
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
const index = 0
|
|
43
|
+
const value = ['parentValue', 'childValue']
|
|
44
|
+
const filterBehavior = 'Immediate'
|
|
45
|
+
|
|
46
|
+
const result = getChangedFilters(filters, index, value, filterBehavior)
|
|
47
|
+
|
|
48
|
+
expect(result[index].active).toBe('parentValue')
|
|
49
|
+
expect(result[index].subGrouping.active).toBe('childValue')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should update query parameters when setByQueryParameter is defined', () => {
|
|
53
|
+
const filters = [
|
|
54
|
+
{
|
|
55
|
+
filterStyle: 'dropdown',
|
|
56
|
+
active: false,
|
|
57
|
+
setByQueryParameter: 'filterParam'
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
const index = 0
|
|
61
|
+
const value = true
|
|
62
|
+
const filterBehavior = 'Immediate'
|
|
63
|
+
|
|
64
|
+
;(getQueryParams as Mock).mockReturnValue({ filterParam: false })
|
|
65
|
+
|
|
66
|
+
getChangedFilters(filters, index, value, filterBehavior)
|
|
67
|
+
|
|
68
|
+
expect(updateQueryString).toHaveBeenCalledWith({ filterParam: true })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should update query parameters for subGrouping when setByQueryParameter is defined', () => {
|
|
72
|
+
const filters = [
|
|
73
|
+
{
|
|
74
|
+
filterStyle: 'nested-dropdown',
|
|
75
|
+
active: null,
|
|
76
|
+
subGrouping: {
|
|
77
|
+
active: null,
|
|
78
|
+
setByQueryParameter: 'subFilterParam'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
const index = 0
|
|
83
|
+
const value = ['parentValue', 'childValue']
|
|
84
|
+
const filterBehavior = 'Immediate'
|
|
85
|
+
|
|
86
|
+
;(getQueryParams as Mock).mockReturnValue({ subFilterParam: null })
|
|
87
|
+
|
|
88
|
+
getChangedFilters(filters, index, value, filterBehavior)
|
|
89
|
+
|
|
90
|
+
expect(updateQueryString).toHaveBeenCalledWith({ subFilterParam: 'childValue' })
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -90,4 +90,35 @@ describe('getNestedOptions', () => {
|
|
|
90
90
|
]
|
|
91
91
|
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
92
92
|
})
|
|
93
|
+
|
|
94
|
+
it('should return an empty array when values is an empty array', () => {
|
|
95
|
+
const params = {
|
|
96
|
+
values: [],
|
|
97
|
+
subGrouping: null
|
|
98
|
+
}
|
|
99
|
+
const expectedOutput: NestedOptions = []
|
|
100
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should handle values with a single element', () => {
|
|
104
|
+
const params = {
|
|
105
|
+
values: ['value1'],
|
|
106
|
+
subGrouping: null
|
|
107
|
+
}
|
|
108
|
+
const expectedOutput: NestedOptions = [[['value1'], []]]
|
|
109
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should handle values with multiple elements', () => {
|
|
113
|
+
const params = {
|
|
114
|
+
values: ['value1', 'value2', 'value3'],
|
|
115
|
+
subGrouping: null
|
|
116
|
+
}
|
|
117
|
+
const expectedOutput: NestedOptions = [
|
|
118
|
+
[['value1'], []],
|
|
119
|
+
[['value2'], []],
|
|
120
|
+
[['value3'], []]
|
|
121
|
+
]
|
|
122
|
+
expect(getNestedOptions(params)).toEqual(expectedOutput)
|
|
123
|
+
})
|
|
93
124
|
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getNewRuntime } from '../getNewRuntime'
|
|
3
|
+
|
|
4
|
+
describe('getNewRuntime', () => {
|
|
5
|
+
it('should return a runtime object with default values when no data is provided', () => {
|
|
6
|
+
const visualizationConfig = { runtime: {} }
|
|
7
|
+
const newFilteredData = null
|
|
8
|
+
|
|
9
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
10
|
+
|
|
11
|
+
expect(result.series).toEqual([])
|
|
12
|
+
expect(result.seriesLabels).toEqual({})
|
|
13
|
+
expect(result.seriesLabelsAll).toEqual([])
|
|
14
|
+
expect(result.seriesKeys).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should populate runtime.series with valid series from newFilteredData', () => {
|
|
18
|
+
const visualizationConfig = {
|
|
19
|
+
runtime: {},
|
|
20
|
+
filters: [],
|
|
21
|
+
columns: {},
|
|
22
|
+
dynamicSeriesType: 'bar',
|
|
23
|
+
dynamicSeriesLineType: 'solid',
|
|
24
|
+
xAxis: { dataKey: 'x' }
|
|
25
|
+
}
|
|
26
|
+
const newFilteredData = [
|
|
27
|
+
{ x: 1, y: 10, z: 20 },
|
|
28
|
+
{ x: 2, y: 15, z: 25 }
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
32
|
+
|
|
33
|
+
expect(result.series).toEqual([
|
|
34
|
+
{ dataKey: 'y', type: 'bar', lineType: 'solid', tooltip: true },
|
|
35
|
+
{ dataKey: 'z', type: 'bar', lineType: 'solid', tooltip: true }
|
|
36
|
+
])
|
|
37
|
+
expect(result.seriesKeys).toEqual(['y', 'z'])
|
|
38
|
+
expect(result.seriesLabels).toEqual({ y: 'y', z: 'z' })
|
|
39
|
+
expect(result.seriesLabelsAll).toEqual(['y', 'z'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should exclude series keys that match filters or columns', () => {
|
|
43
|
+
const visualizationConfig = {
|
|
44
|
+
runtime: {},
|
|
45
|
+
filters: [{ columnName: 'y' }],
|
|
46
|
+
columns: { z: {} },
|
|
47
|
+
dynamicSeriesType: 'bar',
|
|
48
|
+
dynamicSeriesLineType: 'solid',
|
|
49
|
+
xAxis: { dataKey: 'x' }
|
|
50
|
+
}
|
|
51
|
+
const newFilteredData = [
|
|
52
|
+
{ x: 1, y: 10, z: 20, w: 30 },
|
|
53
|
+
{ x: 2, y: 15, z: 25, w: 35 }
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
57
|
+
|
|
58
|
+
expect(result.series).toEqual([{ dataKey: 'w', type: 'bar', lineType: 'solid', tooltip: true }])
|
|
59
|
+
expect(result.seriesKeys).toEqual(['w'])
|
|
60
|
+
expect(result.seriesLabels).toEqual({ w: 'w' })
|
|
61
|
+
expect(result.seriesLabelsAll).toEqual(['w'])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should handle empty newFilteredData gracefully', () => {
|
|
65
|
+
const visualizationConfig = {
|
|
66
|
+
runtime: {},
|
|
67
|
+
filters: [],
|
|
68
|
+
columns: {},
|
|
69
|
+
dynamicSeriesType: 'bar',
|
|
70
|
+
dynamicSeriesLineType: 'solid',
|
|
71
|
+
xAxis: { dataKey: 'x' }
|
|
72
|
+
}
|
|
73
|
+
const newFilteredData = []
|
|
74
|
+
|
|
75
|
+
const result = getNewRuntime(visualizationConfig, newFilteredData)
|
|
76
|
+
|
|
77
|
+
expect(result.series).toEqual([])
|
|
78
|
+
expect(result.seriesKeys).toEqual([])
|
|
79
|
+
expect(result.seriesLabels).toEqual({})
|
|
80
|
+
expect(result.seriesLabelsAll).toEqual([])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -6,17 +6,18 @@ 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
8
|
import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
|
|
9
|
+
import { MapConfig } from '@cdc/map/src/types/MapConfig'
|
|
9
10
|
|
|
10
11
|
type VisualizationWrapper = {
|
|
11
12
|
children: React.ReactNode
|
|
12
|
-
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters
|
|
13
|
+
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters | MapConfig
|
|
13
14
|
currentViewport?: string
|
|
14
15
|
imageId?: string
|
|
15
16
|
isEditor: boolean
|
|
16
17
|
showEditorPanel?: boolean
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const Visualization
|
|
20
|
+
const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, ref) => {
|
|
20
21
|
const {
|
|
21
22
|
config = {},
|
|
22
23
|
isEditor = false,
|
|
@@ -93,7 +94,6 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
return (
|
|
96
|
-
// prettier-ignore
|
|
97
97
|
<div
|
|
98
98
|
{...(config.type === 'chart' ? { 'data-lollipop': config.isLollipopChart } : {})}
|
|
99
99
|
className={getWrappingClasses().join(' ')}
|
|
@@ -4,9 +4,11 @@ import { type MapConfig } from '@cdc/map/src/types/MapConfig'
|
|
|
4
4
|
import { type ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
5
5
|
import { getTextWidth } from '../../helpers/getTextWidth'
|
|
6
6
|
import { DimensionsType } from '../../types/Dimensions'
|
|
7
|
+
import useLegendSeparators from '@cdc/map/src/hooks/useLegendSeparators'
|
|
7
8
|
|
|
8
9
|
const MARGIN = 1
|
|
9
10
|
const BORDER_SIZE = 1
|
|
11
|
+
const BORDER_COLOR = '#d3d3d3'
|
|
10
12
|
const MOBILE_BREAKPOINT = 576
|
|
11
13
|
|
|
12
14
|
type CombinedConfig = MapConfig | ChartConfig
|
|
@@ -27,7 +29,7 @@ const LegendGradient = ({
|
|
|
27
29
|
parentPaddingToSubtract = 0
|
|
28
30
|
}: GradientProps): JSX.Element => {
|
|
29
31
|
const { uid, legend, type } = config
|
|
30
|
-
const { tickRotation, position, style, subStyle,
|
|
32
|
+
const { tickRotation, position, style, subStyle, separators } = legend
|
|
31
33
|
|
|
32
34
|
const isLinearBlocks = subStyle === 'linear blocks'
|
|
33
35
|
let [width] = dimensions
|
|
@@ -36,6 +38,10 @@ const LegendGradient = ({
|
|
|
36
38
|
const legendWidth = Number(width) - parentPaddingToSubtract - MARGIN * 2 - BORDER_SIZE * 2
|
|
37
39
|
const uniqueID = `${uid}-${Date.now()}`
|
|
38
40
|
|
|
41
|
+
// Legend separators logic
|
|
42
|
+
const { legendSeparators, separatorSize, legendSeparatorsToSubtract, getTickSeparatorsAdjustment } =
|
|
43
|
+
useLegendSeparators(separators, legendWidth, isLinearBlocks)
|
|
44
|
+
|
|
39
45
|
const numTicks = colors?.length
|
|
40
46
|
|
|
41
47
|
const longestLabel = (labels || []).reduce((a: string, b) => (a.length > String(b).length ? a : b), '')
|
|
@@ -57,8 +63,8 @@ const LegendGradient = ({
|
|
|
57
63
|
|
|
58
64
|
// render ticks and labels
|
|
59
65
|
const ticks = labels.map((key, index) => {
|
|
60
|
-
const segmentWidth = legendWidth / numTicks
|
|
61
|
-
const xPositionX = index * segmentWidth + segmentWidth + MARGIN
|
|
66
|
+
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
67
|
+
const xPositionX = index * segmentWidth + segmentWidth + MARGIN + getTickSeparatorsAdjustment(index)
|
|
62
68
|
const textAnchor = rotationAngle ? 'end' : 'middle'
|
|
63
69
|
const verticalAnchor = rotationAngle ? 'middle' : 'start'
|
|
64
70
|
const lastTick = index === labels.length - 1
|
|
@@ -94,7 +100,7 @@ const LegendGradient = ({
|
|
|
94
100
|
return (
|
|
95
101
|
<svg className={'w-100 overflow-visible'} height={newHeight}>
|
|
96
102
|
{/* background border*/}
|
|
97
|
-
<rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill=
|
|
103
|
+
<rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill={BORDER_COLOR} />
|
|
98
104
|
{/* Define the gradient */}
|
|
99
105
|
<linearGradient id={`gradient-smooth-${uniqueID}`} x1='0%' y1='0%' x2='100%' y2='0%'>
|
|
100
106
|
{stops}
|
|
@@ -110,25 +116,62 @@ const LegendGradient = ({
|
|
|
110
116
|
/>
|
|
111
117
|
)}
|
|
112
118
|
|
|
113
|
-
{subStyle === 'linear blocks' &&
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
119
|
+
{subStyle === 'linear blocks' && (
|
|
120
|
+
<>
|
|
121
|
+
{colors.map((color, index) => {
|
|
122
|
+
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
123
|
+
const xPosition = index * segmentWidth + MARGIN + getTickSeparatorsAdjustment(index)
|
|
124
|
+
return (
|
|
125
|
+
<Group>
|
|
126
|
+
<rect
|
|
127
|
+
key={index}
|
|
128
|
+
x={xPosition}
|
|
129
|
+
y={MARGIN}
|
|
130
|
+
width={segmentWidth}
|
|
131
|
+
height={boxHeight}
|
|
132
|
+
fill={color}
|
|
133
|
+
stroke='white'
|
|
134
|
+
strokeWidth='0'
|
|
135
|
+
/>
|
|
136
|
+
</Group>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
{/* Legend separators */}
|
|
140
|
+
{legendSeparators.map((separatorAfter, index) => {
|
|
141
|
+
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
142
|
+
const xPosition = separatorAfter * segmentWidth + MARGIN + getTickSeparatorsAdjustment(separatorAfter - 1)
|
|
143
|
+
return (
|
|
144
|
+
<Group>
|
|
145
|
+
{/* Separators block */}
|
|
146
|
+
<rect
|
|
147
|
+
key={index}
|
|
148
|
+
x={xPosition}
|
|
149
|
+
y={MARGIN / 2}
|
|
150
|
+
width={separatorSize}
|
|
151
|
+
height={boxHeight + MARGIN}
|
|
152
|
+
fill={'white'}
|
|
153
|
+
stroke={'white'}
|
|
154
|
+
strokeWidth={MARGIN}
|
|
155
|
+
/>
|
|
156
|
+
|
|
157
|
+
{/* Dotted dividing line */}
|
|
158
|
+
<line
|
|
159
|
+
key={index}
|
|
160
|
+
x1={xPosition + separatorSize / 2}
|
|
161
|
+
x2={xPosition + separatorSize / 2}
|
|
162
|
+
y1={-3}
|
|
163
|
+
y2={boxHeight + MARGIN + 3}
|
|
164
|
+
stroke={'var(--colors-gray-cool-40'}
|
|
165
|
+
strokeWidth={1}
|
|
166
|
+
strokeDasharray='5,3'
|
|
167
|
+
strokeDashoffset={1}
|
|
168
|
+
/>
|
|
169
|
+
</Group>
|
|
170
|
+
)
|
|
171
|
+
})}
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
|
|
132
175
|
{/* Ticks and labels */}
|
|
133
176
|
<g>{ticks}</g>
|
|
134
177
|
</svg>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
.wrapper {
|
|
6
6
|
display: inline-flex;
|
|
7
7
|
align-items: center;
|
|
8
|
+
width: 100%;
|
|
8
9
|
.selected {
|
|
9
10
|
&[aria-disabled='true'] {
|
|
10
11
|
background: var(--lightestGray);
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
border: 1px solid var(--lightGray);
|
|
13
14
|
padding: 7px;
|
|
14
15
|
min-width: 200px;
|
|
16
|
+
width: 100%;
|
|
15
17
|
display: inline-block;
|
|
16
18
|
:is(button) {
|
|
17
19
|
border: none;
|
|
@@ -260,7 +260,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
260
260
|
onKeyUp={handleKeyUp}
|
|
261
261
|
>
|
|
262
262
|
<div
|
|
263
|
-
className={`nested-dropdown-input-container${loading || !options
|
|
263
|
+
className={`nested-dropdown-input-container${loading || !options?.length ? ' disabled' : ''}`}
|
|
264
264
|
aria-label='searchInput'
|
|
265
265
|
aria-disabled={loading}
|
|
266
266
|
role='textbox'
|
|
@@ -276,7 +276,7 @@ const NestedDropdown: React.FC<NestedDropdownProps> = ({
|
|
|
276
276
|
value={userSearchTerm !== null ? userSearchTerm : inputValue}
|
|
277
277
|
onChange={handleSearchTermChange}
|
|
278
278
|
placeholder={loading ? 'Loading...' : '- Select -'}
|
|
279
|
-
disabled={loading || !options
|
|
279
|
+
disabled={loading || !options?.length}
|
|
280
280
|
onClick={() => {
|
|
281
281
|
if (inputHasFocus) setIsListOpened(!isListOpened)
|
|
282
282
|
}}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Tooltip as ReactTooltip } from 'react-tooltip'
|
|
3
|
+
import './richTooltip.css'
|
|
4
|
+
|
|
5
|
+
const RichTooltip = ({ linkText, href = null, tooltipOpacity = 100, tooltipContent }) => {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<a
|
|
9
|
+
className='tooltip-link'
|
|
10
|
+
data-tooltip-content={tooltipContent}
|
|
11
|
+
data-tooltip-id='supression-tooltip'
|
|
12
|
+
href={href}
|
|
13
|
+
>
|
|
14
|
+
{linkText}
|
|
15
|
+
</a>
|
|
16
|
+
|
|
17
|
+
<ReactTooltip
|
|
18
|
+
id='supression-tooltip'
|
|
19
|
+
place='top'
|
|
20
|
+
effect='solid'
|
|
21
|
+
variant='light'
|
|
22
|
+
style={{
|
|
23
|
+
background: `rgba(255, 255, 255, ${tooltipOpacity})`,
|
|
24
|
+
color: 'var(--cool-gray-90)',
|
|
25
|
+
padding: '9px 18px',
|
|
26
|
+
boxShadow: '0px 2px 2px rgba(28, 29, 31, 0.45)',
|
|
27
|
+
maxWidth: '239px',
|
|
28
|
+
fontSize: 'var(--filter-label-font-size)',
|
|
29
|
+
fontFamily: 'var(--app-font-main)',
|
|
30
|
+
borderRadius: '4px'
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default RichTooltip
|