Skip to content

Commit

Permalink
🎉 (stacked bar) share x-axis across facets (#3733)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann authored Jul 2, 2024
1 parent 0800199 commit ac1dca2
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 109 deletions.
8 changes: 8 additions & 0 deletions packages/@ourworldindata/grapher/src/axis/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ export class HorizontalAxis extends AbstractAxis {
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
Expand Down Expand Up @@ -635,6 +639,10 @@ export class VerticalAxis extends AbstractAxis {
: 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
Expand Down
2 changes: 2 additions & 0 deletions packages/@ourworldindata/grapher/src/axis/AxisConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class AxisConfigDefaults implements AxisConfigInterface {
@observable.ref minSize?: number = undefined
@observable.ref hideAxis?: boolean = undefined
@observable.ref hideGridlines?: boolean = undefined
@observable.ref hideTickLabels?: boolean = undefined
@observable.ref labelPadding?: number = undefined
@observable.ref nice?: boolean = undefined
@observable.ref maxTicks?: number = undefined
Expand Down Expand Up @@ -70,6 +71,7 @@ export class AxisConfig
minSize: this.minSize,
hideAxis: this.hideAxis,
hideGridlines: this.hideGridlines,
hideTickLabels: this.hideTickLabels,
labelPadding: this.labelPadding,
nice: this.nice,
maxTicks: this.maxTicks,
Expand Down
134 changes: 73 additions & 61 deletions packages/@ourworldindata/grapher/src/axis/AxisViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export class VerticalAxisComponent extends React.Component<{
detailsMarker,
showTickMarks,
} = this.props
const { tickLabels, labelTextWrap } = verticalAxis
const { tickLabels, labelTextWrap, config } = verticalAxis

return (
<g
Expand Down Expand Up @@ -299,34 +299,38 @@ export class VerticalAxisComponent extends React.Component<{
))}
</g>
)}
<g id={makeIdForHumanConsumption("tick-labels")}>
{tickLabels.map((label, i) => {
const { y, xAlign, yAlign, formattedValue } = label
return (
<text
key={i}
id={makeIdForHumanConsumption(
"tick-label",
formattedValue
)}
x={(
bounds.left +
verticalAxis.width -
verticalAxis.labelPadding
).toFixed(2)}
y={y}
dy={dyFromAlign(yAlign ?? VerticalAlign.middle)}
textAnchor={textAnchorFromAlign(
xAlign ?? HorizontalAlign.right
)}
fill={tickColor || GRAPHER_DARK_TEXT}
fontSize={verticalAxis.tickFontSize}
>
{formattedValue}
</text>
)
})}
</g>
{!config.hideTickLabels && (
<g id={makeIdForHumanConsumption("tick-labels")}>
{tickLabels.map((label, i) => {
const { y, xAlign, yAlign, formattedValue } = label
return (
<text
key={i}
id={makeIdForHumanConsumption(
"tick-label",
formattedValue
)}
x={(
bounds.left +
verticalAxis.width -
verticalAxis.labelPadding
).toFixed(2)}
y={y}
dy={dyFromAlign(
yAlign ?? VerticalAlign.middle
)}
textAnchor={textAnchorFromAlign(
xAlign ?? HorizontalAlign.right
)}
fill={tickColor || GRAPHER_DARK_TEXT}
fontSize={verticalAxis.tickFontSize}
>
{formattedValue}
</text>
)
})}
</g>
)}
</g>
)
}
Expand Down Expand Up @@ -384,6 +388,8 @@ export class HorizontalAxisComponent extends React.Component<{
? bounds.top + labelOffset + 10
: bounds.bottom - labelOffset

const showTickLabels = !axis.config.hideTickLabels

return (
<g
id={makeIdForHumanConsumption("horizontal-axis")}
Expand All @@ -398,40 +404,46 @@ export class HorizontalAxisComponent extends React.Component<{
},
detailsMarker,
})}
{tickLabels.map((label) => {
const { x, xAlign, formattedValue } = label
return (
<g
id={makeIdForHumanConsumption(
"tick",
formattedValue
)}
key={formattedValue}
>
{showTickMarks && (
<line
x1={axis.place(label.value)}
y1={tickMarksYPosition - tickMarkWidth / 2}
x2={axis.place(label.value)}
y2={tickMarksYPosition + tickSize}
stroke={SOLID_TICK_COLOR}
strokeWidth={tickMarkWidth}
/>
)}
<text
x={x}
y={tickLabelYPlacement}
fill={tickColor || GRAPHER_DARK_TEXT}
textAnchor={textAnchorFromAlign(
xAlign ?? HorizontalAlign.center
{(showTickMarks || showTickLabels) &&
tickLabels.map((label) => {
const { x, xAlign, formattedValue } = label
return (
<g
id={makeIdForHumanConsumption(
"tick",
formattedValue
)}
fontSize={axis.tickFontSize}
key={formattedValue}
>
{formattedValue}
</text>
</g>
)
})}
{showTickMarks && (
<line
x1={axis.place(label.value)}
y1={
tickMarksYPosition -
tickMarkWidth / 2
}
x2={axis.place(label.value)}
y2={tickMarksYPosition + tickSize}
stroke={SOLID_TICK_COLOR}
strokeWidth={tickMarkWidth}
/>
)}
{showTickLabels && (
<text
x={x}
y={tickLabelYPlacement}
fill={tickColor || GRAPHER_DARK_TEXT}
textAnchor={textAnchorFromAlign(
xAlign ?? HorizontalAlign.center
)}
fontSize={axis.tickFontSize}
>
{formattedValue}
</text>
)}
</g>
)
})}
</g>
)
}
Expand Down
77 changes: 50 additions & 27 deletions packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
NumericBin,
} from "../color/ColorScaleBin"

