Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔨 drop manager pattern for color legends #4346

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading