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 11 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fix]: rename and fix import to the lowercase version based on new convention of having styles start lowercased.
legend.group.css

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,130 @@
import { useContext, useMemo } from 'react'
import './Legend.Group.css'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fix]: this will need to be updated to the new name.

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, currentViewport } = useContext(ConfigContext)

const getGridColClass = (viewport: string) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using the viewport can you use bootstraps helpers?

col-number, col-md-number, col-lg-number?

const map = { xs: 'col-12', sm: 'col-6', md: 'col-4' }
return map[viewport] || 'col-3'
}

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(' ')
}

const gridClass =
state.legend.position === 'bottom' || state.legend.position === 'top' ? getGridColClass(currentViewport) : '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