@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.
Files changed (69) hide show
  1. package/assets/icon-close.svg +1 -1
  2. package/components/DataTable/DataTable.tsx +18 -13
  3. package/components/DataTable/components/CellAnchor.tsx +1 -1
  4. package/components/DataTable/components/ChartHeader.tsx +2 -1
  5. package/components/DataTable/components/MapHeader.tsx +1 -0
  6. package/components/DataTable/helpers/chartCellMatrix.tsx +2 -1
  7. package/components/DataTable/helpers/mapCellMatrix.tsx +17 -7
  8. package/components/DownloadButton.tsx +17 -2
  9. package/components/EditorPanel/DataTableEditor.tsx +1 -1
  10. package/components/EditorPanel/Inputs.tsx +12 -4
  11. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +2 -1
  12. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +3 -1
  13. package/components/Filters/Filters.tsx +168 -429
  14. package/components/Filters/components/Dropdown.tsx +39 -0
  15. package/components/Filters/components/Tabs.tsx +82 -0
  16. package/components/Filters/helpers/getChangedFilters.ts +31 -0
  17. package/components/Filters/helpers/getNestedOptions.ts +2 -2
  18. package/components/Filters/helpers/getNewRuntime.ts +35 -0
  19. package/components/Filters/helpers/handleSorting.ts +2 -2
  20. package/components/Filters/helpers/tests/getChangedFilters.test.ts +92 -0
  21. package/components/Filters/helpers/tests/getNestedOptions.test.ts +31 -0
  22. package/components/Filters/helpers/tests/getNewRuntime.test.ts +82 -0
  23. package/components/Filters/index.ts +1 -1
  24. package/components/Layout/components/Visualization/index.tsx +3 -3
  25. package/components/Legend/Legend.Gradient.tsx +66 -23
  26. package/components/MultiSelect/multiselect.styles.css +2 -0
  27. package/components/NestedDropdown/NestedDropdown.tsx +2 -2
  28. package/components/RichTooltip/RichTooltip.tsx +37 -0
  29. package/components/RichTooltip/richTooltip.css +16 -0
  30. package/components/Table/Table.tsx +142 -142
  31. package/components/Table/components/Row.tsx +1 -1
  32. package/components/Table/table.styles.css +10 -0
  33. package/components/_stories/DataTable.stories.tsx +9 -2
  34. package/components/_stories/Table.stories.tsx +1 -1
  35. package/components/_stories/styles.scss +0 -4
  36. package/components/ui/Accordion.jsx +8 -1
  37. package/components/ui/Title/index.tsx +4 -1
  38. package/components/ui/Title/{Title.scss → title.styles.css} +0 -2
  39. package/components/ui/_stories/Colors.stories.mdx +220 -0
  40. package/components/ui/_stories/IconGallery.stories.mdx +14 -0
  41. package/components/ui/_stories/Title.stories.tsx +29 -4
  42. package/components/ui/accordion.styles.css +3 -0
  43. package/data/colorPalettes.js +0 -1
  44. package/dist/cove-main.css +3 -8
  45. package/dist/cove-main.css.map +1 -1
  46. package/helpers/constants.ts +6 -0
  47. package/helpers/cove/accessibility.ts +7 -8
  48. package/helpers/coveUpdateWorker.ts +5 -1
  49. package/helpers/filterOrderOptions.ts +17 -0
  50. package/helpers/isNumber.ts +20 -0
  51. package/helpers/isRightAlignedTableValue.js +1 -1
  52. package/helpers/pivotData.ts +16 -11
  53. package/helpers/tests/pivotData.test.ts +74 -0
  54. package/helpers/ver/4.25.3.ts +25 -2
  55. package/helpers/ver/4.25.4.ts +33 -0
  56. package/helpers/ver/tests/4.25.4.test.ts +24 -0
  57. package/helpers/viewports.ts +4 -0
  58. package/package.json +2 -3
  59. package/styles/_global-variables.scss +3 -0
  60. package/styles/_reset.scss +0 -6
  61. package/styles/v2/main.scss +0 -5
  62. package/types/General.ts +1 -0
  63. package/types/Legend.ts +1 -0
  64. package/LICENSE +0 -201
  65. package/components/ui/_stories/Colors.stories.tsx +0 -92
  66. package/components/ui/_stories/Icon.stories.tsx +0 -29
  67. package/helpers/cove/fontSettings.ts +0 -2
  68. package/helpers/isNumber.js +0 -24
  69. 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 v: NestedOptions = filteredValues.map<[ValueTextPair, ValueTextPair[]]>(value => {
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 v
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.sort(sort)
21
- singleFilter.orderedValues = singleFilterValues.sort(sort)
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
+ })
@@ -1,5 +1,5 @@
1
1
  export { default } from './Filters'
2
2
 
3
- export { filterOrderOptions, filterStyleOptions, useFilters } from './Filters'
3
+ export { filterStyleOptions } from './Filters'
4
4
 
5
5
  export { handleSorting } from './helpers/handleSorting'
@@ -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: React.FC<VisualizationWrapper> = forwardRef((props, ref) => {
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, hideBorder } = legend
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='#d3d3d3' />
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
- colors.map((color, index) => {
115
- const segmentWidth = legendWidth / numTicks
116
- const xPosition = index * segmentWidth + MARGIN
117
- return (
118
- <Group>
119
- <rect
120
- key={index}
121
- x={xPosition}
122
- y={MARGIN}
123
- width={segmentWidth}
124
- height={boxHeight}
125
- fill={color}
126
- stroke='white'
127
- strokeWidth='0'
128
- />
129
- </Group>
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.length ? ' disabled' : ''}`}
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.length}
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