From 776886c93e5b8f7dcb08492f91806c120a350f67 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Fri, 20 Dec 2024 10:23:08 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=A8=20drop=20manager=20pattern=20f?= =?UTF-8?q?or=20vertical=20color=20legend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/scatterCharts/ScatterPlotChart.tsx | 26 ++++--- .../src/stackedCharts/StackedBarChart.tsx | 26 +++++-- .../VerticalColorLegend.stories.tsx | 6 +- .../VerticalColorLegend.tsx | 74 ++++++++++--------- 4 files changed, 78 insertions(+), 54 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index 9f65ef78db9..76dd054c791 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx @@ -61,10 +61,7 @@ import { ConnectedScatterLegend, ConnectedScatterLegendManager, } from "./ConnectedScatterLegend" -import { - VerticalColorLegend, - VerticalColorLegendManager, -} from "../verticalColorLegend/VerticalColorLegend" +import { VerticalColorLegend } from "../verticalColorLegend/VerticalColorLegend" import { DualAxisComponent } from "../axis/AxisViews" import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis" @@ -127,7 +124,6 @@ export class ScatterPlotChart ConnectedScatterLegendManager, ScatterSizeLegendManager, ChartInterface, - VerticalColorLegendManager, ColorScaleManager { // currently hovered legend color @@ -510,8 +506,16 @@ export class ScatterPlotChart return this.tooltipState.target?.series } - @computed private get legendDimensions(): VerticalColorLegend { - return new VerticalColorLegend({ manager: this }) + @computed private get verticalColorLegend(): { + width: number + height: number + } { + return VerticalColorLegend.dimensions({ + maxLegendWidth: this.maxLegendWidth, + fontSize: this.fontSize, + legendItems: this.legendItems, + legendTitle: this.legendTitle, + }) } @computed get maxLegendWidth(): number { @@ -527,10 +531,10 @@ export class ScatterPlotChart } @computed.struct get sidebarWidth(): number { - const { legendDimensions, sidebarMinWidth, sidebarMaxWidth } = this + const { verticalColorLegend, sidebarMinWidth, sidebarMaxWidth } = this return Math.max( - Math.min(legendDimensions.width, sidebarMaxWidth), + Math.min(verticalColorLegend.width, sidebarMaxWidth), sidebarMinWidth ) } @@ -762,12 +766,12 @@ export class ScatterPlotChart sizeLegend, sidebarWidth, comparisonLines, - legendDimensions, + verticalColorLegend, } = this const hasLegendItems = this.legendItems.length > 0 const verticalLegendHeight = hasLegendItems - ? legendDimensions.height + ? verticalColorLegend.height : 0 const sizeLegendHeight = sizeLegend?.height ?? 0 const arrowLegendHeight = arrowLegend?.height ?? 0 diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx index 6dcbab4f96c..922f0a3d9f6 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx @@ -18,7 +18,6 @@ import { DualAxisComponent } from "../axis/AxisViews" import { NoDataModal } from "../noDataModal/NoDataModal" import { VerticalColorLegend, - VerticalColorLegendManager, LegendItem, } from "../verticalColorLegend/VerticalColorLegend" import { TooltipFooterIcon } from "../tooltip/TooltipProps.js" @@ -144,7 +143,7 @@ class StackedBarSegment extends React.Component { @observer export class StackedBarChart extends AbstractStackedChart - implements VerticalColorLegendManager, ColorScaleManager + implements ColorScaleManager { readonly minBarSpacing = 4 @@ -318,8 +317,15 @@ export class StackedBarChart ) } - @computed private get verticalColorLegend(): VerticalColorLegend { - return new VerticalColorLegend({ manager: this }) + @computed private get verticalColorLegend(): { + width: number + height: number + } { + return VerticalColorLegend.dimensions({ + maxLegendWidth: this.maxLegendWidth, + fontSize: this.fontSize, + legendItems: this.legendItems, + }) } @computed @@ -476,7 +482,17 @@ export class StackedBarChart return showHorizontalLegend ? ( ) : ( - + ) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx index 42d9e94b5cb..38f6a6bdc6f 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx @@ -1,7 +1,7 @@ import React from "react" import { VerticalColorLegend, - VerticalColorLegendManager, + VerticalColorLegendProps, } from "./VerticalColorLegend" export default { @@ -9,7 +9,7 @@ export default { component: VerticalColorLegend, } -const manager: VerticalColorLegendManager = { +const props: VerticalColorLegendProps = { maxLegendWidth: 500, legendTitle: "Legend Title", legendItems: [ @@ -28,7 +28,7 @@ const manager: VerticalColorLegendManager = { export const CategoricalBins = (): React.ReactElement => { return ( - + ) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx index ea659e517e2..5d33814e6f7 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx @@ -9,21 +9,26 @@ import { } from "../core/GrapherConstants" import { Color } from "@ourworldindata/types" -export interface VerticalColorLegendManager { +export interface VerticalColorLegendProps { + legendItems: LegendItem[] maxLegendWidth?: number fontSize?: number - legendItems: LegendItem[] legendTitle?: string onLegendMouseOver?: (color: string) => void onLegendClick?: (color: string) => void onLegendMouseLeave?: () => void legendX?: number legendY?: number - activeColors: Color[] + activeColors?: Color[] focusColors?: Color[] isStatic?: boolean } +type VerticalColorLegendPropsMinimal = Pick< + VerticalColorLegendProps, + "legendItems" | "maxLegendWidth" | "fontSize" | "legendTitle" +> + export interface LegendItem { label?: string minText?: string @@ -40,37 +45,41 @@ interface SizedLegendSeries { } @observer -export class VerticalColorLegend extends React.Component<{ - manager: VerticalColorLegendManager -}> { - @computed get manager(): VerticalColorLegendManager { - return this.props.manager +export class VerticalColorLegend extends React.Component { + private rectPadding = 5 + private lineHeight = 5 + + static dimensions(props: VerticalColorLegendPropsMinimal): { + width: number + height: number + } { + const legend = new VerticalColorLegend(props) + return { + width: legend.width, + height: legend.height, + } } @computed private get maxLegendWidth(): number { - return this.manager.maxLegendWidth ?? 100 + return this.props.maxLegendWidth ?? 100 } @computed private get fontSize(): number { - return ( - GRAPHER_FONT_SCALE_11_2 * (this.manager.fontSize ?? BASE_FONT_SIZE) - ) + return GRAPHER_FONT_SCALE_11_2 * (this.props.fontSize ?? BASE_FONT_SIZE) } + @computed private get rectSize(): number { return Math.round(this.fontSize / 1.4) } - private rectPadding = 5 - private lineHeight = 5 - @computed private get title(): TextWrap | undefined { - if (!this.manager.legendTitle) return undefined + if (!this.props.legendTitle) return undefined return new TextWrap({ maxWidth: this.maxLegendWidth, fontSize: this.fontSize, fontWeight: 700, lineHeight: 1, - text: this.manager.legendTitle, + text: this.props.legendTitle, }) } @@ -80,17 +89,11 @@ export class VerticalColorLegend extends React.Component<{ } @computed private get series(): SizedLegendSeries[] { - const { - manager, - fontSize, - rectSize, - rectPadding, - titleHeight, - lineHeight, - } = this + const { fontSize, rectSize, rectPadding, titleHeight, lineHeight } = + this let runningYOffset = titleHeight - return manager.legendItems.map((series) => { + return this.props.legendItems.map((series) => { let label = series.label // infer label for numeric bins if (!label && series.minText && series.maxText) { @@ -133,16 +136,16 @@ export class VerticalColorLegend extends React.Component<{ } @computed get legendX(): number { - return this.manager.legendX ?? 0 + return this.props.legendX ?? 0 } @computed get legendY(): number { - return this.manager.legendY ?? 0 + return this.props.legendY ?? 0 } renderLabels(): React.ReactElement { - const { series, manager, rectSize, rectPadding } = this - const { focusColors } = manager + const { series, rectSize, rectPadding } = this + const { focusColors } = this.props return ( @@ -173,8 +176,8 @@ export class VerticalColorLegend extends React.Component<{ } renderSwatches(): React.ReactElement { - const { manager, series, rectSize, rectPadding } = this - const { activeColors } = manager + const { series, rectSize, rectPadding } = this + const { activeColors = [] } = this.props return ( @@ -204,8 +207,9 @@ export class VerticalColorLegend extends React.Component<{ } renderInteractiveElements(): React.ReactElement { - const { series, manager, lineHeight } = this - const { onLegendClick, onLegendMouseOver, onLegendMouseLeave } = manager + const { series, lineHeight } = this + const { onLegendClick, onLegendMouseOver, onLegendMouseLeave } = + this.props return ( {series.map((series) => { @@ -261,7 +265,7 @@ export class VerticalColorLegend extends React.Component<{ })} {this.renderLabels()} {this.renderSwatches()} - {!this.manager.isStatic && this.renderInteractiveElements()} + {!this.props.isStatic && this.renderInteractiveElements()} ) } From 0a4c795447b1813e35d79b27cbeeb007a8d44a31 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Fri, 20 Dec 2024 16:44:41 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=94=A8=20drop=20vertical=20color=20le?= =?UTF-8?q?gend=20observer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/scatterCharts/ScatterPlotChart.tsx | 19 +- .../src/stackedCharts/StackedBarChart.tsx | 29 +- .../VerticalColorLegend.stories.tsx | 4 +- .../VerticalColorLegend.ts | 115 ++++++++ .../VerticalColorLegend.tsx | 272 ------------------ .../VerticalColorLegendComponent.tsx | 182 ++++++++++++ 6 files changed, 327 insertions(+), 294 deletions(-) create mode 100644 packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts delete mode 100644 packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx create mode 100644 packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index 76dd054c791..072e02bd290 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,11 +507,8 @@ 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, @@ -832,7 +830,16 @@ export class ScatterPlotChart /> ))} {this.points} - + {sizeLegend && ( <> {separatorLine(ySizeLegend)} diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx index 922f0a3d9f6..45bb6d0c5bd 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, @@ -317,11 +318,8 @@ 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, @@ -473,7 +471,7 @@ export class StackedBarChart renderLegend(): React.ReactElement | void { const { - manager: { showLegend }, + manager: { showLegend, isStatic }, showHorizontalLegend, } = this @@ -482,16 +480,17 @@ export class StackedBarChart return showHorizontalLegend ? ( ) : ( - ) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx index 38f6a6bdc6f..251bcbb1688 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 00000000000..ff9d6b648c3 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts @@ -0,0 +1,115 @@ +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 +} + +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 5d33814e6f7..00000000000 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx +++ /dev/null @@ -1,272 +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 -} - -type VerticalColorLegendPropsMinimal = Pick< - VerticalColorLegendProps, - "legendItems" | "maxLegendWidth" | "fontSize" | "legendTitle" -> - -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: VerticalColorLegendPropsMinimal): { - 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 00000000000..884587aa33e --- /dev/null +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx @@ -0,0 +1,182 @@ +import React from "react" + +import { Color, makeIdForHumanConsumption } from "@ourworldindata/utils" +import { VerticalColorLegend } from "./VerticalColorLegend" + +export function VerticalColorLegendComponent({ + legend, + x = 0, + y = 0, + activeColors, + focusColors, + onLegendClick, + onLegendMouseOver, + onLegendMouseLeave, +}: { + legend: VerticalColorLegend + x?: number + y?: number + activeColors?: Color[] // inactive colors are grayed out + focusColors?: Color[] // focused colors are bolded + onLegendClick?: (color: string) => void + onLegendMouseOver?: (color: string) => void + onLegendMouseLeave?: () => void +}): React.ReactElement { + const isInteractive = + onLegendClick || onLegendMouseOver || onLegendMouseLeave + + return ( + + {legend.title && + legend.title.render(x, y, { + textProps: { + fontWeight: 700, + }, + })} + + + {isInteractive && ( + + )} + + ) +} + +function Labels({ + legend, + x, + y, + focusColors, +}: { + legend: VerticalColorLegend + x: number + y: number + focusColors?: Color[] +}): React.ReactElement { + return ( + + {legend.series.map((series) => { + const isFocus = focusColors?.includes(series.color) ?? false + + const textX = x + legend.rectSize + legend.rectPadding + const textY = y + series.yOffset + + return ( + + {series.textWrap.render( + textX, + textY, + isFocus + ? { + textProps: { + style: { fontWeight: "bold" }, + }, + } + : undefined + )} + + ) + })} + + ) +} + +function Swatches({ + legend, + x, + y, + activeColors, +}: { + legend: VerticalColorLegend + x: number + y: number + activeColors?: Color[] +}): React.ReactElement { + return ( + + {legend.series.map((series) => { + const isActive = activeColors?.includes(series.color) + + const textX = x + legend.rectSize + legend.rectPadding + const textY = y + series.yOffset + + const renderedTextPosition = + series.textWrap.getPositionForSvgRendering(textX, textY) + + return ( + + ) + })} + + ) +} + +function InteractiveElement({ + x, + y, + legend, + onLegendClick, + onLegendMouseOver, + onLegendMouseLeave, +}: { + x: number + y: number + legend: VerticalColorLegend + onLegendClick?: (color: string) => void + onLegendMouseOver?: (color: string) => void + onLegendMouseLeave?: () => void +}): React.ReactElement { + return ( + + {legend.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 ( + + + + ) + })} + + ) +} From cbcedcebd8fc89802dbdb601c03d459b4841bd05 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Sun, 22 Dec 2024 01:47:35 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=A8=20rename=20vertical=20color=20?= =?UTF-8?q?legend=20props?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scatterCharts/ScatterPlotChart.test.ts | 6 +- .../src/scatterCharts/ScatterPlotChart.tsx | 68 ++++++++------ .../src/stackedCharts/StackedBarChart.tsx | 33 ++++--- .../VerticalColorLegend.stories.tsx | 12 ++- .../VerticalColorLegend.ts | 89 +++++++++++------- .../VerticalColorLegendComponent.tsx | 93 +++++++++++-------- 6 files changed, 180 insertions(+), 121 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.test.ts b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.test.ts index 04c95cb6b9b..e39a9028522 100755 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.test.ts +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.test.ts @@ -531,7 +531,11 @@ describe("colors & legend", () => { }) it("legend contains every continent for which there is data (before timeline filter)", () => { - expect(chart.legendItems.map((item) => item.label).sort()).toEqual([ + expect( + chart.verticalColorLegendBins + .map((item) => item.type === "categorical" && item.label) + .sort() + ).toEqual([ "Africa", "Europe", "North America", diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index 072e02bd290..02acd741153 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 { + VerticalColorLegend, + VerticalColorLegendBin, +} from "../verticalColorLegend/VerticalColorLegend" import { VerticalColorLegendComponent } from "../verticalColorLegend/VerticalColorLegendComponent" import { DualAxisComponent } from "../axis/AxisViews" import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis" @@ -95,7 +98,7 @@ import { ColorScaleConfigDefaults, } from "../color/ColorScaleConfig" import { SelectionArray } from "../selection/SelectionArray" -import { ColorScaleBin } from "../color/ColorScaleBin" +import { CategoricalBin } from "../color/ColorScaleBin" import { ScatterSizeLegend, ScatterSizeLegendManager, @@ -509,17 +512,13 @@ export class ScatterPlotChart @computed private get verticalColorLegend(): VerticalColorLegend { return new VerticalColorLegend({ - maxLegendWidth: this.maxLegendWidth, + bins: this.verticalColorLegendBins, + maxWidth: this.sidebarMaxWidth, + legendTitle: this.colorScale.legendDescription, fontSize: this.fontSize, - legendItems: this.legendItems, - legendTitle: this.legendTitle, }) } - @computed get maxLegendWidth(): number { - return this.sidebarMaxWidth - } - @computed private get sidebarMinWidth(): number { return Math.max(this.bounds.width * 0.1, 60) } @@ -687,16 +686,27 @@ export class ScatterPlotChart return this.transformedTable.get(this.colorColumnSlug) } - @computed get legendItems(): ColorScaleBin[] { - return this.colorScale.legendBins.filter( + @computed get verticalColorLegendBins(): VerticalColorLegendBin[] { + const bins = this.colorScale.legendBins.filter( (bin) => this.colorsInUse.includes(bin.color) && bin.label !== NO_DATA_LABEL ) - } - @computed get legendTitle(): string | undefined { - return this.colorScale.legendDescription + return bins.map((bin) => + bin instanceof CategoricalBin + ? { + type: "categorical", + color: bin.color, + label: bin.label ?? "", + } + : { + type: "numeric", + color: bin.color, + minLabel: bin.minText, + maxLabel: bin.maxText, + } + ) } @computed get sizeScale(): ScaleLinear { @@ -767,7 +777,7 @@ export class ScatterPlotChart verticalColorLegend, } = this - const hasLegendItems = this.legendItems.length > 0 + const hasLegendItems = this.verticalColorLegendBins.length > 0 const verticalLegendHeight = hasLegendItems ? verticalColorLegend.height : 0 @@ -789,7 +799,7 @@ export class ScatterPlotChart (arrowLegendHeight > 0 ? legendPadding : 0) const noDataSectionBounds = new Bounds( - this.legendX, + this.verticalColorLegendX, yNoDataSection, sidebarWidth, bounds.height - yNoDataSection @@ -798,7 +808,7 @@ export class ScatterPlotChart const separatorLine = (y: number): React.ReactElement | null => y > bounds.top ? ( {sizeLegend && ( <> {separatorLine(ySizeLegend)} - {sizeLegend.render(this.legendX, ySizeLegend)} + {sizeLegend.render( + this.verticalColorLegendX, + ySizeLegend + )} )} {arrowLegend && ( @@ -853,7 +866,10 @@ export class ScatterPlotChart className="clickable" onClick={this.onToggleEndpoints} > - {arrowLegend.render(this.legendX, yArrowLegend)} + {arrowLegend.render( + this.verticalColorLegendX, + yArrowLegend + )} )} @@ -999,11 +1015,11 @@ export class ScatterPlotChart ) } - @computed get legendY(): number { + @computed get verticalColorLegendY(): number { return this.bounds.top } - @computed get legendX(): number { + @computed get verticalColorLegendX(): number { return this.bounds.right - this.sidebarWidth } diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx index 45bb6d0c5bd..0224e532810 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx @@ -18,7 +18,7 @@ import { DualAxisComponent } from "../axis/AxisViews" import { NoDataModal } from "../noDataModal/NoDataModal" import { VerticalColorLegend, - LegendItem, + VerticalColorLegendCategoricalBin, } from "../verticalColorLegend/VerticalColorLegend" import { VerticalColorLegendComponent } from "../verticalColorLegend/VerticalColorLegendComponent" import { TooltipFooterIcon } from "../tooltip/TooltipProps.js" @@ -259,12 +259,12 @@ export class StackedBarChart ) } - // used by - @computed get legendItems(): (LegendItem & - Required>)[] { + @computed + get verticalColorLegendBins(): VerticalColorLegendCategoricalBin[] { return this.series .map((series) => { return { + type: "categorical" as const, label: series.seriesName, color: series.color, } @@ -274,7 +274,7 @@ export class StackedBarChart // used by @computed get categoricalLegendData(): CategoricalBin[] { - return this.legendItems.map( + return this.verticalColorLegendBins.map( (legendItem, index) => new CategoricalBin({ index, @@ -320,9 +320,11 @@ export class StackedBarChart @computed private get verticalColorLegend(): VerticalColorLegend { return new VerticalColorLegend({ - maxLegendWidth: this.maxLegendWidth, + bins: this.verticalColorLegendBins, + maxWidth: this.showHorizontalLegend + ? this.bounds.width + : this.sidebarMaxWidth, fontSize: this.fontSize, - legendItems: this.legendItems, }) } @@ -477,20 +479,21 @@ export class StackedBarChart if (!showLegend) return + const x = this.showHorizontalLegend + ? this.bounds.left + : this.bounds.right - this.sidebarWidth + const y = this.bounds.top + return showHorizontalLegend ? ( ) : ( ) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx index 251bcbb1688..add61635410 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.stories.tsx @@ -11,26 +11,30 @@ export default { } const props: VerticalColorLegendProps = { - maxLegendWidth: 500, + maxWidth: 500, legendTitle: "Legend Title", - legendItems: [ + bins: [ { + type: "categorical", label: "Canada", color: "red", }, { + type: "categorical", label: "Mexico", color: "green", }, ], - activeColors: ["red", "green"], } 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 index ff9d6b648c3..15c634a3f3d 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts @@ -7,53 +7,67 @@ import { } from "../core/GrapherConstants" import { Color } from "@ourworldindata/types" -export interface VerticalColorLegendProps { - legendItems: LegendItem[] - maxLegendWidth?: number - fontSize?: number - legendTitle?: string +interface Bin { + color: Color } -export interface LegendItem { - label?: string - minText?: string - maxText?: string - color: Color +export interface VerticalColorLegendCategoricalBin extends Bin { + type: "categorical" + label: string +} + +export interface VerticalColorLegendNumericBin extends Bin { + type: "numeric" + minLabel: string + maxLabel: string } -interface SizedLegendSeries { +export type VerticalColorLegendBin = + | VerticalColorLegendCategoricalBin + | VerticalColorLegendNumericBin + +interface PlacedBin extends Bin { textWrap: TextWrap - color: Color width: number height: number yOffset: number } +export interface VerticalColorLegendProps { + bins: VerticalColorLegendBin[] + maxWidth?: number + fontSize?: number + legendTitle?: string +} + export class VerticalColorLegend { - rectPadding = 5 - lineHeight = 5 + /** Margin between the swatch and the label */ + swatchMarginRight = 5 + + /** Vertical space between two bins */ + verticalBinMargin = 5 - props: VerticalColorLegendProps + private props: VerticalColorLegendProps constructor(props: VerticalColorLegendProps) { this.props = props } - @computed private get maxLegendWidth(): number { - return this.props.maxLegendWidth ?? 100 + @computed private get maxWidth(): number { + return this.props.maxWidth ?? 100 } @computed private get fontSize(): number { return GRAPHER_FONT_SCALE_11_2 * (this.props.fontSize ?? BASE_FONT_SIZE) } - @computed get rectSize(): number { + @computed get swatchSize(): 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, + maxWidth: this.maxWidth, fontSize: this.fontSize, fontWeight: 700, lineHeight: 1, @@ -66,28 +80,35 @@ export class VerticalColorLegend { return this.title.height + 5 } - @computed get series(): SizedLegendSeries[] { - const { fontSize, rectSize, rectPadding, titleHeight, lineHeight } = - this + @computed get placedBins(): PlacedBin[] { + const { + fontSize, + swatchSize, + swatchMarginRight, + titleHeight, + verticalBinMargin, + } = 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}` + return this.props.bins.map((series) => { + let label + if (series.type === "categorical") { + label = series.label + } else { + // infer label for numeric bins + label = `${series.minLabel} – ${series.maxLabel}` } const textWrap = new TextWrap({ - maxWidth: this.maxLegendWidth, + maxWidth: this.maxWidth, fontSize, lineHeight: 1, text: label ?? "", }) - const width = rectSize + rectPadding + textWrap.width - const height = Math.max(textWrap.height, rectSize) + const width = swatchSize + swatchMarginRight + textWrap.width + const height = Math.max(textWrap.height, swatchSize) const yOffset = runningYOffset - runningYOffset += height + lineHeight + runningYOffset += height + verticalBinMargin return { textWrap, @@ -100,7 +121,7 @@ export class VerticalColorLegend { } @computed get width(): number { - const widths = this.series.map((series) => series.width) + const widths = this.placedBins.map((series) => series.width) if (this.title) widths.push(this.title.width) return max(widths) ?? 0 } @@ -108,8 +129,8 @@ export class VerticalColorLegend { @computed get height(): number { return ( this.titleHeight + - sum(this.series.map((series) => series.height)) + - this.lineHeight * this.series.length + sum(this.placedBins.map((series) => series.height)) + + this.verticalBinMargin * this.placedBins.length ) } } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx index 884587aa33e..46207409c3d 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx @@ -3,27 +3,34 @@ import React from "react" import { Color, makeIdForHumanConsumption } from "@ourworldindata/utils" import { VerticalColorLegend } from "./VerticalColorLegend" +interface VerticalColorLegendComponentProps { + legend: VerticalColorLegend + + // positioning + x?: number + y?: number + + // state + activeColors?: Color[] // inactive colors are grayed out + focusColors?: Color[] // focused colors are bolded + + // interaction + onClick?: (color: string) => void + onMouseOver?: (color: string) => void + onMouseLeave?: () => void +} + export function VerticalColorLegendComponent({ legend, x = 0, y = 0, activeColors, focusColors, - onLegendClick, - onLegendMouseOver, - onLegendMouseLeave, -}: { - legend: VerticalColorLegend - x?: number - y?: number - activeColors?: Color[] // inactive colors are grayed out - focusColors?: Color[] // focused colors are bolded - onLegendClick?: (color: string) => void - onLegendMouseOver?: (color: string) => void - onLegendMouseLeave?: () => void -}): React.ReactElement { - const isInteractive = - onLegendClick || onLegendMouseOver || onLegendMouseLeave + onClick, + onMouseOver, + onMouseLeave, +}: VerticalColorLegendComponentProps): React.ReactElement { + const isInteractive = onClick || onMouseOver || onMouseLeave return ( - + {isInteractive && ( )} @@ -65,10 +72,10 @@ function Labels({ }): React.ReactElement { return ( - {legend.series.map((series) => { + {legend.placedBins.map((series) => { const isFocus = focusColors?.includes(series.color) ?? false - const textX = x + legend.rectSize + legend.rectPadding + const textX = x + legend.swatchSize + legend.swatchMarginRight const textY = y + series.yOffset return ( @@ -104,10 +111,10 @@ function Swatches({ }): React.ReactElement { return ( - {legend.series.map((series) => { + {legend.placedBins.map((series) => { const isActive = activeColors?.includes(series.color) - const textX = x + legend.rectSize + legend.rectPadding + const textX = x + legend.swatchSize + legend.swatchMarginRight const textY = y + series.yOffset const renderedTextPosition = @@ -118,9 +125,9 @@ function Swatches({ id={makeIdForHumanConsumption(series.textWrap.text)} key={series.textWrap.text} x={x} - y={renderedTextPosition[1] - legend.rectSize} - width={legend.rectSize} - height={legend.rectSize} + y={renderedTextPosition[1] - legend.swatchSize} + width={legend.swatchSize} + height={legend.swatchSize} fill={isActive ? series.color : "#ccc"} /> ) @@ -133,26 +140,26 @@ function InteractiveElement({ x, y, legend, - onLegendClick, - onLegendMouseOver, - onLegendMouseLeave, + onClick, + onMouseOver, + onMouseLeave, }: { x: number y: number legend: VerticalColorLegend - onLegendClick?: (color: string) => void - onLegendMouseOver?: (color: string) => void - onLegendMouseLeave?: () => void + onClick?: (color: string) => void + onMouseOver?: (color: string) => void + onMouseLeave?: () => void }): React.ReactElement { return ( - {legend.series.map((series) => { - const mouseOver = onLegendMouseOver - ? (): void => onLegendMouseOver(series.color) + {legend.placedBins.map((series) => { + const mouseOver = onMouseOver + ? (): void => onMouseOver(series.color) : undefined - const mouseLeave = onLegendMouseLeave - const click = onLegendClick - ? (): void => onLegendClick(series.color) + const mouseLeave = onMouseLeave + const click = onClick + ? (): void => onClick(series.color) : undefined const cursor = click ? "pointer" : "default" @@ -168,9 +175,13 @@ function InteractiveElement({ > From f06153768cb5b8ca7225fd6c627ed2afa65fdc28 Mon Sep 17 00:00:00 2001 From: Sophia Mersmann Date: Sun, 22 Dec 2024 09:41:12 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A8=20break=20vertical=20color=20l?= =?UTF-8?q?egend=20into=20smaller=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VerticalColorLegend.ts | 2 +- .../VerticalColorLegendComponent.tsx | 223 +++++++++--------- 2 files changed, 118 insertions(+), 107 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts index 15c634a3f3d..cc0aa12e1cf 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.ts @@ -26,7 +26,7 @@ export type VerticalColorLegendBin = | VerticalColorLegendCategoricalBin | VerticalColorLegendNumericBin -interface PlacedBin extends Bin { +export interface PlacedBin extends Bin { textWrap: TextWrap width: number height: number diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx index 46207409c3d..47db061305c 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegendComponent.tsx @@ -1,7 +1,7 @@ import React from "react" import { Color, makeIdForHumanConsumption } from "@ourworldindata/utils" -import { VerticalColorLegend } from "./VerticalColorLegend" +import { PlacedBin, VerticalColorLegend } from "./VerticalColorLegend" interface VerticalColorLegendComponentProps { legend: VerticalColorLegend @@ -38,156 +38,167 @@ export function VerticalColorLegendComponent({ className="ScatterColorLegend clickable" > {legend.title && - legend.title.render(x, y, { - textProps: { - fontWeight: 700, - }, - })} - - + legend.title.render(x, y, { textProps: { fontWeight: 700 } })} + + + {legend.placedBins.map((bin) => ( + + + + {legend.placedBins.map((bin) => ( + + ))} + + {isInteractive && ( - + + {legend.placedBins.map((bin) => ( + + ))} + )} ) } -function Labels({ - legend, +function Label({ + bin, x, y, focusColors, + swatchSize, + swatchMarginRight, }: { - legend: VerticalColorLegend + bin: PlacedBin x: number y: number + swatchSize: number + swatchMarginRight: number focusColors?: Color[] }): React.ReactElement { - return ( - - {legend.placedBins.map((series) => { - const isFocus = focusColors?.includes(series.color) ?? false - - const textX = x + legend.swatchSize + legend.swatchMarginRight - const textY = y + series.yOffset - - return ( - - {series.textWrap.render( - textX, - textY, - isFocus - ? { - textProps: { - style: { fontWeight: "bold" }, - }, - } - : undefined - )} - - ) - })} - + const isFocus = focusColors?.includes(bin.color) ?? false + + const textX = x + swatchSize + swatchMarginRight + const textY = y + bin.yOffset + + return bin.textWrap.render( + textX, + textY, + isFocus + ? { + textProps: { + style: { fontWeight: "bold" }, + }, + } + : undefined ) } -function Swatches({ - legend, +function Swatch({ + bin, x, y, + swatchSize, + swatchMarginRight, activeColors, }: { - legend: VerticalColorLegend + bin: PlacedBin x: number y: number + swatchSize: number + swatchMarginRight: number activeColors?: Color[] }): React.ReactElement { - return ( - - {legend.placedBins.map((series) => { - const isActive = activeColors?.includes(series.color) + const isActive = activeColors?.includes(bin.color) - const textX = x + legend.swatchSize + legend.swatchMarginRight - const textY = y + series.yOffset + const textX = x + swatchSize + swatchMarginRight + const textY = y + bin.yOffset - const renderedTextPosition = - series.textWrap.getPositionForSvgRendering(textX, textY) + const renderedTextPosition = bin.textWrap.getPositionForSvgRendering( + textX, + textY + ) - return ( - - ) - })} - + return ( + ) } function InteractiveElement({ + bin, x, y, - legend, + verticalBinMargin, onClick, onMouseOver, onMouseLeave, }: { + bin: PlacedBin x: number y: number - legend: VerticalColorLegend + verticalBinMargin: number onClick?: (color: string) => void onMouseOver?: (color: string) => void onMouseLeave?: () => void }): React.ReactElement { + const mouseOver = onMouseOver + ? (): void => onMouseOver(bin.color) + : undefined + const mouseLeave = onMouseLeave + const click = onClick ? (): void => onClick(bin.color) : undefined + + const cursor = click ? "pointer" : "default" + return ( - - {legend.placedBins.map((series) => { - const mouseOver = onMouseOver - ? (): void => onMouseOver(series.color) - : undefined - const mouseLeave = onMouseLeave - const click = onClick - ? (): void => onClick(series.color) - : undefined - - const cursor = click ? "pointer" : "default" - - return ( - - - - ) - })} + + ) }