const SHARED_X_AXIS_MIN_FACET_COUNT = 12

const facetBackgroundColor = "none" // we don't use color yet but may use it for background later

const getContentBounds = (
Expand Down Expand Up @@ -211,7 +213,11 @@ export class FacetChart
columnPadding: number
outerPadding: number
} {
return getChartPadding(this.facetFontSize)
const { isSharedXAxis, facetFontSize } = this
return getChartPadding({
baseFontSize: facetFontSize,
isSharedXAxis,
})
}

@computed private get hideFacetLegends(): boolean {
Expand Down Expand Up @@ -342,6 +348,29 @@ export class FacetChart
})
}

@computed private get isSharedYAxis(): boolean {
// When the Y axis is uniform for all facets:
// - for most charts, we want to only show the axis on the left-most facet charts, and omit
// it on the others
// - for bar charts the Y axis is plotted horizontally, so we don't want to omit it
return (
this.uniformYAxis &&
![
ChartTypeName.StackedDiscreteBar,
ChartTypeName.DiscreteBar,
].includes(this.chartTypeName)
)
}

@computed private get isSharedXAxis(): boolean {
return (
this.uniformXAxis &&
// TODO: do this for stacked area charts and line charts as well?
this.chartTypeName === ChartTypeName.StackedBar &&
this.facetCount >= SHARED_X_AXIS_MIN_FACET_COUNT
)
}

