Skip to content

Commit

Permalink
✨ improve hover state of the facet legend
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Dec 12, 2024
1 parent 2250f24 commit 689aa58
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 12 deletions.
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

0 comments on commit 689aa58

Please sign in to comment.