diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 53e43dd8524..5188506049e 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -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 } } diff --git a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx index cc760503661..54d821f2dc4 100644 --- a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx +++ b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx @@ -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( diff --git a/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx b/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx index 7ae57544011..c5ce14e262e 100644 --- a/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx +++ b/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx @@ -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" @@ -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 } @@ -808,12 +810,15 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend { renderLabels(): React.ReactElement { const { manager, marks } = this - const { focusColors } = manager + const { focusColors, hoverColors = [] } = manager return ( {marks.map((mark, index) => { const isFocus = focusColors?.includes(mark.bin.color) + const isNotHovered = + hoverColors.length > 0 && + !hoverColors.includes(mark.bin.color) return ( {mark.label.text} @@ -837,12 +843,15 @@ export class HorizontalCategoricalColorLegend extends HorizontalColorLegend { renderSwatches(): React.ReactElement { const { manager, marks } = this - const { activeColors } = manager + const { activeColors, hoverColors = [] } = manager return ( {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})` @@ -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 ( ) })} diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 0c25e385244..e1ec07006ad 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -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 { @@ -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[] { diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 21e3ede222c..38c2ac2d62d 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -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[] { @@ -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[] { diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index 4f87c513af2..5b5cfc4db5a 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -301,7 +301,10 @@ export class StackedAreaChart extends AbstractStackedChart { private hoverStateForSeries( series: StackedSeries ): InteractionState { - return getInteractionStateForSeries(series, this.hoveredSeriesNames) + return getInteractionStateForSeries(series, { + isInteractionModeActive: this.isHoverModeActive, + activeSeriesNames: this.hoveredSeriesNames, + }) } @computed get lineLegendSeries(): LineLabelSeries[] { @@ -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] : [] }