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

🧪 (line legend) add tests for dropping labels #4309

Merged
merged 1 commit into from
Dec 19, 2024
Merged
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
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
Loading