// Only made public for testing
@computed get placedSeries(): PlacedFacetSeries[] {
const { intermediateChartInstances } = this
Expand All @@ -355,36 +384,18 @@ export class FacetChart
// Define the global axis config, shared between all facets
const sharedAxesSizes: PositionMap<number> = {}

// When the Y axis is uniform for all facets:
// - for most charts, we want to only show the axis on the left-most facet charts, and omit
// it on the others
// - for bar charts the Y axis is plotted horizontally, so we don't want to omit it
const isSharedYAxis =
this.uniformYAxis &&
![
ChartTypeName.StackedDiscreteBar,
ChartTypeName.DiscreteBar,
].includes(this.chartTypeName)

const axes: AxesInfo = {
x: {
config: {},
axisAccessor: (instance) => instance.xAxis,
uniform: this.uniformXAxis,
// For now, X axes are never shared for any chart.
// If we ever allow them to be shared, we need to be careful with how we determine
// the `minSize` – in the intermediate series (at this time) all axes are shown in
// order to find the one with maximum size, but in the placed series, some axes are
// hidden. This expands the available area for the chart, which can in turn increase
// the number of ticks shown, which can make the size of the axis in the placed
// series greater than the one in the intermediate series.
shared: false,
shared: this.isSharedXAxis,
},
y: {
config: {},
axisAccessor: (instance) => instance.yAxis,
uniform: this.uniformYAxis,
shared: isSharedYAxis,
shared: this.isSharedYAxis,
},
}
values(axes).forEach(({ config, axisAccessor, uniform, shared }) => {
Expand Down Expand Up @@ -425,7 +436,11 @@ export class FacetChart
config.max,
])
config.minSize = size
if (shared) sharedAxesSizes[axis.orient] = size
if (shared) {
const sharedAxisSize =
axis.orient === Position.bottom ? 0 : size
sharedAxesSizes[axis.orient] = sharedAxisSize
}
}
} else if (axisWithMaxSize) {
config.minSize = axisWithMaxSize.size
Expand All @@ -446,10 +461,10 @@ export class FacetChart
return this.intermediatePlacedSeries.map((series, i) => {
const chartInstance = intermediateChartInstances[i]
const { xAxis, yAxis } = chartInstance
const { bounds: initialGridBounds, edges } = gridBoundsArr[i]
const { bounds: initialGridBounds, cellEdges } = gridBoundsArr[i]
let bounds = initialGridBounds
// Only expand bounds if the facet is on the same edge as the shared axes
for (const edge of edges) {
for (const edge of cellEdges) {
bounds = bounds.expand({
[edge]: sharedAxesSizes[edge],
})
Expand All @@ -461,9 +476,17 @@ export class FacetChart
useValueBasedColorScheme,
externalLegendFocusBin: this.legendFocusBin,
xAxisConfig: {
hideAxis: shouldHideFacetAxis(
// For now, sharing an x axis means hiding the tick labels of inner facets.
// This means that none of the x axes are actually hidden (we just don't plot their tick labels).
// If we ever allow shared x axes to be actually hidden, we need to be careful with how we determine
// the `minSize` – in the intermediate series (at this time) all axes are shown in
// order to find the one with maximum size, but in the placed series, some axes are
// hidden. This expands the available area for the chart, which can in turn increase
// the number of ticks shown, which can make the size of the axis in the placed
// series greater than the one in the intermediate series.
hideTickLabels: shouldHideFacetAxis(
xAxis,
edges,
cellEdges,
sharedAxesSizes
),
...series.manager.xAxisConfig,
Expand All @@ -472,7 +495,7 @@ export class FacetChart
yAxisConfig: {
hideAxis: shouldHideFacetAxis(
yAxis,
edges,
cellEdges,
sharedAxesSizes
),
...series.manager.yAxisConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,24 @@ export const getFontSize = (
export const getLabelPadding = (baseFontSize: number): number =>
0.5 * baseFontSize

export const getChartPadding = (
export const getChartPadding = ({
baseFontSize,
isSharedXAxis,
}: {
baseFontSize: number
): { rowPadding: number; columnPadding: number; outerPadding: number } => {
isSharedXAxis: boolean
}): { rowPadding: number; columnPadding: number; outerPadding: number } => {
const labelHeight = baseFontSize
const labelPadding = getLabelPadding(baseFontSize)

const rowPadding = isSharedXAxis ? 0 : 1
const columnPadding = 1

return {
rowPadding: Math.round(labelHeight + labelPadding + baseFontSize),
columnPadding: Math.round(baseFontSize),
rowPadding: Math.round(
labelHeight + labelPadding + rowPadding * baseFontSize
),
columnPadding: Math.round(columnPadding * baseFontSize),
outerPadding: 0,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ export class StackedBarChart
}

@computed protected get paddingForLegendRight(): number {
return this.showHorizontalLegend ? 0 : this.sidebarWidth + 20
return this.showHorizontalLegend
? 0
: this.sidebarWidth > 0
? this.sidebarWidth + 20
: 0
}

@computed protected get paddingForLegendTop(): number {
Expand Down
1 change: 1 addition & 0 deletions packages/@ourworldindata/types/src/domainTypes/Layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export enum AxisAlign {
export interface GridParameters {
rows: number
columns: number
count: number
}
Loading

0 comments on commit ac1dca2

Please sign in to comment.