Skip to content

Commit

Permalink
✨ (scatter) show horizontal x-axis label
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Dec 18, 2024
1 parent cf54ea1 commit cf5e224
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 48 deletions.
72 changes: 51 additions & 21 deletions packages/@ourworldindata/grapher/src/axis/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface TickLabelPlacement {
type Scale = ScaleLinear<number, number> | ScaleLogarithmic<number, number>

const OUTER_PADDING = 4
const MAX_LABEL_WIDTH = 320

const doIntersect = (bounds: Bounds, bounds2: Bounds): boolean => {
return bounds.intersects(bounds2)
Expand Down Expand Up @@ -89,6 +90,8 @@ abstract class AbstractAxis {
abstract get size(): number
abstract get orient(): Position
abstract get labelWidth(): number
abstract get labelPadding(): number
abstract get tickPadding(): number

abstract placeTickLabel(value: number): TickLabelPlacement
abstract get tickLabels(): TickLabelPlacement[]
Expand All @@ -101,8 +104,8 @@ abstract class AbstractAxis {
return this.config.hideGridlines ?? false
}

@computed get labelPadding(): number {
return this.config.labelPadding ?? 5
@computed get labelPosition(): AxisAlign {
return this.config.labelPosition ?? AxisAlign.middle
}

@computed get nice(): boolean {
Expand Down Expand Up @@ -511,14 +514,22 @@ export class HorizontalAxis extends AbstractAxis {
: Position.bottom
}

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

@computed get tickPadding(): number {
return this.config.tickPadding ?? 5
}

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

@computed get labelWidth(): number {
return this.rangeSize
return Math.min(MAX_LABEL_WIDTH, this.rangeSize)
}

// note that we intentionally don't take `hideAxisLabels` into account here.
Expand All @@ -527,12 +538,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,13 +639,23 @@ export class VerticalAxis extends AbstractAxis {
return Position.left
}

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

@computed get tickPadding(): number {
return this.config.tickPadding ?? 5
}

@computed get labelWidth(): number {
return this.height
if (this.labelPosition === AxisAlign.middle) return this.height
const availableWidth = this.axisManager?.axisBounds?.width ?? Infinity
return Math.min(availableWidth, MAX_LABEL_WIDTH)
}

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

Expand All @@ -646,13 +665,13 @@ 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, labelPosition } = 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
const labelOffset =
labelPosition === AxisAlign.middle ? this.labelOffset : 0
return Math.max(tickWidth + labelOffset, this.config.minSize ?? 0)
}

@computed get height(): number {
Expand Down Expand Up @@ -766,12 +785,23 @@ export class DualAxis {
return axis.size
}

@computed private get verticalAxisLabelOffset(): number {
return this.props.verticalAxis.labelPosition === AxisAlign.middle
? 0
: this.props.verticalAxis.labelOffset
}

// 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
.pad({
[this.props.horizontalAxis.orient]: this.horizontalAxisSize,
[this.props.verticalAxis.orient]: this.verticalAxisSize,
})
// make space for the x-axis label if plotted above the axis
.padTop(this.verticalAxisLabelOffset)
)
}

@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.middle | AxisAlign.end = 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
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 @@ -365,6 +366,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 @@ -558,7 +563,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 @@ -1049,15 +1054,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 ? 3 : 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 ? 3 : 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. 'start' is not supported.
* For vertical axes, 'middle' rotates the label and places it next to 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 cf5e224

Please sign in to comment.