Skip to content

Feature : [DEV-50357] - Add Grouped Legend to the Maps #2076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/types/Legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export type Legend = {
singleRow: boolean
type: string
verticalSorted: boolean
groupBy: string
}
7 changes: 2 additions & 5 deletions packages/map/examples/default-hex.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,7 @@
"showBubbleZeros": false
},
"mapPosition": {
"coordinates": [
0,
30
],
"coordinates": [0, 30],
"zoom": 1
},
"map": {
Expand Down Expand Up @@ -569,4 +566,4 @@
},
"usingWidgetLoader": true,
"validated": 4.23
}
}
6 changes: 3 additions & 3 deletions packages/map/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@

<body>
<!-- DEFAULT EXAMPLES -->
<div class="react-container" data-config="/examples/private/wastewatermap.json"></div>
<div class="react-container" data-config="/examples/example-hex-map-with-filter.json"></div>
<!-- <div class="react-container" data-config="/examples/private/wastewatermap.json"></div>
<div class="react-container" data-config="/examples/example-hex-map-with-filter.json"></div> -->

<!-- <div class="react-container" data-config="/examples/hex-colors.json"></div> -->
<!-- <div class="react-container react-container--maps" data-config="/examples/annotation/index.json">/</div> -->
Expand Down Expand Up @@ -59,7 +59,7 @@
<!-- data-config="https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/examples/Scale-Based-Categorical-Map-With-Special-Classes.json"-->
<!-- ></div>-->
<!-- <div class="react-container react-container--maps" data-config="/examples/private/world-map.json"></div> -->
<!-- <div class="react-container react-container--maps" data-config="/examples/default-hex.json"></div> -->
<div class="react-container react-container--maps" data-config="/examples/default-hex.json"></div>
<!-- <div class="react-container react-container--maps" data-config="/examples/default-hex.json"></div> -->
<!-- <div class="react-container react-container&#45;&#45;maps" data-config="/examples/hex-with-arrows.json"></div>-->

Expand Down
1 change: 1 addition & 0 deletions packages/map/src/CdcMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ const CdcMap = ({

// Legend - Update when runtimeData does
const legend = generateRuntimeLegend(state, runtimeData, hashLegend)

setRuntimeLegend(legend)
}, [
runtimeData,
Expand Down
9 changes: 9 additions & 0 deletions packages/map/src/_stories/CdcMap.Legend.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,12 @@ export const Legend_Bottom_Single_Row: Story = {
])
}
}
export const Legend_Grouped: Story = {
args: {
config: editConfigKeys(WastewaterMap, [
{ path: ['legend', 'position'], value: 'side' },
{ path: ['legend', 'style'], value: 'boxes' },
{ path: ['legend', 'groupBy'], value: 'pathogen' }
])
}
}
20 changes: 20 additions & 0 deletions packages/map/src/components/EditorPanel/components/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,15 @@ const EditorPanel = () => {
}
})
break
case 'legendGroupBy':
setState({
...state,
legend: {
...state.legend,
groupBy: value
}
})
break
case 'legendSubStyle':
setState({
...state,
Expand Down Expand Up @@ -2315,6 +2324,17 @@ const EditorPanel = () => {
}}
/>
)}

