diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index 3b0dba2ad0..fc27bdb6a7 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx @@ -61,7 +61,10 @@ import { ConnectedScatterLegend, ConnectedScatterLegendManager, } from "./ConnectedScatterLegend" -import { VerticalColorLegend } from "../verticalColorLegend/VerticalColorLegend" +import { + VerticalColorLegendComponent, + VerticalColorLegend, +} from "../verticalColorLegend/VerticalColorLegend" import { DualAxisComponent } from "../axis/AxisViews" import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis" @@ -506,15 +509,14 @@ export class ScatterPlotChart return this.tooltipState.target?.series } - @computed private get verticalColorLegend(): { - width: number - height: number - } { - return VerticalColorLegend.dimensions({ + @computed private get verticalColorLegend(): VerticalColorLegend { + return new VerticalColorLegend({ maxLegendWidth: this.maxLegendWidth, fontSize: this.fontSize, legendItems: this.legendItems, legendTitle: this.legendTitle, + activeColors: this.activeColors, + focusColors: this.focusColors, }) } @@ -832,18 +834,15 @@ export class ScatterPlotChart /> ))} {this.points} - {sizeLegend && ( <> diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx index 4fdc2e90b7..31db65c60b 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx @@ -19,6 +19,7 @@ import { NoDataModal } from "../noDataModal/NoDataModal" import { VerticalColorLegend, LegendItem, + VerticalColorLegendComponent, } from "../verticalColorLegend/VerticalColorLegend" import { TooltipFooterIcon } from "../tooltip/TooltipProps.js" import { @@ -313,14 +314,12 @@ export class StackedBarChart ) } - @computed private get verticalColorLegend(): { - width: number - height: number - } { - return VerticalColorLegend.dimensions({ + @computed private get verticalColorLegend(): VerticalColorLegend { + return new VerticalColorLegend({ maxLegendWidth: this.maxLegendWidth, fontSize: this.fontSize, legendItems: this.legendItems, + activeColors: this.activeColors, }) } @@ -480,6 +479,13 @@ export class StackedBarChart if (!showLegend) return + const eventListeners = this.manager.isStatic + ? undefined + : { + onLegendMouseOver: this.onLegendMouseOver, + onLegendMouseLeave: this.onLegendMouseLeave, + } + return showHorizontalLegend ? ( ) : ( - ) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx index 38f6a6bdc6..9b721761de 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx @@ -2,6 +2,7 @@ import React from "react" import { VerticalColorLegend, VerticalColorLegendProps, + VerticalColorLegendComponent, } from "./VerticalColorLegend" export default { @@ -26,9 +27,10 @@ const props: VerticalColorLegendProps = { } export const CategoricalBins = (): React.ReactElement => { + const verticalColorLegend = new VerticalColorLegend(props) return ( - + ) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx index 38585abcdc..4eaa9b73d8 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx @@ -1,8 +1,12 @@ import React from "react" -import { sum, max, makeIdForHumanConsumption } from "@ourworldindata/utils" +import { + sum, + max, + makeIdForHumanConsumption, + isEmpty, +} from "@ourworldindata/utils" import { TextWrap } from "@ourworldindata/components" import { computed } from "mobx" -import { observer } from "mobx-react" import { GRAPHER_FONT_SCALE_11_2, BASE_FONT_SIZE, @@ -14,14 +18,8 @@ export interface VerticalColorLegendProps { maxLegendWidth?: number fontSize?: number legendTitle?: string - onLegendMouseOver?: (color: string) => void - onLegendClick?: (color: string) => void - onLegendMouseLeave?: () => void - legendX?: number - legendY?: number - activeColors?: Color[] - focusColors?: Color[] - isStatic?: boolean + activeColors?: Color[] // inactive colors are grayed out + focusColors?: Color[] // focused colors are bolded } export interface LegendItem { @@ -39,22 +37,13 @@ interface SizedLegendSeries { yOffset: number } -@observer -export class VerticalColorLegend extends React.Component { - private rectPadding = 5 - private lineHeight = 5 +export class VerticalColorLegend { + rectPadding = 5 + lineHeight = 5 - static dimensions( - props: Pick< - VerticalColorLegendProps, - "legendItems" | "maxLegendWidth" | "fontSize" | "legendTitle" - > - ): { width: number; height: number } { - const legend = new VerticalColorLegend(props) - return { - width: legend.width, - height: legend.height, - } + props: VerticalColorLegendProps + constructor(props: VerticalColorLegendProps) { + this.props = props } @computed private get maxLegendWidth(): number { @@ -65,11 +54,11 @@ export class VerticalColorLegend extends React.Component void + onLegendClick?: (color: string) => void + onLegendMouseLeave?: () => void } +}): React.ReactElement { + return ( + + {state.title && + state.title.render(x, y, { + textProps: { + fontWeight: 700, + }, + })} + + + {eventListeners && !isEmpty(eventListeners) && ( + + )} + + ) +} - @computed get legendY(): number { - return this.props.legendY ?? 0 - } +function Labels({ + x, + y, + state, +}: { + x: number + y: number + state: VerticalColorLegend +}): React.ReactElement { + return ( + + {state.series.map((series) => { + const isFocus = + state.props.focusColors?.includes(series.color) ?? false - renderLabels(): React.ReactElement { - const { series, rectSize, rectPadding } = this - const { focusColors } = this.props + const textX = x + state.rectSize + state.rectPadding + const textY = y + series.yOffset - return ( - - {series.map((series) => { - const isFocus = focusColors?.includes(series.color) ?? false + return ( + + {series.textWrap.render( + textX, + textY, + isFocus + ? { + textProps: { + style: { fontWeight: "bold" }, + }, + } + : undefined + )} + + ) + })} + + ) +} - const textX = this.legendX + rectSize + rectPadding - const textY = this.legendY + series.yOffset +function Swatches({ + x, + y, + state, +}: { + x: number + y: number + state: VerticalColorLegend +}): React.ReactElement { + return ( + + {state.series.map((series) => { + const isActive = state.props.activeColors?.includes( + series.color + ) - return ( - - {series.textWrap.render( - textX, - textY, - isFocus - ? { - textProps: { - style: { fontWeight: "bold" }, - }, - } - : undefined - )} - - ) - })} - - ) - } + const textX = x + state.rectSize + state.rectPadding + const textY = y + series.yOffset - renderSwatches(): React.ReactElement { - const { series, rectSize, rectPadding } = this - const { activeColors = [] } = this.props + const renderedTextPosition = + series.textWrap.getPositionForSvgRendering(textX, textY) - return ( - - {series.map((series) => { - const isActive = activeColors.includes(series.color) + return ( + + ) + })} + + ) +} - const textX = this.legendX + rectSize + rectPadding - const textY = this.legendY + series.yOffset +function InteractiveElement({ + x, + y, + state, + eventListeners, +}: { + x: number + y: number + state: VerticalColorLegend + eventListeners?: { + onLegendMouseOver?: (color: string) => void + onLegendClick?: (color: string) => void + onLegendMouseLeave?: () => void + } +}): React.ReactElement { + const { onLegendMouseOver, onLegendMouseLeave, onLegendClick } = + eventListeners ?? {} + return ( + + {state.series.map((series) => { + const mouseOver = onLegendMouseOver + ? (): void => onLegendMouseOver(series.color) + : undefined + const mouseLeave = onLegendMouseLeave + const click = onLegendClick + ? (): void => onLegendClick(series.color) + : undefined - const renderedTextPosition = - series.textWrap.getPositionForSvgRendering(textX, textY) + const cursor = click ? "pointer" : "default" - return ( + return ( + - ) - })} - - ) - } - - renderInteractiveElements(): React.ReactElement { - const { series, lineHeight } = this - const { onLegendClick, onLegendMouseOver, onLegendMouseLeave } = - this.props - return ( - - {series.map((series) => { - const mouseOver = onLegendMouseOver - ? (): void => onLegendMouseOver(series.color) - : undefined - const mouseLeave = onLegendMouseLeave || undefined - const click = onLegendClick - ? (): void => onLegendClick(series.color) - : undefined - - const cursor = click ? "pointer" : "default" - - return ( - - - - ) - })} - - ) - } - - render(): React.ReactElement { - return ( - - {this.title && - this.title.render(this.legendX, this.legendY, { - textProps: { - fontWeight: 700, - }, - })} - {this.renderLabels()} - {this.renderSwatches()} - {!this.props.isStatic && this.renderInteractiveElements()} - - ) - } + + ) + })} + + ) }