Skip to content

Commit

Permalink
🔨 drop manager pattern for horizontal color legend
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Dec 20, 2024
1 parent 49d4482 commit c549549
Show file tree
Hide file tree
Showing 17 changed files with 549 additions and 346 deletions.
36 changes: 26 additions & 10 deletions packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ import {
} from "../color/ColorConstants"
import { CategoricalBin, ColorScaleBin } from "../color/ColorScaleBin"
import {
HorizontalColorLegendManager,
HorizontalNumericColorLegend,
HorizontalNumericColorLegendProps,
} from "../horizontalColorLegend/HorizontalColorLegends"
import { BaseType, Selection } from "d3"
import { TextWrap } from "@ourworldindata/components"
Expand Down Expand Up @@ -514,7 +514,7 @@ export class DiscreteBarChart
<>
{this.renderDefs()}
{this.showColorLegend && (
<HorizontalNumericColorLegend manager={this} />
<HorizontalNumericColorLegend {...this.legendProps} />
)}
{this.showHorizontalAxis && (
<>
Expand Down Expand Up @@ -833,14 +833,34 @@ export class DiscreteBarChart
return sortBy(legendBins, (bin) => bin instanceof CategoricalBin)
}

@computed
private get legendProps(): HorizontalNumericColorLegendProps {
return {
fontSize: this.fontSize,
legendX: this.legendX,
legendAlign: this.legendAlign,
legendMaxWidth: this.legendMaxWidth,
numericLegendData: this.numericLegendData,
numericBinSize: this.numericBinSize,
numericBinStroke: this.numericBinStroke,
equalSizeBins: this.equalSizeBins,
legendTitle: this.legendTitle,
numericLegendY: this.numericLegendY,
legendTextColor: this.legendTextColor,
legendTickSize: this.legendTickSize,
}
}

@computed get projectedDataColorInLegend(): string {
// if a single color is in use, use that color in the legend
if (uniqBy(this.series, "color").length === 1)
return this.series[0].color
return DEFAULT_PROJECTED_DATA_COLOR_IN_LEGEND
}

@computed get externalLegend(): HorizontalColorLegendManager | undefined {
@computed get externalNumericLegend():
| HorizontalNumericColorLegendProps
| undefined {
if (this.hasColorLegend) {
return {
numericLegendData: this.numericLegendData,
Expand All @@ -860,10 +880,10 @@ export class DiscreteBarChart
legendTextColor = "#555"
legendTickSize = 1

@computed get numericLegend(): HorizontalNumericColorLegend | undefined {
@computed get legendHeight(): number {
return this.hasColorScale && this.manager.showLegend
? new HorizontalNumericColorLegend({ manager: this })
: undefined
? HorizontalNumericColorLegend.height(this.legendProps)
: 0
}

@computed get numericLegendY(): number {
Expand All @@ -876,10 +896,6 @@ export class DiscreteBarChart
: undefined
}

@computed get legendHeight(): number {
return this.numericLegend?.height ?? 0
}

// End of color legend props

@computed get series(): DiscreteBarSeries[] {
Expand Down
14 changes: 11 additions & 3 deletions packages/@ourworldindata/grapher/src/chart/ChartInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
} from "@ourworldindata/types"
import { ColorScale } from "../color/ColorScale"
import { HorizontalAxis, VerticalAxis } from "../axis/Axis"
import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends"
import {
HorizontalCategoricalColorLegendProps,
HorizontalNumericColorLegendProps,
} from "../horizontalColorLegend/HorizontalColorLegends"
// The idea of this interface is to try and start reusing more code across our Chart classes and make it easier
// for a dev to work on a chart type they haven't touched before if they've worked with another that implements
// this interface.
Expand Down Expand Up @@ -41,9 +44,14 @@ export interface ChartInterface {

/**
* The legend that has been hidden from the chart plot (using `manager.hideLegend`).
* Used to create a global legend for faceted charts.
* Used to create a global categorical legend for faceted charts.
*/
externalCategoricalLegend?: HorizontalCategoricalColorLegendProps
/**
* The legend that has been hidden from the chart plot (using `manager.hideLegend`).
* Used to create a global numeric legend for faceted charts.
*/
externalLegend?: HorizontalColorLegendManager
externalNumericLegend?: HorizontalNumericColorLegendProps

/**
* Which facet strategies the chart type finds reasonable in its current setting, if any.
Expand Down
185 changes: 98 additions & 87 deletions packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ import { AxisConfig } from "../axis/AxisConfig"
import { HorizontalAxis, VerticalAxis } from "../axis/Axis"
import {
HorizontalCategoricalColorLegend,
HorizontalCategoricalColorLegendProps,
HorizontalColorLegend,
HorizontalColorLegendManager,
HorizontalColorLegendProps,
HorizontalNumericColorLegend,
HorizontalNumericColorLegendProps,
} from "../horizontalColorLegend/HorizontalColorLegends"
import {
CategoricalBin,
Expand Down Expand Up @@ -118,7 +120,7 @@ interface AxesInfo {
@observer
export class FacetChart
extends React.Component<FacetChartProps>
implements ChartInterface, HorizontalColorLegendManager
implements ChartInterface
{
transformTable(table: OwidTable): OwidTable {
return table
Expand Down Expand Up @@ -589,26 +591,28 @@ export class FacetChart

// legend utils

@computed private get externalLegends(): HorizontalColorLegendManager[] {
@computed
private get externalCategoricalLegends(): HorizontalCategoricalColorLegendProps[] {
return excludeUndefined(
this.intermediateChartInstances.map(
(instance) => instance.externalLegend
(instance) => instance.externalCategoricalLegend
)
)
}

@computed private get isNumericLegend(): boolean {
return this.externalLegends.some((legend) =>
legend.numericLegendData?.some((bin) => bin instanceof NumericBin)
@computed
private get externalNumericLegends(): HorizontalNumericColorLegendProps[] {
return excludeUndefined(
this.intermediateChartInstances.map(
(instance) => instance.externalNumericLegend
)
)
}

@computed private get LegendClass():
| typeof HorizontalNumericColorLegend
| typeof HorizontalCategoricalColorLegend {
return this.isNumericLegend
? HorizontalNumericColorLegend
: HorizontalCategoricalColorLegend
@computed private get isNumericLegend(): boolean {
return this.externalNumericLegends.some((legend) =>
legend.numericLegendData.some((bin) => bin instanceof NumericBin)
)
}

@computed private get showLegend(): boolean {
Expand Down Expand Up @@ -641,10 +645,21 @@ export class FacetChart
return false
}

private getExternalLegendProp<
Prop extends keyof HorizontalColorLegendManager,
>(prop: Prop): HorizontalColorLegendManager[Prop] | undefined {
for (const externalLegend of this.externalLegends) {
private getCategoricalExternalLegendProp<
Prop extends keyof HorizontalCategoricalColorLegendProps,
>(prop: Prop): HorizontalCategoricalColorLegendProps[Prop] | undefined {
for (const externalLegend of this.externalCategoricalLegends) {
if (externalLegend[prop] !== undefined) {
return externalLegend[prop]
}
}
return undefined
}

private getNumericExternalLegendProp<
Prop extends keyof HorizontalNumericColorLegendProps,
>(prop: Prop): HorizontalNumericColorLegendProps[Prop] | undefined {
for (const externalLegend of this.externalNumericLegends) {
if (externalLegend[prop] !== undefined) {
return externalLegend[prop]
}
Expand All @@ -667,64 +682,50 @@ export class FacetChart

// legend props

@computed get legendX(): number {
return this.bounds.x
}

@computed get numericLegendY(): number {
return this.bounds.top
}

@computed get categoryLegendY(): number {
return this.bounds.top
}

@computed get legendMaxWidth(): number {
return this.bounds.width
}

@computed get legendAlign(): HorizontalAlign {
return HorizontalAlign.left
}

@computed get legendTitle(): string | undefined {
return this.getExternalLegendProp("legendTitle")
}

@computed get legendHeight(): number | undefined {
return this.getExternalLegendProp("legendHeight")
}

@computed get legendOpacity(): number | undefined {
return this.getExternalLegendProp("legendOpacity")
}

@computed get legendTextColor(): Color | undefined {
return this.getExternalLegendProp("legendTextColor")
}

@computed get legendTickSize(): number | undefined {
return this.getExternalLegendProp("legendTickSize")
}

@computed get categoricalBinStroke(): Color | undefined {
return this.getExternalLegendProp("categoricalBinStroke")
}

@computed get numericBinSize(): number | undefined {
return this.getExternalLegendProp("numericBinSize")
}

@computed get numericBinStroke(): Color | undefined {
return this.getExternalLegendProp("numericBinStroke")
@computed private get commonLegendProps(): HorizontalColorLegendProps {
return {
fontSize: this.fontSize,
legendX: this.bounds.x,
legendMaxWidth: this.bounds.width,
legendAlign: HorizontalAlign.left,
onLegendMouseOver: this.onLegendMouseOver,
onLegendMouseLeave: this.onLegendMouseLeave,
}
}

@computed get numericBinStrokeWidth(): number | undefined {
return this.getExternalLegendProp("numericBinStrokeWidth")
@computed
private get numericLegendProps(): HorizontalNumericColorLegendProps {
return {
...this.commonLegendProps,
numericLegendY: this.bounds.top,
legendTitle: this.getNumericExternalLegendProp("legendTitle"),
legendTextColor:
this.getNumericExternalLegendProp("legendTextColor"),
legendTickSize: this.getNumericExternalLegendProp("legendTickSize"),
numericBinSize: this.getNumericExternalLegendProp("numericBinSize"),
numericBinStroke:
this.getNumericExternalLegendProp("numericBinStroke"),
numericBinStrokeWidth: this.getNumericExternalLegendProp(
"numericBinStrokeWidth"
),
equalSizeBins: this.getNumericExternalLegendProp("equalSizeBins"),
numericLegendData: this.numericLegendData,
}
}

@computed get equalSizeBins(): boolean | undefined {
return this.getExternalLegendProp("equalSizeBins")
@computed
private get categoricalLegendProps(): HorizontalCategoricalColorLegendProps {
return {
...this.commonLegendProps,
categoryLegendY: this.bounds.top,
categoricalBinStroke: this.getCategoricalExternalLegendProp(
"categoricalBinStroke"
),
hoverColors: this.hoverColors,
activeColors: this.activeColors,
categoricalLegendData: this.categoricalLegendData,
onLegendClick: this.onLegendClick,
}
}

@computed get hoverColors(): Color[] | undefined {
Expand All @@ -750,13 +751,14 @@ export class FacetChart

@computed get numericLegendData(): ColorScaleBin[] {
if (!this.isNumericLegend || !this.hideFacetLegends) return []
const allBins: ColorScaleBin[] = this.externalLegends.flatMap(
(legend) => [
...(legend.numericLegendData ?? []),
...(legend.categoricalLegendData ?? []),
]
)
const uniqBins = this.getUniqBins(allBins)
const uniqBins = this.getUniqBins([
...this.externalCategoricalLegends.flatMap(
(legend) => legend.categoricalLegendData
),
...this.externalNumericLegends.flatMap(
(legend) => legend.numericLegendData
),
])
const sortedBins = sortBy(
uniqBins,
(bin) => bin instanceof CategoricalBin
Expand All @@ -766,12 +768,9 @@ export class FacetChart

@computed get categoricalLegendData(): CategoricalBin[] {
if (this.isNumericLegend || !this.hideFacetLegends) return []
const allBins: CategoricalBin[] = this.externalLegends
.flatMap((legend) => [
...(legend.numericLegendData ?? []),
...(legend.categoricalLegendData ?? []),
])
.filter((bin) => bin instanceof CategoricalBin) as CategoricalBin[]
const allBins = this.externalCategoricalLegends.flatMap(
(legend) => legend.categoricalLegendData
)
const uniqBins = this.getUniqBins(allBins)
const newBins = uniqBins.map(
// remap index to ensure it's unique (the above procedure can lead to duplicates)
Expand Down Expand Up @@ -814,7 +813,9 @@ export class FacetChart
// end of legend props

@computed private get legend(): HorizontalColorLegend {
return new this.LegendClass({ manager: this })
return this.isNumericLegend
? new HorizontalNumericColorLegend(this.numericLegendProps)
: new HorizontalCategoricalColorLegend(this.categoricalLegendProps)
}

/**
Expand Down Expand Up @@ -858,11 +859,21 @@ export class FacetChart
return { fontSize, shortenedLabel: label }
}

private renderLegend(): React.ReactElement {
return this.isNumericLegend ? (
<HorizontalNumericColorLegend {...this.numericLegendProps} />
) : (
<HorizontalCategoricalColorLegend
{...this.categoricalLegendProps}
/>
)
}

render(): React.ReactElement {
const { facetFontSize, LegendClass, showLegend } = this
const { facetFontSize, showLegend } = this
return (
<React.Fragment>
{showLegend && <LegendClass manager={this} />}
{showLegend && this.renderLegend()}
{this.placedSeries.map((facetChart, index: number) => {
const ChartClass =
ChartComponentClassMap.get(this.chartTypeName) ??
Expand Down
Loading

0 comments on commit c549549

Please sign in to comment.