Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Generate the release cycle chart directly as SVG
Instead of Mermaid, use jinja2. Sphinx uses jinja for templating as well,
so it might be possible to integrate this more tightly,
but an include file works nicely for now.
(It's actually easy to generate this chart just with f-strings,
but I don't want to set a bad example for people who shouldn't fully
trust their input. Jinja has autoescaping to prevent SVG injections.)

Styling is done from the main stylesheet, and is bit more straightforward.
I've adjusted the colours to be a bit friendlier to colour-blind people.

There are vertical lines for all years now -- Mermaid's skipping of
every other year was pretty confusing. Year labels have been shortened.
This should work for another 10 years.

The caption is removed; it was redundant in our case.

Precise sizing is hard to do with SVG, but the font family, size and
line height should nearly match the main text on big screens. At least
in the current theme.

In the code, `sorted_versions` is now a list, and the dicts in it have
some extra generated info.
  • Loading branch information
encukou committed Jan 19, 2023
commit 0e5ccfe7f9a0da10895d0046ddd374a0a037a816
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Generated files
Comment thread
hugovk marked this conversation as resolved.
Outdated
# https://github.com/github/linguist/blob/master/docs/overrides.md
#
# To always hide generated files in local diffs, mark them as binary:
# $ git config diff.generated.binary true
#
[attr]generated linguist-generated=true diff=generated

include/release-cycle.svg generated
include/branches.csv generated
include/end-of-life.csv generated
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ include/branches.csv: include/release-cycle.json
include/end-of-life.csv: include/release-cycle.json
$(PYTHON) _tools/generate_release_cycle.py

include/release-cycle.mmd: include/release-cycle.json
include/release-cycle.svg: include/release-cycle.json
$(PYTHON) _tools/generate_release_cycle.py

.PHONY: versions
versions: include/branches.csv include/end-of-life.csv include/release-cycle.mmd
versions: include/branches.csv include/end-of-life.csv include/release-cycle.svg
@echo Release cycle data generated.
98 changes: 51 additions & 47 deletions _static/devguide_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,70 @@
}

/* Release cycle chart */
#python-release-cycle .mermaid .active0,
#python-release-cycle .mermaid .active1,
#python-release-cycle .mermaid .active2,
#python-release-cycle .mermaid .active3 {
fill: #00dd00;
stroke: darkgreen;

.release-cycle-chart {
width: 100%;
/* filter: grayscale(100%); */
Comment thread
hugovk marked this conversation as resolved.
Outdated
}

.release-cycle-chart .release-cycle-year-line {
stroke: var(--color-foreground-primary);
stroke-width: 0.8px;
opacity: 75%;
}

.release-cycle-chart .release-cycle-year-text {
fill: var(--color-foreground-primary);
}

.release-cycle-chart .release-cycle-today-line {
stroke: var(--color-brand-primary);
stroke-width: 1.6px;
}

#python-release-cycle .mermaid .done0,
#python-release-cycle .mermaid .done1,
#python-release-cycle .mermaid .done2,
#python-release-cycle .mermaid .done3 {
fill: orange;
stroke: darkorange;
.release-cycle-chart .release-cycle-row-shade {
fill: var(--color-background-item);
opacity: 50%;
}

#python-release-cycle .mermaid .task0,
#python-release-cycle .mermaid .task1,
#python-release-cycle .mermaid .task2,
#python-release-cycle .mermaid .task3 {
fill: #007acc;
stroke: #004455;
.release-cycle-chart .release-cycle-version-label {
fill: var(--color-foreground-primary);
}

#python-release-cycle .mermaid .section0,
#python-release-cycle .mermaid .section2 {
fill: darkgrey;
.release-cycle-chart .release-cycle-blob {
stroke-width: 1.6px;
/* default colours, overriden below for individual statuses */
fill: var(--color-background-primary);
stroke: var(--color-foreground-primary);
}