{'navigation' !== state.general.type && state.legend.type === 'category' && (
<Select
label='Legend Group By :'
value={legend.groupBy || ''}
options={columnsOptions.map(c => c.key)}
onChange={event => {
handleEditorChanges('legendGroupBy', event.target.value)
}}
/>
)}
{'navigation' !== state.general.type && state.legend.style === 'gradient' && (
<label>
<span className='edit-label'>Gradient Style</span>
Expand Down
6 changes: 4 additions & 2 deletions packages/map/src/components/Legend/components/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import LegendShape from '@cdc/core/components/LegendShape'
import LegendGradient from '@cdc/core/components/Legend/Legend.Gradient'
import LegendItemHex from './LegendItem.Hex'
import Button from '@cdc/core/components/elements/Button'
import LegendGroup from './LegendGroup/Legend.Group'

import useDataVizClasses from '@cdc/core/helpers/useDataVizClasses'
import ConfigContext from '../../../context'
Expand Down Expand Up @@ -116,7 +117,6 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
}

return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions
<li
className={handleListItemClass()}
key={idx}
Expand Down Expand Up @@ -292,7 +292,9 @@ const Legend = forwardRef<HTMLDivElement, LegendProps>((props, ref) => {
}
config={state}
/>
{!!legendListItems.length && (
<LegendGroup legendItems={getFormattedLegendItems()} />

{!!legendListItems.length && ['Select Option', ''].includes(state.legend.groupBy) && (
<ul className={legendClasses.ul.join(' ') || ''} aria-label='Legend items'>
{legendListItems}
</ul>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.group-label {
font-weight: 500;
font-family: Nunito, sans-serif;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.group-list-item {
list-style: none;
font-weight: 400;
font-size: 0.889rem;
margin-top: 0.5rem !important;
cursor: pointer;
}

.group-container {
margin-bottom: 2rem;
}

.legend-group-item-disable {
opacity: 0.4;
}

.legend-group-item-not-disable {
outline: 1px solid #005ea2;
outline-offset: 5px;
border-radius: 1px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { useContext, useMemo } from 'react'
import './legend.group.css'
import LegendShape from '@cdc/core/components/LegendShape'
import { toggleLegendActive } from '@cdc/map/src/helpers/toggleLegendActive'
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
import ConfigContext from '../../../../context'

interface LegendItem {
color: string
label: string
disabled?: boolean
special: boolean
}

interface GroupedData {
[key: string]: LegendItem[]
}

const LegendGroup = ({ legendItems }) => {
const { runtimeLegend, setAccessibleStatus, setRuntimeLegend, state } = useContext(ConfigContext)

const groupLegendItems = (items: LegendItem[], data: object[], groupByKey: string): GroupedData => {
if (!groupByKey || !data || !items) return {}

const columnKey = state.columns.primary.name || ''
const result: GroupedData = {}

for (const row of data) {
const groupValue = row[groupByKey]
if (!groupValue) continue

const label = row[columnKey]
const match = items.find(i => i.label === label)
if (!match) continue

result[groupValue] ||= []
if (!result[groupValue].some(i => i.label === label)) {
result[groupValue].push(match)
}
}

// Sort items in each group
Object.entries(result).forEach(([group, items]) => {
result[group] = [...items].sort(
(a, b) =>
(state.legend.categoryValuesOrder ?? []).indexOf(a.label) -
(state.legend.categoryValuesOrder ?? []).indexOf(b.label)
)
})

return result
}

const handleToggleItem = (item: LegendItem) => {
const newItems = runtimeLegend.items.map(legend =>
legend.value === item.label ? { ...legend, disabled: !legend.disabled } : legend
)

const wasDisabled = runtimeLegend.items.find(i => i.value === item.label)?.disabled
const delta = wasDisabled ? -1 : 1

setRuntimeLegend({
...runtimeLegend,
items: newItems,
disabledAmt: (runtimeLegend.disabledAmt ?? 0) + delta
})

setAccessibleStatus(
`${wasDisabled ? 'Enabled' : 'Disabled'} legend item ${item.label}. Please reference the data table.`
)
}

const getLegendItemClasses = (item: LegendItem, hasDisabledItems: boolean) => {
return [
'group-list-item',
item.disabled ? 'legend-group-item-disable' : hasDisabledItems ? 'legend-group-item-not-disable' : ''
]
.filter(Boolean)
.join(' ')
}

// Новый способ без viewport
const gridClass =
state.legend.position === 'bottom' || state.legend.position === 'top'
? 'col-12 col-sm-6 col-md-4 col-lg-3'
: 'col-12'

const groupedData = useMemo(
() => groupLegendItems(legendItems, state.data, state.legend.groupBy),
[legendItems, state.data, state.legend.groupBy]
)

const hasDisabledItems = runtimeLegend.items.some(item => item.disabled)

return (
<ErrorBoundary component='Grouped Legend'>
<div className='row'>
{Object.entries(groupedData).map(([groupName, items]) => (
<div className={`${gridClass} group-container`} key={groupName}>
<p className='group-label'>{groupName}</p>
<ul className='row'>
{items.map((item, index) => (
<li
key={`${item.label}-${index}`}
role='button'
tabIndex={0}
title={`Legend item ${item.label} - Click to disable`}
className={getLegendItemClasses(item, hasDisabledItems)}
onClick={() => handleToggleItem(item)}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
toggleLegendActive(index, item.label, runtimeLegend, setRuntimeLegend, setAccessibleStatus)
}
}}
>
<LegendShape shape={state.legend.style === 'boxes' ? 'square' : 'circle'} fill={item.color} />
<span>{item.label}</span>
</li>
))}
</ul>
</div>
))}
</div>
</ErrorBoundary>
)
}

export default LegendGroup
3 changes: 2 additions & 1 deletion packages/map/src/data/initial-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ export default {
subStyle: 'linear blocks',
tickRotation: '',
singleColumnLegend: false,
hideBorder: false
hideBorder: false,
groupBy: ''
},
filters: [],
table: {
Expand Down