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

✨ (scatter) show horizontal y-axis label #4331

Draft
wants to merge 1 commit 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
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
Loading