diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx
index 3b0dba2ad0..ed23ecbc71 100644
--- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx
+++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx
@@ -62,6 +62,7 @@ import {
ConnectedScatterLegendManager,
} from "./ConnectedScatterLegend"
import { VerticalColorLegend } from "../verticalColorLegend/VerticalColorLegend"
+import { VerticalColorLegendComponent } from "../verticalColorLegend/VerticalColorLegendComponent"
import { DualAxisComponent } from "../axis/AxisViews"
import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis"
@@ -506,15 +507,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 +832,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..1821eda016 100644
--- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx
+++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx
@@ -20,6 +20,7 @@ import {
VerticalColorLegend,
LegendItem,
} from "../verticalColorLegend/VerticalColorLegend"
+import { VerticalColorLegendComponent } from "../verticalColorLegend/VerticalColorLegendComponent"
import { TooltipFooterIcon } from "../tooltip/TooltipProps.js"
import {
Tooltip,
@@ -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..ec43b0adb8 100644
--- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx
+++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx
@@ -3,6 +3,7 @@ import {
VerticalColorLegend,
VerticalColorLegendProps,
} from "./VerticalColorLegend"
+import { VerticalColorLegendComponent } from "./VerticalColorLegendComponent"
export default {
title: "VerticalColorLegend",
@@ -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.ts b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts
new file mode 100644
index 0000000000..cbabfb4a43
--- /dev/null
+++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts
@@ -0,0 +1,117 @@
+import { sum, max } from "@ourworldindata/utils"
+import { TextWrap } from "@ourworldindata/components"
+import { computed } from "mobx"
+import {
+ GRAPHER_FONT_SCALE_11_2,
+ BASE_FONT_SIZE,
+} from "../core/GrapherConstants"
+import { Color } from "@ourworldindata/types"
+
+export interface VerticalColorLegendProps {
+ legendItems: LegendItem[]
+ maxLegendWidth?: number
+ fontSize?: number
+ legendTitle?: string
+ activeColors?: Color[] // inactive colors are grayed out
+ focusColors?: Color[] // focused colors are bolded
+}
+
+export interface LegendItem {
+ label?: string
+ minText?: string
+ maxText?: string
+ color: Color
+}
+
+interface SizedLegendSeries {
+ textWrap: TextWrap
+ color: Color
+ width: number
+ height: number
+ yOffset: number
+}
+
+export class VerticalColorLegend {
+ rectPadding = 5
+ lineHeight = 5
+
+ props: VerticalColorLegendProps
+ constructor(props: VerticalColorLegendProps) {
+ this.props = props
+ }
+
+ @computed private get maxLegendWidth(): number {
+ return this.props.maxLegendWidth ?? 100
+ }
+
+ @computed private get fontSize(): number {
+ return GRAPHER_FONT_SCALE_11_2 * (this.props.fontSize ?? BASE_FONT_SIZE)
+ }
+
+ @computed get rectSize(): number {
+ return Math.round(this.fontSize / 1.4)
+ }
+
+ @computed get title(): TextWrap | undefined {
+ if (!this.props.legendTitle) return undefined
+ return new TextWrap({
+ maxWidth: this.maxLegendWidth,
+ fontSize: this.fontSize,
+ fontWeight: 700,
+ lineHeight: 1,
+ text: this.props.legendTitle,
+ })
+ }
+
+ @computed private get titleHeight(): number {
+ if (!this.title) return 0
+ return this.title.height + 5
+ }
+
+ @computed get series(): SizedLegendSeries[] {
+ const { fontSize, rectSize, rectPadding, titleHeight, lineHeight } =
+ this
+
+ let runningYOffset = titleHeight
+ return this.props.legendItems.map((series) => {
+ let label = series.label
+ // infer label for numeric bins
+ if (!label && series.minText && series.maxText) {
+ label = `${series.minText} – ${series.maxText}`
+ }
+ const textWrap = new TextWrap({
+ maxWidth: this.maxLegendWidth,
+ fontSize,
+ lineHeight: 1,
+ text: label ?? "",
+ })
+ const width = rectSize + rectPadding + textWrap.width
+ const height = Math.max(textWrap.height, rectSize)
+ const yOffset = runningYOffset
+
+ runningYOffset += height + lineHeight
+
+ return {
+ textWrap,
+ color: series.color,
+ width,
+ height,
+ yOffset,
+ }
+ })
+ }
+
+ @computed get width(): number {
+ const widths = this.series.map((series) => series.width)
+ if (this.title) widths.push(this.title.width)
+ return max(widths) ?? 0
+ }
+
+ @computed get height(): number {
+ return (
+ this.titleHeight +
+ sum(this.series.map((series) => series.height)) +
+ this.lineHeight * this.series.length
+ )
+ }
+}
diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx
deleted file mode 100644
index 38585abcdc..0000000000
--- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-import React from "react"
-import { sum, max, makeIdForHumanConsumption } 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,
-} from "../core/GrapherConstants"
-import { Color } from "@ourworldindata/types"
-
-export interface VerticalColorLegendProps {
- legendItems: LegendItem[]
- 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
-}
-
-export interface LegendItem {
- label?: string
- minText?: string
- maxText?: string
- color: Color
-}
-
-interface SizedLegendSeries {
- textWrap: TextWrap
- color: Color
- width: number
- height: number
- yOffset: number
-}
-
-@observer
-export class VerticalColorLegend extends React.Component {
- private rectPadding = 5
- private 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,
- }
- }
-
- @computed private get maxLegendWidth(): number {
- return this.props.maxLegendWidth ?? 100
- }
-
- @computed private get fontSize(): number {
- return GRAPHER_FONT_SCALE_11_2 * (this.props.fontSize ?? BASE_FONT_SIZE)
- }
-
- @computed private get rectSize(): number {
- return Math.round(this.fontSize / 1.4)
- }
-
- @computed private get title(): TextWrap | undefined {
- if (!this.props.legendTitle) return undefined
- return new TextWrap({
- maxWidth: this.maxLegendWidth,
- fontSize: this.fontSize,
- fontWeight: 700,
- lineHeight: 1,
- text: this.props.legendTitle,
- })
- }
-
- @computed private get titleHeight(): number {
- if (!this.title) return 0
- return this.title.height + 5
- }
-
- @computed private get series(): SizedLegendSeries[] {
- const { fontSize, rectSize, rectPadding, titleHeight, lineHeight } =
- this
-
- let runningYOffset = titleHeight
- return this.props.legendItems.map((series) => {
- let label = series.label
- // infer label for numeric bins
- if (!label && series.minText && series.maxText) {
- label = `${series.minText} – ${series.maxText}`
- }
- const textWrap = new TextWrap({
- maxWidth: this.maxLegendWidth,
- fontSize,
- lineHeight: 1,
- text: label ?? "",
- })
- const width = rectSize + rectPadding + textWrap.width
- const height = Math.max(textWrap.height, rectSize)
- const yOffset = runningYOffset
-
- runningYOffset += height + lineHeight
-
- return {
- textWrap,
- color: series.color,
- width,
- height,
- yOffset,
- }
- })
- }
-
- @computed get width(): number {
- const widths = this.series.map((series) => series.width)
- if (this.title) widths.push(this.title.width)
- return max(widths) ?? 0
- }
-
- @computed get height(): number {
- return (
- this.titleHeight +
- sum(this.series.map((series) => series.height)) +
- this.lineHeight * this.series.length
- )
- }
-
- @computed get legendX(): number {
- return this.props.legendX ?? 0
- }
-
- @computed get legendY(): number {
- return this.props.legendY ?? 0
- }
-
- renderLabels(): React.ReactElement {
- const { series, rectSize, rectPadding } = this
- const { focusColors } = this.props
-
- return (
-
- {series.map((series) => {
- const isFocus = focusColors?.includes(series.color) ?? false
-
- const textX = this.legendX + rectSize + rectPadding
- const textY = this.legendY + series.yOffset
-
- return (
-
- {series.textWrap.render(
- textX,
- textY,
- isFocus
- ? {
- textProps: {
- style: { fontWeight: "bold" },
- },
- }
- : undefined
- )}
-
- )
- })}
-
- )
- }
-
- renderSwatches(): React.ReactElement {
- const { series, rectSize, rectPadding } = this
- const { activeColors = [] } = this.props
-
- return (
-
- {series.map((series) => {
- const isActive = activeColors.includes(series.color)
-
- const textX = this.legendX + rectSize + rectPadding
- const textY = this.legendY + series.yOffset
-
- const renderedTextPosition =
- series.textWrap.getPositionForSvgRendering(textX, textY)
-
- 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()}
-
- )
- }
-}
diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx
new file mode 100644
index 0000000000..8ee3a10742
--- /dev/null
+++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx
@@ -0,0 +1,174 @@
+import React from "react"
+
+import { isEmpty, makeIdForHumanConsumption } from "@ourworldindata/utils"
+import { VerticalColorLegend } from "./VerticalColorLegend"
+
+export function VerticalColorLegendComponent({
+ state,
+ x = 0,
+ y = 0,
+ eventListeners,
+}: {
+ state: VerticalColorLegend
+ x?: number
+ y?: number
+ eventListeners?: {
+ onLegendMouseOver?: (color: string) => void
+ onLegendClick?: (color: string) => void
+ onLegendMouseLeave?: () => void
+ }
+}): React.ReactElement {
+ return (
+
+ {state.title &&
+ state.title.render(x, y, {
+ textProps: {
+ fontWeight: 700,
+ },
+ })}
+
+
+ {eventListeners && !isEmpty(eventListeners) && (
+
+ )}
+
+ )
+}
+
+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
+
+ const textX = x + state.rectSize + state.rectPadding
+ const textY = y + series.yOffset
+
+ return (
+
+ {series.textWrap.render(
+ textX,
+ textY,
+ isFocus
+ ? {
+ textProps: {
+ style: { fontWeight: "bold" },
+ },
+ }
+ : undefined
+ )}
+
+ )
+ })}
+
+ )
+}
+
+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
+ )
+
+ const textX = x + state.rectSize + state.rectPadding
+ const textY = y + series.yOffset
+
+ const renderedTextPosition =
+ series.textWrap.getPositionForSvgRendering(textX, textY)
+
+ return (
+
+ )
+ })}
+
+ )
+}
+
+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 cursor = click ? "pointer" : "default"
+
+ return (
+
+
+
+ )
+ })}
+
+ )
+}