"""Draws the release timeline of home/releases/index.md as an inline SVG into docs/images/.
The releases and their dates come from the tables of that page (rows like `| [v3.11.2](v3.11.2.md) | 2022-08-12 | ...`),
so after adding a release to the page, run `python3 scripts/figures/release_timeline.py` from docs/mkdocs and commit
the regenerated docs/images/release_timeline.svg. The page includes the SVG text with
`--8<-- "images/release_timeline.svg"`, so it follows the light/dark toggle (see the `nj-fig` rules in
docs/css/brand.css and the `nj-int-*` rules in docs/css/fig-integration.css). Every marker has a
tooltip.
Rows without a date (the "Unreleased" row) are skipped; any other release row that cannot be parsed stops the script.
"""
import datetime
import re
import sys
from pathlib import Path
DOCS = Path(__file__).resolve().parents[2] / "docs"
OUT = Path(__file__).resolve().parents[2] / "docs" / "images"
PAGE = DOCS / "home" / "releases" / "index.md"
ROW = re.compile(r"^\|\s*\[v(\d+)\.(\d+)\.(\d+)\]\([^)]*\)\s*\|\s*([^|]*?)\s*\|")
W, LEFT, RIGHT = 880, 24, 856
LABEL_ROW, CHAR_W = 17, 7.0
def parse(markdown):
releases = []
for line in markdown.splitlines():
if not line.startswith("| [v"):
continue
m = ROW.match(line)
if not m:
sys.exit("cannot parse release row: " + line)
try:
date = datetime.date.fromisoformat(m.group(4))
except ValueError:
sys.exit("cannot parse date in row: " + line)
releases.append(((int(m.group(1)), int(m.group(2)), int(m.group(3))), date))
if len(releases) < 2:
sys.exit("found fewer than two releases in " + str(PAGE))
if len({v for v, _ in releases}) != len(releases):
sys.exit("duplicate release in " + str(PAGE))
return sorted(releases, key=lambda r: (r[1], r[0]))
def name(version):
return "v%d.%d.%d" % version
def draw(releases):
first, last = releases[0][1], releases[-1][1]
start, end = datetime.date(first.year, 1, 1), datetime.date(last.year + 1, 1, 1)
span = (end - start).days
def xof(date):
return LEFT + (RIGHT - LEFT) * (date - start).days / span
# minor labels, placed greedily into the lowest free row above the axis
rows_end, labels = [], []
for version, date in releases:
if version[2] != 0:
continue
text = "%d.%d" % version[:2]
x, half = xof(date), len(text) * CHAR_W / 2 + 2
row = next((i for i, e in enumerate(rows_end) if e < x - half), None)
if row is None:
rows_end.append(0)
row = len(rows_end) - 1
rows_end[row] = x + half
labels.append((x, row, text))
top = 14 + LABEL_ROW * len(rows_end)
axis = top + 12
height = axis + 70
s = ['')
return "\n".join(s) + "\n"
releases = parse(PAGE.read_text(encoding="utf-8"))
(OUT / "release_timeline.svg").write_text(draw(releases), encoding="utf-8")
print("wrote", OUT / "release_timeline.svg", "with", len(releases), "releases")