Skip to content

Commit

Permalink
✨ (scatter) horizontal y-axis label
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Dec 20, 2024
1 parent 6205b1b commit 679df95
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 58 deletions.
84 changes: 55 additions & 29 deletions packages/@ourworldindata/grapher/src/axis/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ abstract class AbstractAxis {
*/
abstract get size(): number
abstract get orient(): Position
abstract get labelWidth(): number
abstract get labelMaxWidth(): number
abstract get labelPadding(): number

abstract placeTickLabel(value: number): TickLabelPlacement
abstract get tickLabels(): TickLabelPlacement[]
Expand All @@ -101,8 +102,12 @@ 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 labelPosition(): AxisAlign {
return this.config.labelPosition ?? AxisAlign.middle
}

@computed get nice(): boolean {
Expand Down Expand Up @@ -488,7 +493,7 @@ abstract class AbstractAxis {
const text = this.label
return text
? new MarkdownTextWrap({
maxWidth: this.labelWidth,
maxWidth: this.labelMaxWidth,
fontSize: this.labelFontSize,
text,
lineHeight: 1,
Expand All @@ -497,6 +502,12 @@ abstract class AbstractAxis {
})
: undefined
}

@computed get labelHeight(): number {
return this.labelTextWrap
? this.labelTextWrap.height + this.labelPadding
: 0
}
}

export class HorizontalAxis extends AbstractAxis {
Expand All @@ -511,13 +522,15 @@ export class HorizontalAxis extends AbstractAxis {
: Position.bottom
}

@computed get labelPadding(): number {
return this.config.labelPadding ?? 10
}

@computed get labelOffset(): number {
return this.labelTextWrap
? this.labelTextWrap.height + this.labelPadding * 2
: 0
return this.labelHeight
}

@computed get labelWidth(): number {
@computed get labelMaxWidth(): number {
return this.rangeSize
}

Expand All @@ -527,12 +540,10 @@ export class HorizontalAxis extends AbstractAxis {
// we might end up with misaligned axes.
@computed get height(): number {
if (this.hideAxis) return 0
const { labelOffset, labelPadding } = this
const { labelOffset, tickPadding } = this
const maxTickHeight = max(this.tickLabels.map((tick) => tick.height))
const height = maxTickHeight
? maxTickHeight + labelOffset + labelPadding
: 0
return Math.max(height, this.config.minSize ?? 0)
const tickHeight = maxTickHeight ? maxTickHeight + tickPadding : 0
return Math.max(tickHeight + labelOffset, this.config.minSize ?? 0)
}

@computed get size(): number {
Expand Down Expand Up @@ -630,14 +641,24 @@ export class VerticalAxis extends AbstractAxis {
return Position.left
}

@computed get labelWidth(): number {
return this.height
@computed get labelPadding(): number {
return this.config.labelPadding ?? 16
}

@computed get labelOffset(): number {
return this.labelTextWrap
? this.labelTextWrap.height + this.labelPadding * 2
: 0
@computed get labelMaxWidth(): number {
// if rotated and positioned to the left of the axis,
// the label width is limited by the height of the axis
if (this.labelPosition === AxisAlign.middle) return this.height

return this.axisManager?.axisBounds?.width ?? Infinity
}

@computed get labelOffsetLeft(): number {
return this.labelPosition === AxisAlign.middle ? this.labelHeight : 0
}

@computed get labelOffsetTop(): number {
return this.labelPosition === AxisAlign.middle ? 0 : this.labelHeight
}

// note that we intentionally don't take `hideAxisLabels` into account here.
Expand All @@ -646,13 +667,11 @@ export class VerticalAxis extends AbstractAxis {
// we might end up with misaligned axes.
@computed get width(): number {
if (this.hideAxis) return 0
const { labelOffset, labelPadding } = this
const { tickPadding, labelOffsetLeft } = this
const maxTickWidth = max(this.tickLabels.map((tick) => tick.width))
const width =
maxTickWidth !== undefined
? maxTickWidth + labelOffset + labelPadding
: 0
return Math.max(width, this.config.minSize ?? 0)
const tickWidth =
maxTickWidth !== undefined ? maxTickWidth + tickPadding : 0
return Math.max(tickWidth + labelOffsetLeft, this.config.minSize ?? 0)
}

@computed get height(): number {
Expand Down Expand Up @@ -768,10 +787,17 @@ export class DualAxis {

// Now we can determine the "true" inner bounds of the dual axis
@computed get innerBounds(): Bounds {
return this.bounds.pad({
[this.props.horizontalAxis.orient]: this.horizontalAxisSize,
[this.props.verticalAxis.orient]: this.verticalAxisSize,
})
return (
this.bounds
// add padding to account for the width of the vertical axis
// and the height of the horizontal axis
.pad({
[this.props.horizontalAxis.orient]: this.horizontalAxisSize,
[this.props.verticalAxis.orient]: this.verticalAxisSize,
})
// make space for the y-axis label if plotted above the axis
.padTop(this.props.verticalAxis.labelOffsetTop)
)
}

@computed get bounds(): Bounds {
Expand Down
4 changes: 4 additions & 0 deletions packages/@ourworldindata/grapher/src/axis/AxisConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AxisAlign,
Position,
TickFormattingOptions,
Bounds,
} from "@ourworldindata/utils"
import { observable, computed } from "mobx"
import { HorizontalAxis, VerticalAxis } from "./Axis"
Expand All @@ -20,6 +21,7 @@ import {

export interface AxisManager {
fontSize: number
axisBounds?: Bounds
detailsOrderedByReference?: string[]
}

Expand All @@ -33,7 +35,9 @@ class AxisConfigDefaults implements AxisConfigInterface {
@observable.ref hideAxis?: boolean = undefined
@observable.ref hideGridlines?: boolean = undefined
@observable.ref hideTickLabels?: boolean = undefined
@observable.ref labelPosition?: AxisAlign = AxisAlign.middle
@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
Expand Down
41 changes: 22 additions & 19 deletions packages/@ourworldindata/grapher/src/axis/AxisViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { VerticalAxis, HorizontalAxis, DualAxis } from "./Axis"
import classNames from "classnames"
import { GRAPHER_DARK_TEXT } from "../color/ColorConstants"
import { ScaleType, DetailsMarker } from "@ourworldindata/types"
import { ScaleType, DetailsMarker, AxisAlign } from "@ourworldindata/types"

const dasharrayFromFontSize = (fontSize: number): string => {
const dashLength = Math.round((fontSize / 16) * 3)
Expand Down Expand Up @@ -265,27 +265,27 @@ export class VerticalAxisComponent extends React.Component<{
} = this.props
const { tickLabels, labelTextWrap, config } = verticalAxis

const isLabelCentered = verticalAxis.labelPosition === AxisAlign.middle
const labelX = isLabelCentered ? -verticalAxis.rangeCenter : bounds.left
const labelY = isLabelCentered ? bounds.left : bounds.top

return (
<g
id={makeIdForHumanConsumption("vertical-axis")}
className="VerticalAxis"
>
{labelTextWrap &&
labelTextWrap.renderSVG(
-verticalAxis.rangeCenter,
bounds.left,
{
id: makeIdForHumanConsumption(
"vertical-axis-label"
),
textProps: {
transform: "rotate(-90)",
fill: labelColor || GRAPHER_DARK_TEXT,
textAnchor: "middle",
},
detailsMarker,
}
)}
labelTextWrap.renderSVG(labelX, labelY, {
id: makeIdForHumanConsumption("vertical-axis-label"),
textProps: {
transform: isLabelCentered
? "rotate(-90)"
: undefined,
fill: labelColor || GRAPHER_DARK_TEXT,
textAnchor: isLabelCentered ? "middle" : "start",
},
detailsMarker,
})}
{showTickMarks && (
<g id={makeIdForHumanConsumption("tick-marks")}>
{tickLabels.map((label, i) => (
Expand Down Expand Up @@ -315,7 +315,7 @@ export class VerticalAxisComponent extends React.Component<{
x={(
bounds.left +
verticalAxis.width -
verticalAxis.labelPadding
verticalAxis.tickPadding
).toFixed(2)}
y={y}
dy={dyFromAlign(
Expand Down Expand Up @@ -392,17 +392,20 @@ export class HorizontalAxisComponent extends React.Component<{

const showTickLabels = !axis.config.hideTickLabels

const isLabelCentered = axis.labelPosition === AxisAlign.middle
const labelX = isLabelCentered ? axis.rangeCenter : bounds.right

return (
<g
id={makeIdForHumanConsumption("horizontal-axis")}
className="HorizontalAxis"
>
{label &&
label.renderSVG(axis.rangeCenter, labelYPosition, {
label.renderSVG(labelX, labelYPosition, {
id: makeIdForHumanConsumption("horizontal-axis-label"),
textProps: {
fill: labelColor || GRAPHER_DARK_TEXT,
textAnchor: "middle",
textAnchor: isLabelCentered ? "middle" : "end",
},
detailsMarker,
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ColorSchemeName,
ValueRange,
ColumnSlug,
AxisAlign,
} from "@ourworldindata/types"
import { ComparisonLine } from "../scatterCharts/ComparisonLine"
import { observable, computed, action } from "mobx"
Expand Down Expand Up @@ -345,6 +346,10 @@ export class ScatterPlotChart
)
}

@computed get axisBounds(): Bounds {
return this.innerBounds
}

@computed private get canAddCountry(): boolean {
const { addCountryMode } = this.manager
return (addCountryMode &&
Expand Down Expand Up @@ -538,7 +543,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,
})
Expand Down Expand Up @@ -774,7 +779,7 @@ export class ScatterPlotChart

const legendPadding = 16
const ySizeLegend =
bounds.top +
this.legendY +
verticalLegendHeight +
(verticalLegendHeight > 0 ? legendPadding : 0)
const yArrowLegend =
Expand Down Expand Up @@ -989,7 +994,7 @@ export class ScatterPlotChart
}

@computed get legendY(): number {
return this.bounds.top
return this.bounds.top + this.yAxis.labelHeight
}

@computed get legendX(): number {
Expand Down Expand Up @@ -1029,15 +1034,23 @@ export class ScatterPlotChart

@computed private get yAxisConfig(): AxisConfig {
const { yAxisConfig = {} } = this.manager
const labelPadding = this.manager.isNarrow ? 2 : undefined
const config = { ...yAxisConfig, labelPadding }
const labelPadding = this.manager.isNarrow ? 10 : undefined
const config = {
...yAxisConfig,
labelPosition: AxisAlign.end,
labelPadding,
}
return new AxisConfig(config, this)
}

@computed private get xAxisConfig(): AxisConfig {
const { xAxisConfig = {} } = this.manager
const labelPadding = this.manager.isNarrow ? 2 : undefined
const config = { ...xAxisConfig, labelPadding }
const labelPadding = this.manager.isNarrow ? 8 : undefined
const config = {
...xAxisConfig,
labelPosition: AxisAlign.end,
labelPadding,
}
return new AxisConfig(config, this)
}

Expand Down
16 changes: 13 additions & 3 deletions packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,22 @@ export interface AxisConfigInterface {
minSize?: number

/**
* The padding between:
* - an axis tick and an axis gridline
* - an axis label and an axis tick
* Position of the axis label.
* For vertical axes, 'middle' rotates the label and places it to the left of the axis,
* 'end' places the label above the axis.
*/
labelPosition?: AxisAlign

/**
* The padding between an axis label and an axis tick
*/
labelPadding?: number

/**
* The padding between an axis tick and an axis gridline
*/
tickPadding?: number

/**
* Extend scale to start & end on "nicer" round values.
* See: https://github.com/d3/d3-scale#continuous_nice
Expand Down

0 comments on commit 679df95

Please sign in to comment.