Skip to content
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

✨ improve hover state of facet legends #4285

Merged
merged 1 commit into from
Dec 13, 2024
Merged
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
16 changes: 14 additions & 2 deletions packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,21 @@ export function byInteractionState(series: {

export function getInteractionStateForSeries(
series: ChartSeries,
activeSeriesNames: SeriesName[]
props: {
activeSeriesNames: SeriesName[]
// usually the interaction mode is active when there is
// at least one active element. But sometimes the interaction
// mode might be active although there are no active elements.
// For example, when the facet legend is hovered but a particular
// chart doesn't plot the hovered element.
isInteractionModeActive?: boolean
}
): InteractionState {
const activeSeriesNames = props.activeSeriesNames
const isInteractionModeActive =
props.isInteractionModeActive ?? activeSeriesNames.length > 0

const active = activeSeriesNames.includes(series.seriesName)
const background = activeSeriesNames.length > 0 && !active
const background = isInteractionModeActive && !active
return { active, background }
}
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,11 @@ export class FacetChart
return this.getExternalLegendProp("equalSizeBins")
}

@computed get hoverColors(): Color[] | undefined {
if (!this.legendHoverBin) return undefined
return [this.legendHoverBin.color]
}

@computed get numericLegendData(): ColorScaleBin[] {
if (!this.isNumericLegend || !this.hideFacetLegends) return []
const allBins: ColorScaleBin[] = this.externalLegends.flatMap(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
GRAPHER_FONT_SCALE_12,
GRAPHER_FONT_SCALE_12_8,
GRAPHER_FONT_SCALE_14,
GRAPHER_OPACITY_MUTE,
} from "../core/GrapherConstants"
import { darkenColorForLine } from "../color/ColorUtils"

Expand Down Expand Up @@ -91,8 +92,9 @@ export interface HorizontalColorLegendManager {
onLegendMouseLeave?: () => void
onLegendMouseOver?: (d: ColorScaleBin) => void
onLegendClick?: (d: ColorScaleBin) => void
activeColors?: string[]
focusColors?: string[]
activeColors?: string[] // inactive colors are grayed out
focusColors?: string[] // focused colors are bolded
hoverColors?: string[] // non-hovered colors are muted
isStatic?: boolean
}

Expand Down Expand Up @@ -808,12 +810,15 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend {

renderLabels(): React.ReactElement {
const { manager, marks } = this
const { focusColors } = manager
const { focusColors, hoverColors = [] } = manager

return (
<g id={makeIdForHumanConsumption("labels")}>
{marks.map((mark, index) => {
const isFocus = focusColors?.includes(mark.bin.color)
const isNotHovered =
hoverColors.length > 0 &&
!hoverColors.includes(mark.bin.color)

return (
<text
Expand All @@ -826,6 +831,7 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend {
dy={dyFromAlign(VerticalAlign.middle)}
fontSize={mark.label.fontSize}
fontWeight={isFocus ? "bold" : undefined}
opacity={isNotHovered ? GRAPHER_OPACITY_MUTE : 1}
>
{mark.label.text}
</text>
Expand All @@ -837,12 +843,15 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend {

renderSwatches(): React.ReactElement {
const { manager, marks } = this
const { activeColors } = manager
const { activeColors, hoverColors = [] } = manager

return (
<g id={makeIdForHumanConsumption("swatches")}>
{marks.map((mark, index) => {
const isActive = activeColors?.includes(mark.bin.color)
const isNotHovered =
hoverColors.length > 0 &&
!hoverColors.includes(mark.bin.color)

const color = mark.bin.patternRef
? `url(#${mark.bin.patternRef})`
Expand All @@ -851,6 +860,10 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend {
const fill =
isActive || activeColors === undefined ? color : "#ccc"

const opacity = isNotHovered
? GRAPHER_OPACITY_MUTE
: manager.legendOpacity

return (
<rect
id={makeIdForHumanConsumption(mark.label.text)}
Expand All @@ -862,7 +875,7 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend {
fill={fill}
stroke={manager.categoricalBinStroke}
strokeWidth={0.4}
opacity={manager.legendOpacity}
opacity={opacity}
/>
)
})}
Expand Down
13 changes: 11 additions & 2 deletions packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,13 @@ export class LineChart
}

@computed get isHoverModeActive(): boolean {
return this.hoveredSeriesNames.length > 0
return (
this.hoveredSeriesNames.length > 0 ||
// if the external legend is hovered, we want to mute
// all non-hovered series even if the chart doesn't plot
// the currently hovered series
(!!this.manager.externalLegendHoverBin && !this.hasColorScale)
)
}

@computed private get hasEntityYearHighlight(): boolean {
Expand Down Expand Up @@ -1290,7 +1296,10 @@ export class LineChart
}

private hoverStateForSeries(series: LineChartSeries): InteractionState {
return getInteractionStateForSeries(series, this.hoveredSeriesNames)
return getInteractionStateForSeries(series, {
isInteractionModeActive: this.isHoverModeActive,
activeSeriesNames: this.hoveredSeriesNames,
})
}

@computed get renderSeries(): RenderLineChartSeries[] {
Expand Down
13 changes: 11 additions & 2 deletions packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,13 @@ export class SlopeChart
}

@computed private get isHoverModeActive(): boolean {
return this.hoveredSeriesNames.length > 0
return (
this.hoveredSeriesNames.length > 0 ||
// if the external legend is hovered, we want to mute
// all non-hovered series even if the chart doesn't plot
// the currently hovered series
!!this.manager.externalLegendHoverBin
)
}

@computed private get yColumns(): CoreColumn[] {
Expand Down Expand Up @@ -448,7 +454,10 @@ export class SlopeChart
}

private hoverStateForSeries(series: SlopeChartSeries): InteractionState {
return getInteractionStateForSeries(series, this.hoveredSeriesNames)
return getInteractionStateForSeries(series, {
isInteractionModeActive: this.isHoverModeActive,
activeSeriesNames: this.hoveredSeriesNames,
})
}

@computed private get renderSeries(): RenderSlopeChartSeries[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,10 @@ export class StackedAreaChart extends AbstractStackedChart {
private hoverStateForSeries(
series: StackedSeries<number>
): InteractionState {
return getInteractionStateForSeries(series, this.hoveredSeriesNames)
return getInteractionStateForSeries(series, {
isInteractionModeActive: this.isHoverModeActive,
activeSeriesNames: this.hoveredSeriesNames,
})
}

@computed get lineLegendSeries(): LineLabelSeries[] {
Expand Down Expand Up @@ -438,6 +441,16 @@ export class StackedAreaChart extends AbstractStackedChart {
)
}

@computed get isHoverModeActive(): boolean {
return (
!!this.hoveredSeriesName ||
// if the external legend is hovered, we want to mute
// all non-hovered series even if the chart doesn't plot
// the currently hovered series
!!this.manager.externalLegendHoverBin
)
}

@computed get hoveredSeriesNames(): string[] {
return this.hoveredSeriesName ? [this.hoveredSeriesName] : []
}
Expand Down
Loading