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 horizontal color legend / TAS-800 #4369

Draft
wants to merge 17 commits into
base: refactor-vertical-color-legend
Choose a base branch
from
Draft
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
41 changes: 28 additions & 13 deletions packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ import { HorizontalAxisZeroLine } from "../axis/AxisViews"
import { NoDataModal } from "../noDataModal/NoDataModal"
import { AxisConfig, AxisManager } from "../axis/AxisConfig"
import { ColorSchemes } from "../color/ColorSchemes"
import { ChartInterface } from "../chart/ChartInterface"
import { ChartInterface, ExternalLegendProps } from "../chart/ChartInterface"
import {
BACKGROUND_COLOR,
DiscreteBarChartManager,
@@ -70,12 +70,10 @@ import {
OWID_NO_DATA_GRAY,
} from "../color/ColorConstants"
import { CategoricalBin, ColorScaleBin } from "../color/ColorScaleBin"
import {
HorizontalColorLegendManager,
HorizontalNumericColorLegend,
} from "../horizontalColorLegend/HorizontalColorLegends"
import { BaseType, Selection } from "d3"
import { TextWrap } from "@ourworldindata/components"
import { HorizontalNumericColorLegend } from "../horizontalColorLegend/HorizontalNumericColorLegend"
import { HorizontalNumericColorLegendComponent } from "../horizontalColorLegend/HorizontalNumericColorLegendComponent"

const labelToTextPadding = 10
const labelToBarPadding = 5
@@ -170,7 +168,7 @@ export class DiscreteBarChart

@computed private get boundsWithoutColorLegend(): Bounds {
return this.bounds.padTop(
this.showColorLegend ? this.legendHeight + LEGEND_PADDING : 0
this.numericLegend ? this.legendHeight + LEGEND_PADDING : 0
)
}

@@ -504,8 +502,12 @@ export class DiscreteBarChart
return (
<>
{this.renderDefs()}
{this.showColorLegend && (
<HorizontalNumericColorLegend manager={this} />
{this.numericLegend && (
<HorizontalNumericColorLegendComponent
legend={this.numericLegend}
binStrokeColor={this.numericBinStroke}
textColor={this.legendTextColor}
/>
)}
{!this.isLogScale && (
<HorizontalAxisZeroLine
@@ -823,10 +825,10 @@ export class DiscreteBarChart
return DEFAULT_PROJECTED_DATA_COLOR_IN_LEGEND
}

@computed get externalLegend(): HorizontalColorLegendManager | undefined {
@computed get externalLegend(): ExternalLegendProps | undefined {
if (this.hasColorLegend) {
return {
numericLegendData: this.numericLegendData,
numericBins: this.numericLegendData,
}
}
return undefined
@@ -843,9 +845,22 @@ export class DiscreteBarChart
legendTextColor = "#555"
legendTickSize = 1

@computed get numericLegend(): HorizontalNumericColorLegend | undefined {
return this.hasColorScale && this.manager.showLegend
? new HorizontalNumericColorLegend({ manager: this })
@computed private get numericLegend():
| HorizontalNumericColorLegend
| undefined {
return this.showColorLegend
? new HorizontalNumericColorLegend({
fontSize: this.fontSize,
x: this.legendX,
align: this.legendAlign,
maxWidth: this.legendMaxWidth,
numericBins: this.numericLegendData,
binSize: this.numericBinSize,
equalSizeBins: this.equalSizeBins,
title: this.legendTitle,
y: this.numericLegendY,
tickSize: this.legendTickSize,
})
: undefined
}

10 changes: 8 additions & 2 deletions packages/@ourworldindata/grapher/src/chart/ChartInterface.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,9 @@ import {
} from "@ourworldindata/types"
import { ColorScale } from "../color/ColorScale"
import { HorizontalAxis, VerticalAxis } from "../axis/Axis"
import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends"
import { HorizontalCategoricalColorLegendProps } from "../horizontalColorLegend/HorizontalCategoricalColorLegend"
import { HorizontalNumericColorLegendProps } from "../horizontalColorLegend/HorizontalNumericColorLegend"

// 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.
@@ -19,6 +21,10 @@ export interface ChartSeries {

export type ChartTableTransformer = (inputTable: OwidTable) => OwidTable

export type ExternalLegendProps =
Partial<HorizontalCategoricalColorLegendProps> &
Partial<HorizontalNumericColorLegendProps>

export interface ChartInterface {
failMessage: string // We require every chart have some fail message(s) to show to the user if something went wrong

@@ -43,7 +49,7 @@ 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.
*/
externalLegend?: HorizontalColorLegendManager
externalLegend?: ExternalLegendProps

/**
* Which facet strategies the chart type finds reasonable in its current setting, if any.
166 changes: 83 additions & 83 deletions packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ import {
DefaultChartClass,
} from "../chart/ChartTypeMap"
import { ChartManager } from "../chart/ChartManager"
import { ChartInterface } from "../chart/ChartInterface"
import { ChartInterface, ExternalLegendProps } from "../chart/ChartInterface"
import {
getChartPadding,
getFontSize,
@@ -53,18 +53,22 @@ import { autoDetectYColumnSlugs, makeSelectionArray } from "../chart/ChartUtils"
import { SelectionArray } from "../selection/SelectionArray"
import { AxisConfig } from "../axis/AxisConfig"
import { HorizontalAxis, VerticalAxis } from "../axis/Axis"
import {
HorizontalCategoricalColorLegend,
HorizontalColorLegend,
HorizontalColorLegendManager,
HorizontalNumericColorLegend,
} from "../horizontalColorLegend/HorizontalColorLegends"
import {
CategoricalBin,
ColorScaleBin,
NumericBin,
} from "../color/ColorScaleBin"
import { GRAPHER_DARK_TEXT } from "../color/ColorConstants"
import {
HorizontalCategoricalColorLegend,
HorizontalCategoricalColorLegendProps,
} from "../horizontalColorLegend/HorizontalCategoricalColorLegend"
import {
HorizontalNumericColorLegend,
HorizontalNumericColorLegendProps,
} from "../horizontalColorLegend/HorizontalNumericColorLegend"
import { HorizontalNumericColorLegendComponent } from "../horizontalColorLegend/HorizontalNumericColorLegendComponent"
import { HorizontalCategoricalColorLegendComponent } from "../horizontalColorLegend/HorizontalCategoricalColorLegendComponent"

const SHARED_X_AXIS_MIN_FACET_COUNT = 12

@@ -118,7 +122,7 @@ interface AxesInfo {
@observer
export class FacetChart
extends React.Component<FacetChartProps>
implements ChartInterface, HorizontalColorLegendManager
implements ChartInterface
{
transformTable(table: OwidTable): OwidTable {
return table
@@ -589,7 +593,8 @@ export class FacetChart

// legend utils

@computed private get externalLegends(): HorizontalColorLegendManager[] {
@computed
private get externalLegends(): ExternalLegendProps[] {
return excludeUndefined(
this.intermediateChartInstances.map(
(instance) => instance.externalLegend
@@ -599,18 +604,10 @@ export class FacetChart

@computed private get isNumericLegend(): boolean {
return this.externalLegends.some((legend) =>
legend.numericLegendData?.some((bin) => bin instanceof NumericBin)
legend.numericBins?.some((bin) => bin instanceof NumericBin)
)
}

@computed private get LegendClass():
| typeof HorizontalNumericColorLegend
| typeof HorizontalCategoricalColorLegend {
return this.isNumericLegend
? HorizontalNumericColorLegend
: HorizontalCategoricalColorLegend
}

@computed private get showLegend(): boolean {
const { isNumericLegend, categoricalLegendData, numericLegendData } =
this
@@ -641,9 +638,9 @@ export class FacetChart
return false
}

private getExternalLegendProp<
Prop extends keyof HorizontalColorLegendManager,
>(prop: Prop): HorizontalColorLegendManager[Prop] | undefined {
private getExternalLegendProp<Prop extends keyof ExternalLegendProps>(
prop: Prop
): ExternalLegendProps[Prop] | undefined {
for (const externalLegend of this.externalLegends) {
if (externalLegend[prop] !== undefined) {
return externalLegend[prop]
@@ -667,64 +664,35 @@ 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(): ExternalLegendProps {
return {
fontSize: this.fontSize,
x: this.bounds.x,
maxWidth: this.bounds.width,
align: HorizontalAlign.left,
}
}

@computed get numericBinStrokeWidth(): number | undefined {
return this.getExternalLegendProp("numericBinStrokeWidth")
@computed
private get numericLegendProps(): HorizontalNumericColorLegendProps {
return {
...this.commonLegendProps,
y: this.bounds.top,
title: this.getExternalLegendProp("title"),
tickSize: this.getExternalLegendProp("tickSize"),
binSize: this.getExternalLegendProp("binSize"),
equalSizeBins: this.getExternalLegendProp("equalSizeBins"),
numericBins: this.numericLegendData,
}
}

@computed get equalSizeBins(): boolean | undefined {
return this.getExternalLegendProp("equalSizeBins")
@computed
private get categoricalLegendProps(): HorizontalCategoricalColorLegendProps {
return {
...this.commonLegendProps,
categoricalBins: this.categoricalLegendData,
}
}

@computed get hoverColors(): Color[] | undefined {
@@ -752,8 +720,8 @@ export class FacetChart
if (!this.isNumericLegend || !this.hideFacetLegends) return []
const allBins: ColorScaleBin[] = this.externalLegends.flatMap(
(legend) => [
...(legend.numericLegendData ?? []),
...(legend.categoricalLegendData ?? []),
...(legend.numericBins ?? []),
...(legend.categoricalBins ?? []),
]
)
const uniqBins = this.getUniqBins(allBins)
@@ -768,8 +736,8 @@ export class FacetChart
if (this.isNumericLegend || !this.hideFacetLegends) return []
const allBins: CategoricalBin[] = this.externalLegends
.flatMap((legend) => [
...(legend.numericLegendData ?? []),
...(legend.categoricalLegendData ?? []),
...(legend.numericBins ?? []),
...(legend.categoricalBins ?? []),
])
.filter((bin) => bin instanceof CategoricalBin) as CategoricalBin[]
const uniqBins = this.getUniqBins(allBins)
@@ -814,8 +782,18 @@ export class FacetChart

// end of legend props

@computed private get legend(): HorizontalColorLegend {
return new this.LegendClass({ manager: this })
@computed private get categoryLegend(): HorizontalCategoricalColorLegend {
return new HorizontalCategoricalColorLegend(this.categoricalLegendProps)
}

@computed private get numericLegend(): HorizontalNumericColorLegend {
return new HorizontalNumericColorLegend(this.numericLegendProps)
}

@computed private get legend():
| HorizontalNumericColorLegend
| HorizontalCategoricalColorLegend {
return this.isNumericLegend ? this.numericLegend : this.categoryLegend
}

@computed private get isFocusModeSupported(): boolean {
@@ -866,11 +844,33 @@ export class FacetChart
return { fontSize, shortenedLabel: label }
}

private renderLegend(): React.ReactElement {
return this.isNumericLegend ? (
<HorizontalNumericColorLegendComponent
legend={this.numericLegend}
x={this.bounds.x}
onMouseOver={this.onLegendMouseOver}
onMouseLeave={this.onLegendMouseLeave}
/>
) : (
<HorizontalCategoricalColorLegendComponent
legend={this.categoryLegend}
x={this.bounds.x}
y={this.bounds.top}
onMouseOver={this.onLegendMouseOver}
onMouseLeave={this.onLegendMouseLeave}
hoverColors={this.hoverColors}
activeColors={this.activeColors}
onClick={this.onLegendClick}
/>
)
}

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) ??
Loading
Loading