Skip to content

Commit

Permalink
🧪 (line legend) add tests for dropping labels (#4309)
Browse files Browse the repository at this point in the history
Adds tests in preparation for a refactor of the code responsible for dropping labels when there is not enough space.

I also renamed some of the computed values in the line legend component for clarity.

The SVG tester comes back with a difference because there was a small issue about label padding that I fixed.
  • Loading branch information
sophiamersmann authored Dec 19, 2024
1 parent edd3a4d commit 332107f
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 79 deletions.
4 changes: 2 additions & 2 deletions packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,7 @@ export class LineChart
// only pass props that are required to calculate
// the width to avoid circular dependencies
return LineLegend.stableWidth({
labelSeries: this.lineLegendSeries,
series: this.lineLegendSeries,
maxWidth: this.maxLineLegendWidth,
fontSize: this.fontSize,
fontWeight: this.fontWeight,
Expand Down Expand Up @@ -953,7 +953,7 @@ export class LineChart
))}
{manager.showLegend && (
<LineLegend
labelSeries={this.lineLegendSeries}
series={this.lineLegendSeries}
yAxis={this.yAxis}
x={this.lineLegendX}
yRange={this.lineLegendY}
Expand Down
204 changes: 181 additions & 23 deletions packages/@ourworldindata/grapher/src/lineLegend/LineLegend.test.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,189 @@
#! /usr/bin/env jest

import { PartialBy } from "@ourworldindata/utils"
import { AxisConfig } from "../axis/AxisConfig"
import { LineLegend, LineLegendProps } from "./LineLegend"

const props: LineLegendProps = {
labelSeries: [
{
seriesName: "Canada",
label: "Canada",
color: "red",
yValue: 50,
annotation: "A country in North America",
},
{
seriesName: "Mexico",
label: "Mexico",
color: "green",
yValue: 20,
annotation: "Below Canada",
},
],
x: 200,
yAxis: new AxisConfig({ min: 0, max: 100 }).toVerticalAxis(),
import {
LEGEND_ITEM_MIN_SPACING,
LineLabelSeries,
LineLegend,
} from "./LineLegend"

const makeAxis = ({
min = 0,
max = 100,
yRange,
}: {
min?: number
max?: number
yRange: [number, number]
}) => {
const yAxis = new AxisConfig({ min, max }).toVerticalAxis()
yAxis.range = yRange
return yAxis
}

const makeSeries = (
series: PartialBy<LineLabelSeries, "label" | "color">[]
): LineLabelSeries[] =>
series.map((s) => ({
label: s.seriesName,
color: "blue",
...s,
}))

const series = makeSeries([
{
seriesName: "Canada",
yValue: 50,
annotation: "A country in North America",
},
{ seriesName: "Mexico", yValue: 20, annotation: "Below Canada" },
])

it("can create a new legend", () => {
const legend = new LineLegend(props)
const legend = new LineLegend({
series,
yAxis: makeAxis({ yRange: [0, 100] }),
})

expect(legend.visibleSeriesNames.length).toEqual(2)
})

describe("dropping labels", () => {
it("drops labels that don't fit into the available space", () => {
const lineLegend = new LineLegend({
series,
yAxis: makeAxis({ yRange: [0, 50] }),
})

// two labels are given, but only one fits
expect(lineLegend.sizedSeries).toHaveLength(2)
expect(lineLegend.visibleSeriesNames).toEqual(["Canada"])
})

it("prioritises labels based on importance sorting", () => {
const lineLegend = new LineLegend({
series,
yAxis: makeAxis({ yRange: [0, 50] }),
seriesNamesSortedByImportance: ["Mexico", "Canada"],
})

// 'Mexico' is picked since it's given higher importance
expect(lineLegend.visibleSeriesNames).toEqual(["Mexico"])
})

it("skips more important series if they don't fit", () => {
const series = makeSeries([
{ seriesName: "Canada", yValue: 5 },
{ seriesName: "Mexico", yValue: 20 },
{ seriesName: "Spain", yValue: 40 },
{ seriesName: "Democratic Republic of Congo", yValue: 45 },
])

const lineLegend = new LineLegend({
series,
yAxis: makeAxis({ yRange: [0, 50] }),
maxWidth: 100,
seriesNamesSortedByImportance: [
"Mexico",
"Canada",
"Democratic Republic of Congo",
"Spain",
],
})

// 'Democratic Republic of Congo' is skipped since it doesn't fit
expect(lineLegend.visibleSeriesNames).toEqual([
"Mexico",
"Canada",
"Spain",
])
})

it("prioritises to label focused series", () => {
const seriesWithFocus = series.map((s) => ({
...s,
focus: {
active: s.seriesName === "Mexico",
background: s.seriesName !== "Mexico",
},
}))

const lineLegendWithFocus = new LineLegend({
series: seriesWithFocus,
yAxis: makeAxis({ yRange: [0, 50] }),
})

// 'Mexico' is picked since it's focused
expect(lineLegendWithFocus.visibleSeriesNames).toEqual(["Mexico"])
})

it("uses all available space", () => {
const series = makeSeries([
{ seriesName: "Canada", yValue: 5 },
{ seriesName: "Mexico", yValue: 20 },
{ seriesName: "Spain", yValue: 40 },
{ seriesName: "France", yValue: 45 },
])

const yRange: [number, number] = [0, 50]
const lineLegend = new LineLegend({
series,
yAxis: makeAxis({ yRange }),
})

// 'Spain' is dropped since it doesn't fit
expect(lineLegend.visibleSeriesNames).toEqual([
"Canada",
"Mexico",
"France",
])

// verify that we can't fit 'Spain' into the available space
const droppedLabel = lineLegend.sizedSeries.find(
(series) => series.seriesName === "Spain"
)!
const droppedLabelHeight = droppedLabel.height + LEGEND_ITEM_MIN_SPACING
const availableHeight = yRange[1] - yRange[0]
const remainingHeight = availableHeight - lineLegend.visibleSeriesHeight
expect(remainingHeight).toBeLessThan(droppedLabelHeight)
})

it("picks labels from the edges", () => {
const series = makeSeries([
{ seriesName: "Canada", yValue: 10 },
{ seriesName: "Mexico", yValue: 50 },
{ seriesName: "France", yValue: 90 },
])

const lineLegend = new LineLegend({
series,
yAxis: makeAxis({ yRange: [0, 40] }),
})

expect(lineLegend.visibleSeriesNames).toEqual(["Canada", "France"])
})

it("picks labels in a balanced way", () => {
const series = makeSeries([
{ seriesName: "Canada", yValue: 10 },
{ seriesName: "Mexico", yValue: 12 },
{ seriesName: "Brazil", yValue: 14 },
{ seriesName: "Argentina", yValue: 15 },
{ seriesName: "Chile", yValue: 60 },
{ seriesName: "Peru", yValue: 90 },
])

const lineLegend = new LineLegend({
series,
yAxis: makeAxis({ yRange: [0, 50] }),
})

expect(legend.sizedLabels.length).toEqual(2)
// drops 'Mexico', 'Brazil' and 'Argentina' since they're close to each other
expect(lineLegend.visibleSeriesNames).toEqual([
"Canada",
"Chile",
"Peru",
])
})
})
Loading

0 comments on commit 332107f

Please sign in to comment.