Walldye

Montreal lap chart

A lap chart of the 2011 Canadian Grand Prix, which rain stopped for over two hours.

Made with Claude Opus 5.5

Unofficial fan tribute, not affiliated with or endorsed by Formula One Group. Formula 1 and its characters are trademarks of their owners. Non-commercial; contact takedown@walldye.com for takedown.

Technique
line art, instrument displays
Shape
Any screen
Added
28 September 2026

Export

Shape
cropped from 16:9
Crop
Size
Format

Colors

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Notes

Each line is one car’s race position at the end of every lap. The race started behind the safety car on a wet track and was stopped for over two hours in heavy rain, and positions are held through the stoppage.

Button made five pit stops and served a drive-through for speeding behind the safety car. On lap 37 he touched Fernando Alonso at turn 3, pitted with a puncture and fell to 21st and last. Sebastian Vettel was leading on the final lap when he ran wide at turn 6, and Button went by to win. The positions come from jolpica-f1, a community-run archive of Formula 1 results that carries on the Ergast database.

Sources

  1. jolpica-f1.
  2. 2011 Canadian Grand Prix.

Source code

wallpapers/racing-canada-lap-chart/design.py, 127 lines

"""The 2011 Canadian Grand Prix as a lap chart: one line per car stepping between positions over 70 laps."""

from typing import TypedDict

import numpy as np
from numpy.typing import NDArray
from shapely.geometry import LineString, Point

from walldye import ACCENT, ACCENT_3, ACCENT_4, BG, UI, UI_ALT, Canvas, P, by_regime, design

type Arr = NDArray[np.float64]


class Car(TypedDict):
    id: str
    grid: int
    out: bool  # retired, so the line stops short
    pos: list[int]  # position at the end of each lap
    pits: list[int]  # laps with a stop or a drive-through


LAPS, CARS = 70, 24
HALT = 24  # the race was stopped for two hours during the next lap; the chart breaks here
GAP = 3.0  # width of the break, in laps
# Safety car laps, read off the leader's lap times; the one across the break is split round it.
SC = ((1, 4), (9, 12), (20, 24), (25, 34), (38, 40), (57, 60))
DROP = 36  # the winner's last lap before the puncture put him last
SPAN = LAPS + GAP
LEAD = by_regime(ACCENT_4, ACCENT_3)  # the winner's line before the drop


def lap_u(k: float) -> float:
    """Distance along the lap axis, in lap widths, with the break after HALT."""
    return k + (GAP if k > HALT else 0.0)


def line(car: Car) -> Arr:
    """(u, v) chart points from the grid to the last lap, one per lap plus the break's far edge."""
    pts = [(0.0, car["grid"] - 1.0)]
    for k, p in enumerate(car["pos"], 1):
        if k == HALT + 1:
            pts.append((HALT + GAP, pts[-1][1]))
        pts.append((lap_u(k), p - 1.0))
    return np.array(pts)


def idx(k: int) -> int:
    """Index of lap k's point in line()'s output."""
    return k if k <= HALT else k + 1


@design(aspects="any")
def draw(s: Canvas) -> None:
    cars: list[Car] = s.data("laps.json")

    if s.landscape:
        du, dv = min(18.0, s.w * 0.68 / SPAN), 20.0
        x0 = min(s.w - 150, s.w * 0.62 + SPAN * du / 2) - SPAN * du
        y0 = s.h * 0.64 - (CARS - 1) * dv / 2
    else:  # laps stay across, P1 on top, so the chart spans the short side
        du, dv = (s.w - 180) / SPAN, 24.0
        x0, y0 = 90, s.h * 0.66 - (CARS - 1) * dv / 2
    x0, y0 = round(x0), round(y0)

    def at(uv: Arr) -> Arr:
        """Chart (lap, position - 1) to canvas."""
        return np.column_stack([x0 + uv[:, 0] * du, y0 + uv[:, 1] * dv])

    def x(u: float) -> float:
        return x0 + u * du

    # lap axis under the chart, broken where the race stopped, with a longer tick every ten laps
    # and the laps run behind the safety car as a strip beneath it
    ay = y0 + (CARS - 1) * dv + 22
    axis = P().M(x(0), ay).H(x(HALT)).M(x(HALT + GAP), ay).H(x(SPAN))
    for k in range(LAPS + 1):
        axis.M(x(lap_u(k)), ay).V(ay + (10 if k % 10 == 0 else 5))
    s.stroke(axis, UI, 1.2)
    shade = P()
    for a, b in SC:
        ua = HALT + GAP if a == HALT + 1 else lap_u(a - 1)
        shade.rect(x(ua), ay + 14, (lap_u(b) - ua) * du, 4)
    s.fill(shade, UI)

    # positions held through the stoppage, four dots across the break
    held = [
        (HALT + GAP * i / 5, car["pos"][HALT - 1] - 1.0)
        for car in cars
        if len(car["pos"]) >= HALT
        for i in range(1, 5)
    ]
    s.fill(P().dots(at(np.array(held)), 1.0), UI)

    win = next(car for car in cars if car["id"] == "button")
    wpts, cut = line(win), idx(DROP)
    lit = at(wpts[cut:])
    lit_path = LineString(lit)

    def clear(marks: list[Arr]) -> Arr:
        """Marks in canvas units, less those the lit line's knock-out would bite into."""
        xy = at(np.array(marks))
        return xy[[lit_path.distance(Point(p)) >= 6 for p in xy.tolist()]]

    gray = P()
    ends, pit = [], []
    for car in cars:
        if car is win:
            continue
        pts = line(car)
        gray.poly(at(pts[: HALT + 1]))
        if len(pts) > HALT + 1:
            gray.poly(at(pts[HALT + 1 :]))
        if car["out"]:
            ends.append(pts[-1])
        pit += [pts[idx(k)] for k in car["pits"]]
    s.stroke(gray, UI, 1.2, join="round")
    s.fill(P().dots(clear(pit), 2.2), UI_ALT)
    s.fill(P().dots(clear(ends), 2.5), UI_ALT)

    # the winner: dim up to the drop, then lit over a knock-out that cuts the lines it crosses
    lead = P().poly(at(wpts[: HALT + 1])).poly(at(wpts[HALT + 1 : cut + 1]))
    stops = {k: wpts[idx(k)] for k in win["pits"]}
    s.stroke(P().poly(lit), BG, 7, join="round", cap="round")
    s.stroke(lead, LEAD, 1.5, join="round")
    s.fill(P().dots(at(np.array([p for k, p in stops.items() if k <= DROP])), 2.4), LEAD)
    s.stroke(P().poly(lit), ACCENT, 3, join="round", cap="round")
    s.fill(P().dots(at(np.array([p for k, p in stops.items() if k > DROP])), 3.8), ACCENT)

Run it yourself

$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render racing-canada-lap-chart --theme fireproof -o racing-canada-lap-chart-fireproof-16x9.svg