diff --git a/packages/@ourworldindata/grapher/src/axis/Axis.ts b/packages/@ourworldindata/grapher/src/axis/Axis.ts index eb9f2e9159f..1f0fce8edcf 100644 --- a/packages/@ourworldindata/grapher/src/axis/Axis.ts +++ b/packages/@ourworldindata/grapher/src/axis/Axis.ts @@ -23,7 +23,6 @@ import { OwidVariableRoundingMode, } from "@ourworldindata/utils" import { AxisConfig, AxisManager } from "./AxisConfig" -import { MarkdownTextWrap } from "@ourworldindata/components" import { ColumnTypeMap, CoreColumn } from "@ourworldindata/core-table" import { GRAPHER_FONT_SCALE_12 } from "../core/GrapherConstants.js" @@ -73,7 +72,6 @@ abstract class AbstractAxis { @observable hideFractionalTicks = false @observable.struct range: ValueRange = [0, 0] @observable private _scaleType?: ScaleType - @observable private _label?: string constructor(config: AxisConfig, axisManager?: AxisManager) { this.config = config @@ -88,7 +86,6 @@ abstract class AbstractAxis { */ abstract get size(): number abstract get orient(): Position - abstract get labelWidth(): number abstract placeTickLabel(value: number): TickLabelPlacement abstract get tickLabels(): TickLabelPlacement[] @@ -101,8 +98,8 @@ abstract class AbstractAxis { return this.config.hideGridlines ?? false } - @computed get labelPadding(): number { - return this.config.labelPadding ?? 5 + @computed get tickPadding(): number { + return this.config.tickPadding ?? 5 } @computed get nice(): boolean { @@ -133,14 +130,6 @@ abstract class AbstractAxis { this._scaleType = value } - @computed get label(): string { - return this._label ?? this.config.label ?? "" - } - - set label(value: string) { - this._label = value - } - // This will expand the domain but never shrink. // This will change the min unless the user's min setting is less // This will change the max unless the user's max setting is greater @@ -167,7 +156,6 @@ abstract class AbstractAxis { this.hideFractionalTicks = parentAxis.hideFractionalTicks this.range = parentAxis.range.slice() as ValueRange this._scaleType = parentAxis._scaleType - this._label = parentAxis._label return this } @@ -479,24 +467,6 @@ abstract class AbstractAxis { tick.toString() ) } - - @computed get labelFontSize(): number { - return GRAPHER_FONT_SCALE_12 * this.fontSize - } - - @computed get labelTextWrap(): MarkdownTextWrap | undefined { - const text = this.label - return text - ? new MarkdownTextWrap({ - maxWidth: this.labelWidth, - fontSize: this.labelFontSize, - text, - lineHeight: 1, - detailsOrderedByReference: - this.axisManager?.detailsOrderedByReference, - }) - : undefined - } } export class HorizontalAxis extends AbstractAxis { @@ -511,27 +481,15 @@ export class HorizontalAxis extends AbstractAxis { : Position.bottom } - @computed get labelOffset(): number { - return this.labelTextWrap - ? this.labelTextWrap.height + this.labelPadding * 2 - : 0 - } - - @computed get labelWidth(): number { - return this.rangeSize - } - // note that we intentionally don't take `hideAxisLabels` into account here. // tick labels might be hidden in faceted charts. when faceted, it's important // the axis size doesn't change as a result of hiding the axis labels, or else // we might end up with misaligned axes. @computed get height(): number { if (this.hideAxis) return 0 - const { labelOffset, labelPadding } = this + const { tickPadding } = this const maxTickHeight = max(this.tickLabels.map((tick) => tick.height)) - const height = maxTickHeight - ? maxTickHeight + labelOffset + labelPadding - : 0 + const height = maxTickHeight ? maxTickHeight + tickPadding : 0 return Math.max(height, this.config.minSize ?? 0) } @@ -630,28 +588,16 @@ export class VerticalAxis extends AbstractAxis { return Position.left } - @computed get labelWidth(): number { - return this.height - } - - @computed get labelOffset(): number { - return this.labelTextWrap - ? this.labelTextWrap.height + this.labelPadding * 2 - : 0 - } - // note that we intentionally don't take `hideAxisLabels` into account here. // tick labels might be hidden in faceted charts. when faceted, it's important // the axis size doesn't change as a result of hiding the axis labels, or else // we might end up with misaligned axes. @computed get width(): number { if (this.hideAxis) return 0 - const { labelOffset, labelPadding } = this + const { tickPadding } = this const maxTickWidth = max(this.tickLabels.map((tick) => tick.width)) const width = - maxTickWidth !== undefined - ? maxTickWidth + labelOffset + labelPadding - : 0 + maxTickWidth !== undefined ? maxTickWidth + tickPadding : 0 return Math.max(width, this.config.minSize ?? 0) } diff --git a/packages/@ourworldindata/grapher/src/axis/AxisConfig.ts b/packages/@ourworldindata/grapher/src/axis/AxisConfig.ts index 676da96add0..d469ebe3010 100644 --- a/packages/@ourworldindata/grapher/src/axis/AxisConfig.ts +++ b/packages/@ourworldindata/grapher/src/axis/AxisConfig.ts @@ -20,7 +20,6 @@ import { export interface AxisManager { fontSize: number - detailsOrderedByReference?: string[] } class AxisConfigDefaults implements AxisConfigInterface { @@ -33,7 +32,7 @@ class AxisConfigDefaults implements AxisConfigInterface { @observable.ref hideAxis?: boolean = undefined @observable.ref hideGridlines?: boolean = undefined @observable.ref hideTickLabels?: boolean = undefined - @observable.ref labelPadding?: number = undefined + @observable.ref tickPadding?: number = undefined @observable.ref nice?: boolean = undefined @observable.ref maxTicks?: number = undefined @observable.ref tickFormattingOptions?: TickFormattingOptions = undefined @@ -89,7 +88,7 @@ export class AxisConfig hideAxis: this.hideAxis, hideGridlines: this.hideGridlines, hideTickLabels: this.hideTickLabels, - labelPadding: this.labelPadding, + tickPadding: this.tickPadding, nice: this.nice, maxTicks: this.maxTicks, tickFormattingOptions: this.tickFormattingOptions, diff --git a/packages/@ourworldindata/grapher/src/axis/AxisViews.tsx b/packages/@ourworldindata/grapher/src/axis/AxisViews.tsx index bd4c533dc97..63a462f5b88 100644 --- a/packages/@ourworldindata/grapher/src/axis/AxisViews.tsx +++ b/packages/@ourworldindata/grapher/src/axis/AxisViews.tsx @@ -15,6 +15,8 @@ import { VerticalAxis, HorizontalAxis, DualAxis } from "./Axis" import classNames from "classnames" import { GRAPHER_DARK_TEXT } from "../color/ColorConstants" import { ScaleType, DetailsMarker } from "@ourworldindata/types" +import { MarkdownTextWrap } from "@ourworldindata/components" +import { GRAPHER_AXIS_LABEL_PADDING } from "../core/GrapherConstants.js" const dasharrayFromFontSize = (fontSize: number): string => { const dashLength = Math.round((fontSize / 16) * 3) @@ -172,11 +174,9 @@ interface DualAxisViewProps { dualAxis: DualAxis highlightValue?: { x: number; y: number } showTickMarks?: boolean - labelColor?: string tickColor?: string lineWidth?: number gridDashPattern?: string - detailsMarker?: DetailsMarker } @observer @@ -185,11 +185,9 @@ export class DualAxisComponent extends React.Component { const { dualAxis, showTickMarks, - labelColor, tickColor, lineWidth, gridDashPattern, - detailsMarker, } = this.props const { bounds, horizontalAxis, verticalAxis, innerBounds } = dualAxis @@ -215,9 +213,7 @@ export class DualAxisComponent extends React.Component { ) @@ -227,10 +223,8 @@ export class DualAxisComponent extends React.Component { axis={horizontalAxis} showTickMarks={showTickMarks} preferredAxisPosition={innerBounds.bottom} - labelColor={labelColor} tickColor={tickColor} tickMarkWidth={lineWidth} - detailsMarker={detailsMarker} /> ) @@ -250,42 +244,17 @@ export class VerticalAxisComponent extends React.Component<{ bounds: Bounds verticalAxis: VerticalAxis showTickMarks?: boolean - labelColor?: string tickColor?: string - detailsMarker?: DetailsMarker }> { render(): React.ReactElement { - const { - bounds, - verticalAxis, - labelColor, - tickColor, - detailsMarker, - showTickMarks, - } = this.props - const { tickLabels, labelTextWrap, config } = verticalAxis + const { bounds, verticalAxis, tickColor, showTickMarks } = this.props + const { tickLabels, config } = verticalAxis return ( - {labelTextWrap && - labelTextWrap.renderSVG( - -verticalAxis.rangeCenter, - bounds.left, - { - id: makeIdForHumanConsumption( - "vertical-axis-label" - ), - textProps: { - transform: "rotate(-90)", - fill: labelColor || GRAPHER_DARK_TEXT, - textAnchor: "middle", - }, - detailsMarker, - } - )} {showTickMarks && ( {tickLabels.map((label, i) => ( @@ -315,7 +284,7 @@ export class VerticalAxisComponent extends React.Component<{ x={( bounds.left + verticalAxis.width - - verticalAxis.labelPadding + verticalAxis.tickPadding ).toFixed(2)} y={y} dy={dyFromAlign( @@ -343,10 +312,8 @@ export class HorizontalAxisComponent extends React.Component<{ axis: HorizontalAxis showTickMarks?: boolean preferredAxisPosition?: number - labelColor?: string tickColor?: string tickMarkWidth?: number - detailsMarker?: DetailsMarker }> { @computed get scaleType(): ScaleType { return this.props.axis.scaleType @@ -370,25 +337,20 @@ export class HorizontalAxisComponent extends React.Component<{ axis, showTickMarks, preferredAxisPosition, - labelColor, tickColor, tickMarkWidth = 1, - detailsMarker, } = this.props - const { tickLabels, labelTextWrap: label, labelOffset, orient } = axis + const { tickLabels, orient } = axis const tickSize = 5 const horizontalAxisLabelsOnTop = orient === Position.top - const labelYPosition = horizontalAxisLabelsOnTop - ? bounds.top - : bounds.bottom - (label?.height ?? 0) const tickMarksYPosition = horizontalAxisLabelsOnTop ? bounds.top + axis.height - 5 : (preferredAxisPosition ?? bounds.bottom) const tickLabelYPlacement = horizontalAxisLabelsOnTop - ? bounds.top + labelOffset + 10 - : bounds.bottom - labelOffset + ? bounds.top + 10 + : bounds.bottom const showTickLabels = !axis.config.hideTickLabels @@ -397,15 +359,6 @@ export class HorizontalAxisComponent extends React.Component<{ id={makeIdForHumanConsumption("horizontal-axis")} className="HorizontalAxis" > - {label && - label.renderSVG(axis.rangeCenter, labelYPosition, { - id: makeIdForHumanConsumption("horizontal-axis-label"), - textProps: { - fill: labelColor || GRAPHER_DARK_TEXT, - textAnchor: "middle", - }, - detailsMarker, - })} {showTickMarks && ( {tickLabels.map((label) => ( @@ -472,3 +425,64 @@ export class VerticalAxisTickMark extends React.Component<{ ) } } + +export function HorizonalAxisLabel({ + textWrap, + dualAxis, + padding = GRAPHER_AXIS_LABEL_PADDING, + color = GRAPHER_DARK_TEXT, + detailsMarker, +}: { + textWrap: MarkdownTextWrap + dualAxis: DualAxis + padding?: number + color?: string + detailsMarker?: DetailsMarker +}): React.ReactElement | null { + const { horizontalAxis, bounds } = dualAxis + + const horizontalAxisLabelsOnTop = horizontalAxis.orient === Position.top + + const x = horizontalAxis.rangeCenter + const y = horizontalAxisLabelsOnTop + ? bounds.top - textWrap.height - padding + : bounds.bottom + padding + + return textWrap.renderSVG(x, y, { + id: makeIdForHumanConsumption("horizontal-axis-label"), + textProps: { + fill: color || GRAPHER_DARK_TEXT, + textAnchor: "middle", + }, + detailsMarker, + }) +} + +export function VerticalAxisLabel({ + textWrap, + dualAxis, + padding = GRAPHER_AXIS_LABEL_PADDING, + color = GRAPHER_DARK_TEXT, + detailsMarker, +}: { + textWrap: MarkdownTextWrap + dualAxis: DualAxis + padding?: number + color?: string + detailsMarker?: DetailsMarker +}): React.ReactElement | null { + const { verticalAxis, bounds } = dualAxis + + const x = -verticalAxis.rangeCenter + const y = bounds.left - textWrap.height - padding + + return textWrap.renderSVG(x, y, { + id: makeIdForHumanConsumption("vertical-axis-label"), + textProps: { + transform: "rotate(-90)", + fill: color || GRAPHER_DARK_TEXT, + textAnchor: "middle", + }, + detailsMarker, + }) +} diff --git a/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx b/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx index 3f79cc7bdec..d1ed968d0bf 100644 --- a/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx @@ -298,7 +298,6 @@ export class DiscreteBarChart axis.formatColumn = this.yColumns[0] // todo: does this work for columns as series? axis.range = this.xRange - axis.label = "" return axis } @@ -522,7 +521,6 @@ export class DiscreteBarChart bounds={boundsWithoutColorLegend} axis={yAxis} preferredAxisPosition={innerBounds.bottom} - labelColor={manager.secondaryColorInStaticCharts} tickMarkWidth={axisLineWidth} /> { if (manager.yColumnSlugs && manager.yColumnSlugs.length) @@ -228,3 +231,23 @@ export function byHoverThenFocusState(series: { // background series rank lowest return 1 } + +export function makeAxisLabelWrap({ + text, + maxWidth, + baseFontSize = BASE_FONT_SIZE, + detailsOrderedByReference, +}: { + text: string + maxWidth: number + baseFontSize?: number + detailsOrderedByReference?: string[] +}): MarkdownTextWrap { + return new MarkdownTextWrap({ + text, + maxWidth, + fontSize: GRAPHER_FONT_SCALE_12 * baseFontSize, + lineHeight: 1, + detailsOrderedByReference, + }) +} diff --git a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts index 1601f36f65e..fb51eecd461 100644 --- a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts +++ b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts @@ -34,6 +34,7 @@ export const GRAPHER_LIGHT_TEXT = "#858585" export const GRAPHER_OPACITY_MUTE = 0.3 +export const GRAPHER_AXIS_LABEL_PADDING = 10 export const GRAPHER_AXIS_LINE_WIDTH_DEFAULT = 1 export const GRAPHER_AXIS_LINE_WIDTH_THICK = 2 diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index dd54175a663..517e1ec8b4a 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -29,7 +29,7 @@ import { computed, action, observable } from "mobx" import { observer } from "mobx-react" import { select } from "d3-selection" import { easeLinear } from "d3-ease" -import { DualAxisComponent } from "../axis/AxisViews" +import { DualAxisComponent, HorizonalAxisLabel } from "../axis/AxisViews" import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis" import { LineLegend, LineLabelSeries } from "../lineLegend/LineLegend" import { ComparisonLine } from "../scatterCharts/ComparisonLine" @@ -60,6 +60,7 @@ import { GRAPHER_AXIS_LINE_WIDTH_DEFAULT, BASE_FONT_SIZE, GRAPHER_OPACITY_MUTE, + GRAPHER_AXIS_LABEL_PADDING, } from "../core/GrapherConstants" import { ColorSchemes } from "../color/ColorSchemes" import { AxisConfig, AxisManager } from "../axis/AxisConfig" @@ -86,6 +87,7 @@ import { getHoverStateForSeries, getSeriesKey, isTargetOutsideElement, + makeAxisLabelWrap, makeClipPath, makeSelectionArray, } from "../chart/ChartUtils" @@ -115,6 +117,7 @@ import { getSeriesName, } from "./LineChartHelpers" import { FocusArray } from "../focus/FocusArray.js" +import { MarkdownTextWrap } from "@ourworldindata/components" const LINE_CHART_CLASS_NAME = "LineChart" @@ -497,9 +500,11 @@ export class LineChart } @computed private get boundsWithoutColorLegend(): Bounds { - return this.bounds.padTop( - this.hasColorLegend ? this.legendHeight + LEGEND_PADDING : 0 - ) + const colorLegendHeight = this.hasColorLegend + ? this.legendHeight + LEGEND_PADDING + : 0 + + return this.bounds.padTop(colorLegendHeight) } @computed get maxLineLegendWidth(): number { @@ -833,10 +838,6 @@ export class LineChart return guid() } - @computed get detailsOrderedByReference(): string[] { - return this.manager.detailsOrderedByReference ?? [] - } - @computed get fontSize(): number { return this.manager.fontSize ?? BASE_FONT_SIZE } @@ -918,14 +919,21 @@ export class LineChart : undefined return ( - + <> + + {this.horizontalAxisLabelWrap && ( + + )} + ) } @@ -1409,6 +1417,17 @@ export class LineChart return axis } + @computed get horizontalAxisLabelWrap(): MarkdownTextWrap | undefined { + if (!this.xAxisConfig.label) return + const maxWidth = this.innerBounds.width - this.verticalAxisPart.size + return makeAxisLabelWrap({ + text: this.xAxisConfig.label, + maxWidth, + baseFontSize: this.fontSize, + detailsOrderedByReference: this.manager.detailsOrderedByReference, + }) + } + @computed private get yAxisConfig(): AxisConfig { // TODO: enable nice axis ticks for linear scales return new AxisConfig( @@ -1437,14 +1456,13 @@ export class LineChart axis.hideFractionalTicks = this.yColumns.every( (yColumn) => yColumn.isAllIntegers ) // all y axis points are integral, don't show fractional ticks in that case - axis.label = "" axis.formatColumn = this.formatColumn return axis } - @computed get dualAxis(): DualAxis { - return new DualAxis({ - bounds: this.boundsWithoutColorLegend + @computed private get innerBounds(): Bounds { + return ( + this.boundsWithoutColorLegend .padRight( this.manager.showLegend ? this.lineLegendWidth @@ -1453,7 +1471,20 @@ export class LineChart // top padding leaves room for tick labels .padTop(6) // bottom padding avoids axis labels to be cut off at some resolutions - .padBottom(2), + .padBottom(2) + ) + } + + @computed private get axisBounds(): Bounds { + const xAxisLabelHeight = this.horizontalAxisLabelWrap + ? this.horizontalAxisLabelWrap.height + GRAPHER_AXIS_LABEL_PADDING + : 0 + return this.innerBounds.padBottom(xAxisLabelHeight) + } + + @computed get dualAxis(): DualAxis { + return new DualAxis({ + bounds: this.axisBounds, verticalAxis: this.verticalAxisPart, horizontalAxis: this.horizontalAxisPart, }) diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index 9f65ef78db9..3461a5ee404 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx @@ -47,8 +47,10 @@ import { observer } from "mobx-react" import { NoDataModal } from "../noDataModal/NoDataModal" import { BASE_FONT_SIZE, + GRAPHER_AXIS_LABEL_PADDING, GRAPHER_AXIS_LINE_WIDTH_DEFAULT, GRAPHER_AXIS_LINE_WIDTH_THICK, + GRAPHER_FONT_SCALE_12, } from "../core/GrapherConstants" import { OwidTable, @@ -65,7 +67,11 @@ import { VerticalColorLegend, VerticalColorLegendManager, } from "../verticalColorLegend/VerticalColorLegend" -import { DualAxisComponent } from "../axis/AxisViews" +import { + DualAxisComponent, + HorizonalAxisLabel, + VerticalAxisLabel, +} from "../axis/AxisViews" import { DualAxis, HorizontalAxis, VerticalAxis } from "../axis/Axis" import { @@ -90,7 +96,11 @@ import { SCATTER_QUADTREE_SAMPLING_DISTANCE, } from "./ScatterPlotChartConstants" import { ScatterPointsWithLabels } from "./ScatterPointsWithLabels" -import { autoDetectYColumnSlugs, makeSelectionArray } from "../chart/ChartUtils" +import { + autoDetectYColumnSlugs, + makeAxisLabelWrap, + makeSelectionArray, +} from "../chart/ChartUtils" import { OWID_NO_DATA_GRAY } from "../color/ColorConstants" import { ColorScaleConfig, @@ -111,6 +121,7 @@ import { makeTooltipRoundingNotice, } from "../tooltip/Tooltip" import { NoDataSection } from "./NoDataSection" +import { MarkdownTextWrap } from "@ourworldindata/components" function computeSizeDomain(table: OwidTable, slug: ColumnSlug): ValueRange { const sizeValues = table.get(slug).values.filter(isNumber) @@ -334,10 +345,11 @@ export class ScatterPlotChart return this.props.bounds ?? DEFAULT_BOUNDS } + private sidebarPadding = 20 @computed private get innerBounds(): Bounds { return ( this.bounds - .padRight(this.sidebarWidth + 20) + .padRight(this.sidebarWidth + this.sidebarPadding) // top padding leaves room for tick labels .padTop(6) // bottom padding makes sure the x-axis label doesn't overflow @@ -345,6 +357,12 @@ export class ScatterPlotChart ) } + @computed private get axisBounds(): Bounds { + return this.innerBounds + .padLeft(this.verticalAxisLabelHeight) + .padBottom(this.horizontalAxisLabelHeight) + } + @computed private get canAddCountry(): boolean { const { addCountryMode } = this.manager return (addCountryMode && @@ -379,10 +397,6 @@ export class ScatterPlotChart ) } - @computed get detailsOrderedByReference(): string[] { - return this.manager.detailsOrderedByReference ?? [] - } - @computed get fontSize(): number { return this.manager.fontSize ?? BASE_FONT_SIZE } @@ -538,7 +552,7 @@ export class ScatterPlotChart @computed get dualAxis(): DualAxis { const { horizontalAxisPart, verticalAxisPart } = this return new DualAxis({ - bounds: this.innerBounds, + bounds: this.axisBounds, horizontalAxis: horizontalAxisPart, verticalAxis: verticalAxisPart, }) @@ -809,14 +823,28 @@ export class ScatterPlotChart + {this.horizontalAxisLabelWrap && ( + + )} + {this.verticalAxisLabelWrap && ( + + )} {comparisonLines && comparisonLines.map((line, i) => ( )} diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx index 5b40a3086bb..1da7b1598e4 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/AbstractStackedChart.tsx @@ -8,7 +8,10 @@ import { MissingDataStrategy, SeriesStrategy, } from "@ourworldindata/types" -import { BASE_FONT_SIZE } from "../core/GrapherConstants" +import { + BASE_FONT_SIZE, + GRAPHER_AXIS_LABEL_PADDING, +} from "../core/GrapherConstants" import { Bounds, DEFAULT_BOUNDS, @@ -31,6 +34,7 @@ import { import { autoDetectSeriesStrategy, autoDetectYColumnSlugs, + makeAxisLabelWrap, makeSelectionArray, } from "../chart/ChartUtils" import { easeLinear } from "d3-ease" @@ -44,6 +48,7 @@ import { CategoricalColorMap, } from "../color/CategoricalColorAssigner.js" import { BinaryMapPaletteE } from "../color/CustomSchemes" +import { MarkdownTextWrap } from "@ourworldindata/components" // used in StackedBar charts to color negative and positive bars const POSITIVE_COLOR = BinaryMapPaletteE.colorSets[0][0] // orange @@ -151,10 +156,6 @@ export class AbstractStackedChart return this.manager.fontSize ?? BASE_FONT_SIZE } - @computed get detailsOrderedByReference(): string[] { - return this.manager.detailsOrderedByReference ?? [] - } - protected get paddingForLegendRight(): number { return 0 } @@ -220,10 +221,18 @@ export class AbstractStackedChart ) } + @computed private get axisBounds(): Bounds { + const xAxisLabelHeight = this.horizontalAxisLabelWrap + ? this.horizontalAxisLabelWrap.height + GRAPHER_AXIS_LABEL_PADDING + : 0 + + return this.innerBounds.padBottom(xAxisLabelHeight) + } + @computed protected get dualAxis(): DualAxis { const { horizontalAxisPart, verticalAxisPart } = this return new DualAxis({ - bounds: this.innerBounds, + bounds: this.axisBounds, horizontalAxis: horizontalAxisPart, verticalAxis: verticalAxisPart, }) @@ -252,6 +261,20 @@ export class AbstractStackedChart return axis } + @computed protected get horizontalAxisLabelWrap(): + | MarkdownTextWrap + | undefined { + if (!this.xAxisConfig.label) return undefined + + const maxWidth = this.innerBounds.width - this.verticalAxisPart.size + + return makeAxisLabelWrap({ + text: this.xAxisConfig.label, + maxWidth, + baseFontSize: this.fontSize, + }) + } + @computed private get yAxisConfig(): AxisConfig { // TODO: enable nice axis ticks for linear scales return new AxisConfig(this.manager.yAxisConfig, this) diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx index c95f9cc3c72..7504a6168d5 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx @@ -27,12 +27,17 @@ import { action, computed, observable } from "mobx" import { observer } from "mobx-react" import { BASE_FONT_SIZE, + GRAPHER_AXIS_LABEL_PADDING, GRAPHER_AXIS_LINE_WIDTH_DEFAULT, GRAPHER_AXIS_LINE_WIDTH_THICK, GRAPHER_FONT_SCALE_12, Patterns, } from "../core/GrapherConstants" -import { DualAxisComponent } from "../axis/AxisViews" +import { + DualAxisComponent, + HorizonalAxisLabel, + VerticalAxisLabel, +} from "../axis/AxisViews" import { NoDataModal } from "../noDataModal/NoDataModal" import { AxisConfig } from "../axis/AxisConfig" import { ChartInterface } from "../chart/ChartInterface" @@ -46,6 +51,7 @@ import { OwidTable, CoreColumn } from "@ourworldindata/core-table" import { autoDetectYColumnSlugs, getShortNameForEntity, + makeAxisLabelWrap, makeSelectionArray, } from "../chart/ChartUtils" import { StackedPoint, StackedSeries } from "./StackedConstants" @@ -88,6 +94,7 @@ import { LabelCandidateWithElement, MarimekkoBarProps, } from "./MarimekkoChartConstants" +import { MarkdownTextWrap } from "@ourworldindata/components" const MARKER_MARGIN: number = 4 const MARKER_AREA_HEIGHT: number = 25 @@ -548,11 +555,23 @@ export class MarimekkoChart const marginToEnsureWidestEntityLabelFitsEvenIfAtX0 = Math.max(whiteSpaceOnLeft, this.longestLabelWidth) - whiteSpaceOnLeft + const yLabelHeight = this.verticalAxisLabelWrap + ? this.verticalAxisLabelWrap.height + GRAPHER_AXIS_LABEL_PADDING + : 0 + const xLabelHeight = this.horizontalAxisLabelWrap + ? this.horizontalAxisLabelWrap.height + GRAPHER_AXIS_LABEL_PADDING + : 0 return this.bounds .padBottom(this.longestLabelHeight + 2) .padBottom(labelLinesHeight) .padTop(this.legend.height + this.legendPaddingTop) - .padLeft(marginToEnsureWidestEntityLabelFitsEvenIfAtX0) + .padTop(xLabelHeight) + .padLeft( + Math.max( + marginToEnsureWidestEntityLabelFitsEvenIfAtX0, + yLabelHeight + ) + ) } @computed get isStatic(): boolean { @@ -631,10 +650,41 @@ export class MarimekkoChart axis.updateDomainPreservingUserSettings(this.yDomainDefault) axis.formatColumn = this.yColumns[0] - axis.label = this.currentVerticalAxisLabel return axis } + + @computed private get verticalAxisLabelWrap(): + | MarkdownTextWrap + | undefined { + if (!this.currentVerticalAxisLabel) return + + const maxWidth = + this.bounds.height - + this.legend.height - + MARKER_AREA_HEIGHT - + this.longestLabelHeight + + return makeAxisLabelWrap({ + text: this.currentVerticalAxisLabel, + maxWidth, + baseFontSize: this.fontSize, + detailsOrderedByReference: this.manager.detailsOrderedByReference, + }) + } + + @computed private get horizontalAxisLabelWrap(): + | MarkdownTextWrap + | undefined { + if (!this.currentHorizontalAxisLabel) return + return makeAxisLabelWrap({ + text: this.currentHorizontalAxisLabel, + maxWidth: this.bounds.width, + baseFontSize: this.fontSize, + detailsOrderedByReference: this.manager.detailsOrderedByReference, + }) + } + @computed private get isNarrow(): boolean { // TODO: this should probably come from grapher? return this.bounds.width < 650 // innerBounds would lead to dependency cycle @@ -677,7 +727,6 @@ export class MarimekkoChart axis.formatColumn = xColumn - axis.label = this.currentHorizontalAxisLabel return axis } @@ -865,10 +914,6 @@ export class MarimekkoChart return this.baseFontSize } - @computed get detailsOrderedByReference(): string[] { - return this.manager.detailsOrderedByReference ?? [] - } - @computed get categoricalLegendData(): CategoricalBin[] { const { colorColumnSlug, colorScale, series } = this if (colorColumnSlug) { @@ -1045,14 +1090,28 @@ export class MarimekkoChart + {this.horizontalAxisLabelWrap && ( + + )} + {this.verticalAxisLabelWrap && ( + + )} {this.renderBars()} {target && ( diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index a45e91b3c5a..e8b7f44e430 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -27,7 +27,7 @@ import { GRAPHER_AXIS_LINE_WIDTH_THICK, } from "../core/GrapherConstants" import { observer } from "mobx-react" -import { DualAxisComponent } from "../axis/AxisViews" +import { DualAxisComponent, HorizonalAxisLabel } from "../axis/AxisViews" import { DualAxis } from "../axis/Axis" import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend" import { NoDataModal } from "../noDataModal/NoDataModal" @@ -663,17 +663,24 @@ export class StackedAreaChart extends AbstractStackedChart { renderAxis(): React.ReactElement { const { manager } = this return ( - + <> + + {this.horizontalAxisLabelWrap && ( + + )} + ) } diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx index 6dcbab4f96c..4e734263c5a 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx @@ -14,7 +14,7 @@ import { partition, makeIdForHumanConsumption, } from "@ourworldindata/utils" -import { DualAxisComponent } from "../axis/AxisViews" +import { DualAxisComponent, HorizonalAxisLabel } from "../axis/AxisViews" import { NoDataModal } from "../noDataModal/NoDataModal" import { VerticalColorLegend, @@ -488,13 +488,20 @@ export class StackedBarChart : GRAPHER_AXIS_LINE_WIDTH_DEFAULT return ( - + <> + + {this.horizontalAxisLabelWrap && ( + + )} + ) } diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx index b298d2ae160..01be6b66344 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedDiscreteBarChart.tsx @@ -324,7 +324,6 @@ export class StackedDiscreteBarChart axis.formatColumn = this.yColumns[0] // todo: does this work for columns as series? axis.range = this.xRange - axis.label = "" return axis } @@ -700,7 +699,6 @@ export class StackedDiscreteBarChart bounds={bounds} axis={yAxis} preferredAxisPosition={innerBounds.bottom} - labelColor={manager.secondaryColorInStaticCharts} tickMarkWidth={axisLineWidth} />