@cdc/core 4.25.3 → 4.25.6-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/Alert/components/Alert.tsx +1 -1
- package/components/DataTable/DataTable.tsx +18 -16
- package/components/DataTable/DataTableStandAlone.tsx +15 -9
- package/components/DataTable/components/CellAnchor.tsx +1 -1
- package/components/DataTable/components/ChartHeader.tsx +8 -5
- package/components/DataTable/components/DataTableEditorPanel.tsx +25 -3
- package/components/DataTable/components/MapHeader.tsx +1 -0
- package/components/DataTable/helpers/chartCellMatrix.tsx +14 -10
- package/components/DataTable/helpers/getChartCellValue.ts +42 -26
- package/components/DataTable/helpers/mapCellMatrix.tsx +25 -7
- package/components/DownloadButton.tsx +17 -2
- package/components/EditorPanel/DataTableEditor.tsx +1 -1
- package/components/EditorPanel/FootnotesEditor.tsx +76 -22
- package/components/EditorPanel/Inputs.tsx +12 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +3 -2
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +51 -35
- package/components/Filters/Filters.tsx +158 -461
- 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/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/index.ts +1 -1
- package/components/Footnotes/Footnotes.tsx +1 -1
- package/components/Footnotes/FootnotesStandAlone.tsx +8 -33
- package/components/Layout/components/Visualization/index.tsx +4 -3
- package/components/Legend/Legend.Gradient.tsx +68 -24
- package/components/MultiSelect/MultiSelect.tsx +3 -6
- package/components/MultiSelect/multiselect.styles.css +2 -0
- package/components/NestedDropdown/NestedDropdown.tsx +21 -21
- 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/cove/number.ts +5 -3
- package/helpers/coveUpdateWorker.ts +9 -1
- package/helpers/filterOrderOptions.ts +17 -0
- package/helpers/formatConfigBeforeSave.ts +19 -32
- 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/updateFieldFactory.ts +1 -0
- package/helpers/ver/4.25.3.ts +25 -2
- package/helpers/ver/4.25.4.ts +110 -0
- package/helpers/ver/4.25.6.ts +36 -0
- package/helpers/ver/4.25.7.ts +26 -0
- package/helpers/ver/tests/4.25.4.test.ts +89 -0
- package/helpers/ver/tests/4.25.6.test.ts +84 -0
- package/helpers/viewports.ts +4 -0
- package/package.json +7 -6
- package/styles/_global-variables.scss +3 -0
- package/styles/_global.scss +0 -4
- package/styles/_reset.scss +0 -6
- package/styles/filters.scss +0 -4
- package/styles/v2/main.scss +0 -5
- package/types/Axis.ts +2 -0
- package/types/DataSet.ts +14 -0
- package/types/Footnotes.ts +5 -2
- package/types/General.ts +1 -0
- package/types/Legend.ts +1 -0
- package/types/Table.ts +1 -0
- package/types/Visualization.ts +3 -12
- package/types/VizFilter.ts +3 -0
- 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
- /package/helpers/{fetchRemoteData.js → fetchRemoteData.ts} +0 -0
|
@@ -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' }}
|
|
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
|
}
|
|
@@ -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
|
})
|
|
@@ -11,7 +11,7 @@ const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
|
|
|
11
11
|
<ul className='cove-footnotes'>
|
|
12
12
|
{footnotes.map((note, i) => {
|
|
13
13
|
return (
|
|
14
|
-
<li key={note.symbol
|
|
14
|
+
<li key={`${note.symbol || 'footnote-'}${i}`} className='mb-1'>
|
|
15
15
|
{note.symbol && <span className='me-1'>{note.symbol}</span>}
|
|
16
16
|
{note.text}
|
|
17
17
|
</li>
|
|
@@ -1,45 +1,20 @@
|
|
|
1
|
-
import EditorWrapper from '../EditorWrapper'
|
|
2
1
|
import Footnotes from './Footnotes'
|
|
3
|
-
import
|
|
4
|
-
import { ViewPort } from '../../types/ViewPort'
|
|
5
|
-
import FootnotesConfig, { Footnote } from '../../types/Footnotes'
|
|
2
|
+
import FootnotesConfig from '../../types/Footnotes'
|
|
6
3
|
import _ from 'lodash'
|
|
7
4
|
import { useMemo } from 'react'
|
|
8
|
-
import {
|
|
5
|
+
import { filterVizData } from '../../helpers/filterVizData'
|
|
6
|
+
import { VizFilter } from '../../types/VizFilter'
|
|
9
7
|
|
|
10
8
|
type StandAloneProps = {
|
|
11
|
-
isEditor?: boolean
|
|
12
|
-
visualizationKey: string
|
|
13
9
|
config: FootnotesConfig
|
|
14
|
-
|
|
15
|
-
viewport?: ViewPort
|
|
10
|
+
filters?: VizFilter[]
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
const FootnotesStandAlone: React.FC<StandAloneProps> = ({
|
|
19
|
-
|
|
20
|
-
config,
|
|
21
|
-
viewport,
|
|
22
|
-
isEditor,
|
|
23
|
-
updateConfig
|
|
24
|
-
}) => {
|
|
25
|
-
const updateField = updateFieldFactory<Footnote[]>(config, updateConfig)
|
|
26
|
-
if (isEditor)
|
|
27
|
-
return (
|
|
28
|
-
<EditorWrapper
|
|
29
|
-
component={FootnotesStandAlone}
|
|
30
|
-
visualizationKey={visualizationKey}
|
|
31
|
-
visualizationConfig={config}
|
|
32
|
-
updateConfig={updateConfig}
|
|
33
|
-
type={'Footnotes'}
|
|
34
|
-
viewport={viewport}
|
|
35
|
-
>
|
|
36
|
-
<FootnotesEditor key={visualizationKey} config={config} updateField={updateField} />
|
|
37
|
-
</EditorWrapper>
|
|
38
|
-
)
|
|
39
|
-
|
|
13
|
+
const FootnotesStandAlone: React.FC<StandAloneProps> = ({ config, filters }) => {
|
|
14
|
+
if (!config) return null
|
|
40
15
|
// get the api footnotes from the config
|
|
41
16
|
const apiFootnotes = useMemo(() => {
|
|
42
|
-
const configData =
|
|
17
|
+
const configData = filterVizData(filters, config.data)
|
|
43
18
|
if (configData && config.dataKey && config.dynamicFootnotes) {
|
|
44
19
|
const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
|
|
45
20
|
const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
|
|
@@ -47,7 +22,7 @@ const FootnotesStandAlone: React.FC<StandAloneProps> = ({
|
|
|
47
22
|
return _data.map(row => ({ symbol: row[symbolColumn], text: row[textColumn] }))
|
|
48
23
|
}
|
|
49
24
|
return []
|
|
50
|
-
}, [config.dynamicFootnotes, config.
|
|
25
|
+
}, [config.dynamicFootnotes, config.data, filters])
|
|
51
26
|
|
|
52
27
|
// get static footnotes from the config.footnotes
|
|
53
28
|
const staticFootnotes = config.staticFootnotes || []
|
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
3
3
|
import React, { forwardRef } from 'react'
|
|
4
4
|
import { Config as DataBiteConfig } from '@cdc/data-bite/src/types/Config'
|
|
5
|
+
import { Config as DataTableConfig } from '@cdc/data-table/src/types/Config'
|
|
5
6
|
import './visualizations.scss'
|
|
6
7
|
import { Config as WaffleChartConfig } from '@cdc/waffle-chart/src/types/Config'
|
|
7
8
|
import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
|
|
8
9
|
import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
|
|
10
|
+
import { MapConfig } from '@cdc/map/src/types/MapConfig'
|
|
9
11
|
|
|
10
12
|
type VisualizationWrapper = {
|
|
11
13
|
children: React.ReactNode
|
|
12
|
-
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters
|
|
14
|
+
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters | MapConfig | DataTableConfig
|
|
13
15
|
currentViewport?: string
|
|
14
16
|
imageId?: string
|
|
15
17
|
isEditor: boolean
|
|
16
18
|
showEditorPanel?: boolean
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const Visualization
|
|
21
|
+
const Visualization = forwardRef<HTMLDivElement, VisualizationWrapper>((props, ref) => {
|
|
20
22
|
const {
|
|
21
23
|
config = {},
|
|
22
24
|
isEditor = false,
|
|
@@ -93,7 +95,6 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
return (
|
|
96
|
-
// prettier-ignore
|
|
97
98
|
<div
|
|
98
99
|
{...(config.type === 'chart' ? { 'data-lollipop': config.isLollipopChart } : {})}
|
|
99
100
|
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
|
|
@@ -92,9 +98,10 @@ const LegendGradient = ({
|
|
|
92
98
|
|
|
93
99
|
if (style === 'gradient') {
|
|
94
100
|
return (
|
|
95
|
-
|
|
101
|
+
// TODO: figure out why bootstrap 'overflow: visible' is not working consistently
|
|
102
|
+
<svg className={'w-100 overflow-visible'} height={newHeight} style={{ overflow: 'visible' }} width={width}>
|
|
96
103
|
{/* background border*/}
|
|
97
|
-
<rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill=
|
|
104
|
+
<rect x={0} y={0} width={legendWidth + MARGIN * 2} height={boxHeight + MARGIN * 2} fill={BORDER_COLOR} />
|
|
98
105
|
{/* Define the gradient */}
|
|
99
106
|
<linearGradient id={`gradient-smooth-${uniqueID}`} x1='0%' y1='0%' x2='100%' y2='0%'>
|
|
100
107
|
{stops}
|
|
@@ -110,25 +117,62 @@ const LegendGradient = ({
|
|
|
110
117
|
/>
|
|
111
118
|
)}
|
|
112
119
|
|
|
113
|
-
{subStyle === 'linear blocks' &&
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
{subStyle === 'linear blocks' && (
|
|
121
|
+
<>
|
|
122
|
+
{colors.map((color, index) => {
|
|
123
|
+
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
124
|
+
const xPosition = index * segmentWidth + MARGIN + getTickSeparatorsAdjustment(index)
|
|
125
|
+
return (
|
|
126
|
+
<Group>
|
|
127
|
+
<rect
|
|
128
|
+
key={index}
|
|
129
|
+
x={xPosition}
|
|
130
|
+
y={MARGIN}
|
|
131
|
+
width={segmentWidth}
|
|
132
|
+
height={boxHeight}
|
|
133
|
+
fill={color}
|
|
134
|
+
stroke='white'
|
|
135
|
+
strokeWidth='0'
|
|
136
|
+
/>
|
|
137
|
+
</Group>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
{/* Legend separators */}
|
|
141
|
+
{legendSeparators.map((separatorAfter, index) => {
|
|
142
|
+
const segmentWidth = (legendWidth - legendSeparatorsToSubtract) / numTicks
|
|
143
|
+
const xPosition = separatorAfter * segmentWidth + MARGIN + getTickSeparatorsAdjustment(separatorAfter - 1)
|
|
144
|
+
return (
|
|
145
|
+
<Group>
|
|
146
|
+
{/* Separators block */}
|
|
147
|
+
<rect
|
|
148
|
+
key={index}
|
|
149
|
+
x={xPosition}
|
|
150
|
+
y={MARGIN / 2}
|
|
151
|
+
width={separatorSize}
|
|
152
|
+
height={boxHeight + MARGIN}
|
|
153
|
+
fill={'white'}
|
|
154
|
+
stroke={'white'}
|
|
155
|
+
strokeWidth={MARGIN}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{/* Dotted dividing line */}
|
|
159
|
+
<line
|
|
160
|
+
key={index}
|
|
161
|
+
x1={xPosition + separatorSize / 2}
|
|
162
|
+
x2={xPosition + separatorSize / 2}
|
|
163
|
+
y1={-3}
|
|
164
|
+
y2={boxHeight + MARGIN + 3}
|
|
165
|
+
stroke={'var(--colors-gray-cool-40,#8d9297)'}
|
|
166
|
+
strokeWidth={1}
|
|
167
|
+
strokeDasharray='5,3'
|
|
168
|
+
strokeDashoffset={1}
|
|
169
|
+
/>
|
|
170
|
+
</Group>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
|
|
132
176
|
{/* Ticks and labels */}
|
|
133
177
|
<g>{ticks}</g>
|
|
134
178
|
</svg>
|
|
@@ -36,9 +36,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
|
36
36
|
tooltip,
|
|
37
37
|
loading
|
|
38
38
|
}) => {
|
|
39
|
-
const
|
|
40
|
-
const [selectedItems, setSelectedItems] = useState<Option[]>()
|
|
41
|
-
const items = selectedItems || preselectedItems
|
|
39
|
+
const items = useMemo(() => options.filter(opt => selected.includes(opt.value)).slice(0, limit), [options])
|
|
42
40
|
const [expanded, setExpanded] = useState(false)
|
|
43
41
|
const multiSelectRef = useRef(null)
|
|
44
42
|
|
|
@@ -68,14 +66,12 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
|
68
66
|
if (e && e.type === 'keyup' && e.key !== 'Enter') return
|
|
69
67
|
if (limit && items?.length >= limit) return
|
|
70
68
|
const newItems = [...items, option]
|
|
71
|
-
setSelectedItems(newItems)
|
|
72
69
|
update(newItems)
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
const handleItemRemove = (option: Option, e = null) => {
|
|
76
73
|
if (e && e.type === 'keyup' && e.key !== 'Enter') return
|
|
77
74
|
const newItems = items.filter(item => item.value !== option.value)
|
|
78
|
-
setSelectedItems(newItems)
|
|
79
75
|
update(newItems)
|
|
80
76
|
}
|
|
81
77
|
|
|
@@ -99,6 +95,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
|
99
95
|
items.map(item => (
|
|
100
96
|
<div key={item.value} aria-labelledby={label ? multiID + label : undefined}>
|
|
101
97
|
{item.label}
|
|
98
|
+
|
|
102
99
|
<button
|
|
103
100
|
aria-label='Remove'
|
|
104
101
|
onClick={e => {
|
|
@@ -114,7 +111,7 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
|
114
111
|
</div>
|
|
115
112
|
))
|
|
116
113
|
) : (
|
|
117
|
-
<span className='
|
|
114
|
+
<span className='ps-1 pt-1'>{loading ? 'Loading...' : '- Select -'}</span>
|
|
118
115
|
)}
|
|
119
116
|
<button
|
|
120
117
|
aria-label={expanded ? 'Collapse' : 'Expand'}
|
|
@@ -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;
|