/* Set master colours */
:root {
--mermaid-section1-3: white;
--mermaid-text-color: black;
.release-cycle-chart .release-cycle-blob-label {
/* white looks good on both light & dark */
fill: white;
filter:
drop-shadow(1px 1px 0.5px rgba(0, 0, 0, 0.5))
drop-shadow(-1px 1px 0.5px rgba(0, 0, 0, 0.5))
drop-shadow(1px -1px 0.5px rgba(0, 0, 0, 0.5))
drop-shadow(-1px -1px 0.5px rgba(0, 0, 0, 0.5))
;
Comment thread
CAM-Gerlach marked this conversation as resolved.
Outdated
}

@media (prefers-color-scheme: dark) {
body[data-theme=auto] {
--mermaid-section1-3: black;
--mermaid-text-color: #ffffffcc;
}
.release-cycle-chart .release-cycle-blob-end-of-life {
fill: #DD2200;
stroke: #FF8888;
}
body[data-theme=dark] {
--mermaid-section1-3: black;
--mermaid-text-color: #ffffffcc;

.release-cycle-chart .release-cycle-blob-security {
fill: #FFDD44;
stroke: #FF8800;
}

#python-release-cycle .mermaid .section1,
#python-release-cycle .mermaid .section3 {
fill: var(--mermaid-section1-3);
.release-cycle-chart .release-cycle-blob-bugfix {
fill: #00DD22;
stroke: #008844;
}

#python-release-cycle .mermaid .grid .tick text,
#python-release-cycle .mermaid .sectionTitle0,
#python-release-cycle .mermaid .sectionTitle1,
#python-release-cycle .mermaid .sectionTitle2,
#python-release-cycle .mermaid .sectionTitle3,
#python-release-cycle .mermaid .taskTextOutside0,
#python-release-cycle .mermaid .taskTextOutside1,
#python-release-cycle .mermaid .taskTextOutside2,
#python-release-cycle .mermaid .taskTextOutside3,
#python-release-cycle .mermaid .titleText {
fill: var(--mermaid-text-color);
.release-cycle-chart .release-cycle-blob-feature {
fill: #2222EE;
stroke: #008888;
}
124 changes: 78 additions & 46 deletions _tools/generate_release_cycle.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
"""Read in a JSON and generate two CSVs and a Mermaid file."""
"""Read in a JSON and generate two CSVs and a SVG file."""
Comment thread
hugovk marked this conversation as resolved.
Outdated
from __future__ import annotations

import csv
import datetime as dt
import json

MERMAID_HEADER = """
gantt
dateFormat YYYY-MM-DD
title Python release cycle
axisFormat %Y
""".lstrip()

MERMAID_SECTION = """
section Python {version}
{release_status} :{mermaid_status} python{version}, {first_release},{eol}
""" # noqa: E501

MERMAID_STATUS_MAPPING = {
"feature": "",
"bugfix": "active,",
"security": "done,",
"end-of-life": "crit,",
}
import jinja2


def csv_date(date_str: str, now_str: str) -> str:
Expand All @@ -32,24 +15,27 @@ def csv_date(date_str: str, now_str: str) -> str:
return f"*{date_str}*"
return date_str


def mermaid_date(date_str: str) -> str:
"""Format a date for Mermaid."""
def parse_date(date_str: str) -> dt.date:
if len(date_str) == len("yyyy-mm"):
# Mermaid needs a full yyyy-mm-dd, so let's approximate
date_str = f"{date_str}-01"
return date_str

# We need a full yyyy-mm-dd, so let's approximate
return dt.date.fromisoformat(date_str + '-01')
return dt.date.fromisoformat(date_str)

class Versions:
"""For converting JSON to CSV and Mermaid."""
"""For converting JSON to CSV and SVG."""

def __init__(self) -> None:
with open("include/release-cycle.json", encoding="UTF-8") as in_file:
self.versions = json.load(in_file)

# Generate a few additional fields
for key, version in self.versions.items():
version['key'] = key
version['first_release_date'] = parse_date(version['first_release'])
version['end_of_life_date'] = parse_date(version['end_of_life'])
self.sorted_versions = sorted(
self.versions.items(),
key=lambda k: [int(i) for i in k[0].split(".")],
self.versions.values(),
key=lambda v: [int(i) for i in v['key'].split(".")],
reverse=True,
)

Expand All @@ -59,7 +45,7 @@ def write_csv(self) -> None:

versions_by_category = {"branches": {}, "end-of-life": {}}
headers = None
for version, details in self.sorted_versions:
for details in self.sorted_versions:
row = {
"Branch": details["branch"],
"Schedule": f":pep:`{details['pep']}`",
Expand All @@ -70,38 +56,84 @@ def write_csv(self) -> None:
}
headers = row.keys()
cat = "end-of-life" if details["status"] == "end-of-life" else "branches"
versions_by_category[cat][version] = row
versions_by_category[cat][details['key']] = row

for cat, versions in versions_by_category.items():
with open(f"include/{cat}.csv", "w", encoding="UTF-8", newline="") as file:
csv_file = csv.DictWriter(file, fieldnames=headers, lineterminator="\n")
csv_file.writeheader()
csv_file.writerows(versions.values())

def write_mermaid(self) -> None:
"""Output Mermaid file."""
out = [MERMAID_HEADER]
def write_svg(self) -> None:
"""Output SVG file."""
env = jinja2.Environment(
loader=jinja2.FileSystemLoader('_tools/'),
autoescape=True,
Comment thread
hugovk marked this conversation as resolved.
undefined=jinja2.StrictUndefined,
)
template = env.get_template("release_cycle_template.svg")

# Scale. Should be roughly the pixel size of the font.
# All later sizes are miltiplied by this, so you can think of all other
# numbers being multiples of the font size, like using `em` units in
# CSS.
# (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
# those.)
SCALE = 18

# Width of the drawing and main parts
DIAGRAM_WIDTH = 46
LEGEND_WIDTH = 7
RIGHT_MARGIN = 0.5

# Height of one line. If you change this you'll need to tweak
# some positioning nombers in the template as well.
Comment thread
hugovk marked this conversation as resolved.
Outdated
LINE_HEIGHT = 1.5

first_date = min(
ver['first_release_date'] for ver in self.sorted_versions
)
last_date = max(
ver['end_of_life_date'] for ver in self.sorted_versions
)

def date_to_x(date):
Comment thread
hugovk marked this conversation as resolved.
Outdated
"""Convert datetime.date to a SVG X coordinate"""
Comment thread
hugovk marked this conversation as resolved.
Outdated
num_days = (date - first_date).days
total_days = (last_date - first_date).days
ratio = num_days / total_days
x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN)
return x + LEGEND_WIDTH

def year_to_x(year):
Comment thread
hugovk marked this conversation as resolved.
Outdated
"""Convert year number to a SVG X coordinate of 1st January"""
Comment thread
hugovk marked this conversation as resolved.
Outdated
return date_to_x(dt.date(year, 1, 1))

for version, details in reversed(self.versions.items()):
v = MERMAID_SECTION.format(
version=version,
first_release=details["first_release"],
eol=mermaid_date(details["end_of_life"]),
release_status=details["status"],
mermaid_status=MERMAID_STATUS_MAPPING[details["status"]],
)
out.append(v)
def format_year(year):
Comment thread
hugovk marked this conversation as resolved.
Outdated
"""Format year number for display"""
return f"'{year % 100:02}"

with open(
"include/release-cycle.mmd", "w", encoding="UTF-8", newline="\n"
"include/release-cycle.svg", "w", encoding="UTF-8",
Comment thread
hugovk marked this conversation as resolved.
Outdated
) as f:
f.writelines(out)
template.stream(
SCALE=SCALE,
diagram_width=DIAGRAM_WIDTH,
diagram_height=(len(self.sorted_versions) + 2) * LINE_HEIGHT,
years=range(first_date.year, last_date.year),
LINE_HEIGHT=LINE_HEIGHT,
versions=list(reversed(self.sorted_versions)),
today=dt.date.today(),
year_to_x=year_to_x,
date_to_x=date_to_x,
format_year=format_year,
).dump(f)


def main() -> None:
versions = Versions()
versions.write_csv()
versions.write_mermaid()
versions.write_svg()


if __name__ == "__main__":
Expand Down
Loading