Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6d62d63

Browse files
committed
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.
1 parent 9513ac8 commit 6d62d63

10 files changed

+1455
-162
lines changed

.gitattributes

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Generated files
2+
# https://github.com/github/linguist/blob/master/docs/overrides.md
3+
#
4+
# To always hide generated files in local diffs, mark them as binary:
5+
# $ git config diff.generated.binary true
6+
#
7+
[attr]generated linguist-generated=true diff=generated
8+
9+
include/release-cycle.svg generated
10+
include/branches.csv generated
11+
include/end-of-life.csv generated

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,9 @@ include/branches.csv: include/release-cycle.json
194194
include/end-of-life.csv: include/release-cycle.json
195195
$(PYTHON) _tools/generate_release_cycle.py
196196

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

200200
.PHONY: versions
201-
versions: include/branches.csv include/end-of-life.csv include/release-cycle.mmd
201+
versions: include/branches.csv include/end-of-life.csv include/release-cycle.svg
202202
@echo Release cycle data generated.

_static/devguide_overrides.css

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,66 +7,70 @@
77
}
88

99
/* Release cycle chart */
10-
#python-release-cycle .mermaid .active0,
11-
#python-release-cycle .mermaid .active1,
12-
#python-release-cycle .mermaid .active2,
13-
#python-release-cycle .mermaid .active3 {
14-
fill: #00dd00;
15-
stroke: darkgreen;
10+
11+
.release-cycle-chart {
12+
width: 100%;
13+
/* filter: grayscale(100%); */
14+
}
15+
16+
.release-cycle-chart .release-cycle-year-line {
17+
stroke: var(--color-foreground-primary);
18+
stroke-width: 0.8px;
19+
opacity: 75%;
20+
}
21+
22+
.release-cycle-chart .release-cycle-year-text {
23+
fill: var(--color-foreground-primary);
24+
}
25+
26+
.release-cycle-chart .release-cycle-today-line {
27+
stroke: var(--color-brand-primary);
28+
stroke-width: 1.6px;
1629
}
1730

18-
#python-release-cycle .mermaid .done0,
19-
#python-release-cycle .mermaid .done1,
20-
#python-release-cycle .mermaid .done2,
21-
#python-release-cycle .mermaid .done3 {
22-
fill: orange;
23-
stroke: darkorange;
31+
.release-cycle-chart .release-cycle-row-shade {
32+
fill: var(--color-background-item);
33+
opacity: 50%;
2434
}
2535

26-
#python-release-cycle .mermaid .task0,
27-
#python-release-cycle .mermaid .task1,
28-
#python-release-cycle .mermaid .task2,
29-
#python-release-cycle .mermaid .task3 {
30-
fill: #007acc;
31-
stroke: #004455;
36+
.release-cycle-chart .release-cycle-version-label {
37+
fill: var(--color-foreground-primary);
3238
}
3339

34-
#python-release-cycle .mermaid .section0,
35-
#python-release-cycle .mermaid .section2 {
36-
fill: darkgrey;
40+
.release-cycle-chart .release-cycle-blob {
41+
stroke-width: 1.6px;
42+
/* default colours, overriden below for individual statuses */
43+
fill: var(--color-background-primary);
44+
stroke: var(--color-foreground-primary);
3745
}
3846

39-
/* Set master colours */
40-
:root {
41-
--mermaid-section1-3: white;
42-
--mermaid-text-color: black;
47+
.release-cycle-chart .release-cycle-blob-label {
48+
/* white looks good on both light & dark */
49+
fill: white;
50+
filter:
51+
drop-shadow(1px 1px 0.5px rgba(0, 0, 0, 0.5))
52+
drop-shadow(-1px 1px 0.5px rgba(0, 0, 0, 0.5))
53+
drop-shadow(1px -1px 0.5px rgba(0, 0, 0, 0.5))
54+
drop-shadow(-1px -1px 0.5px rgba(0, 0, 0, 0.5))
55+
;
4356
}
4457

45-
@media (prefers-color-scheme: dark) {
46-
body[data-theme=auto] {
47-
--mermaid-section1-3: black;
48-
--mermaid-text-color: #ffffffcc;
49-
}
58+
.release-cycle-chart .release-cycle-blob-end-of-life {
59+
fill: #DD2200;
60+
stroke: #FF8888;
5061
}
51-
body[data-theme=dark] {
52-
--mermaid-section1-3: black;
53-
--mermaid-text-color: #ffffffcc;
62+
63+
.release-cycle-chart .release-cycle-blob-security {
64+
fill: #FFDD44;
65+
stroke: #FF8800;
5466
}
5567

56-
#python-release-cycle .mermaid .section1,
57-
#python-release-cycle .mermaid .section3 {
58-
fill: var(--mermaid-section1-3);
68+
.release-cycle-chart .release-cycle-blob-bugfix {
69+
fill: #00DD22;
70+
stroke: #008844;
5971
}
6072

61-
#python-release-cycle .mermaid .grid .tick text,
62-
#python-release-cycle .mermaid .sectionTitle0,
63-
#python-release-cycle .mermaid .sectionTitle1,
64-
#python-release-cycle .mermaid .sectionTitle2,
65-
#python-release-cycle .mermaid .sectionTitle3,
66-
#python-release-cycle .mermaid .taskTextOutside0,
67-
#python-release-cycle .mermaid .taskTextOutside1,
68-
#python-release-cycle .mermaid .taskTextOutside2,
69-
#python-release-cycle .mermaid .taskTextOutside3,
70-
#python-release-cycle .mermaid .titleText {
71-
fill: var(--mermaid-text-color);
73+
.release-cycle-chart .release-cycle-blob-feature {
74+
fill: #2222EE;
75+
stroke: #008888;
7276
}

_tools/generate_release_cycle.py

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
1-
"""Read in a JSON and generate two CSVs and a Mermaid file."""
1+
"""Read in a JSON and generate two CSVs and a SVG file."""
22
from __future__ import annotations
33

44
import csv
55
import datetime as dt
66
import json
77

8-
MERMAID_HEADER = """
9-
gantt
10-
dateFormat YYYY-MM-DD
11-
title Python release cycle
12-
axisFormat %Y
13-
""".lstrip()
14-
15-
MERMAID_SECTION = """
16-
section Python {version}
17-
{release_status} :{mermaid_status} python{version}, {first_release},{eol}
18-
""" # noqa: E501
19-
20-
MERMAID_STATUS_MAPPING = {
21-
"feature": "",
22-
"bugfix": "active,",
23-
"security": "done,",
24-
"end-of-life": "crit,",
25-
}
8+
import jinja2
269

2710

2811
def csv_date(date_str: str, now_str: str) -> str:
@@ -32,24 +15,27 @@ def csv_date(date_str: str, now_str: str) -> str:
3215
return f"*{date_str}*"
3316
return date_str
3417

35-
36-
def mermaid_date(date_str: str) -> str:
37-
"""Format a date for Mermaid."""
18+
def parse_date(date_str: str) -> dt.date:
3819
if len(date_str) == len("yyyy-mm"):
39-
# Mermaid needs a full yyyy-mm-dd, so let's approximate
40-
date_str = f"{date_str}-01"
41-
return date_str
42-
20+
# We need a full yyyy-mm-dd, so let's approximate
21+
return dt.date.fromisoformat(date_str + '-01')
22+
return dt.date.fromisoformat(date_str)
4323

4424
class Versions:
45-
"""For converting JSON to CSV and Mermaid."""
25+
"""For converting JSON to CSV and SVG."""
4626

4727
def __init__(self) -> None:
4828
with open("include/release-cycle.json", encoding="UTF-8") as in_file:
4929
self.versions = json.load(in_file)
30+
31+
# Generate a few additional fields
32+
for key, version in self.versions.items():
33+
version['key'] = key
34+
version['first_release_date'] = parse_date(version['first_release'])
35+
version['end_of_life_date'] = parse_date(version['end_of_life'])
5036
self.sorted_versions = sorted(
51-
self.versions.items(),
52-
key=lambda k: [int(i) for i in k[0].split(".")],
37+
self.versions.values(),
38+
key=lambda v: [int(i) for i in v['key'].split(".")],
5339
reverse=True,
5440
)
5541

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

6046
versions_by_category = {"branches": {}, "end-of-life": {}}
6147
headers = None
62-
for version, details in self.sorted_versions:
48+
for details in self.sorted_versions:
6349
row = {
6450
"Branch": details["branch"],
6551
"Schedule": f":pep:`{details['pep']}`",
@@ -70,38 +56,84 @@ def write_csv(self) -> None:
7056
}
7157
headers = row.keys()
7258
cat = "end-of-life" if details["status"] == "end-of-life" else "branches"
73-
versions_by_category[cat][version] = row
59+
versions_by_category[cat][details['key']] = row
7460

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

81-
def write_mermaid(self) -> None:
82-
"""Output Mermaid file."""
83-
out = [MERMAID_HEADER]
84-
85-
for version, details in reversed(self.versions.items()):
86-
v = MERMAID_SECTION.format(
87-
version=version,
88-
first_release=details["first_release"],
89-
eol=mermaid_date(details["end_of_life"]),
90-
release_status=details["status"],
91-
mermaid_status=MERMAID_STATUS_MAPPING[details["status"]],
92-
)
93-
out.append(v)
67+
def write_svg(self) -> None:
68+
"""Output SVG file."""
69+
env = jinja2.Environment(
70+
loader=jinja2.FileSystemLoader('_tools/'),
71+
autoescape=True,
72+
undefined=jinja2.StrictUndefined,
73+
)
74+
template = env.get_template("release_cycle_template.svg")
75+
76+
# Scale. Should be roughly the pixel size of the font.
77+
# All later sizes are miltiplied by this, so you can think of all other
78+
# numbers being multiples of the font size, like using `em` units in
79+
# CSS.
80+
# (Ideally we'd actually use `em` units, but SVG viewBox doesn't take
81+
# those.)
82+
SCALE = 18
83+
84+
# Width of the drawing and main parts
85+
DIAGRAM_WIDTH = 46
86+
LEGEND_WIDTH = 7
87+
RIGHT_MARGIN = 0.5
88+
89+
# Height of one line. If you change this you'll need to tweak
90+
# some positioning nombers in the template as well.
91+
LINE_HEIGHT = 1.5
92+
93+
first_date = min(
94+
ver['first_release_date'] for ver in self.sorted_versions
95+
)
96+
last_date = max(
97+
ver['end_of_life_date'] for ver in self.sorted_versions
98+
) + dt.timedelta(days=10*365)
99+
100+
def date_to_x(date):
101+
"""Convert datetime.date to a SVG X coordinate"""
102+
num_days = (date - first_date).days
103+
total_days = (last_date - first_date).days
104+
ratio = num_days / total_days
105+
x = ratio * (DIAGRAM_WIDTH - LEGEND_WIDTH - RIGHT_MARGIN)
106+
return x + LEGEND_WIDTH
107+
108+
def year_to_x(year):
109+
"""Convert year number to a SVG X coordinate of 1st January"""
110+
return date_to_x(dt.date(year, 1, 1))
111+
112+
def format_year(year):
113+
"""Format year number for display"""
114+
return f"'{year % 100:02}"
94115

95116
with open(
96-
"include/release-cycle.mmd", "w", encoding="UTF-8", newline="\n"
117+
"include/release-cycle.svg", "w", encoding="UTF-8",
97118
) as f:
98-
f.writelines(out)
119+
template.stream(
120+
SCALE=SCALE,
121+
diagram_width=DIAGRAM_WIDTH,
122+
diagram_height=(len(self.sorted_versions) + 2) * LINE_HEIGHT,
123+
years=range(first_date.year, last_date.year),
124+
LINE_HEIGHT=LINE_HEIGHT,
125+
versions=list(reversed(self.sorted_versions)),
126+
today=dt.date.today(),
127+
year_to_x=year_to_x,
128+
date_to_x=date_to_x,
129+
format_year=format_year,
130+
).dump(f)
99131

100132

101133
def main() -> None:
102134
versions = Versions()
103135
versions.write_csv()
104-
versions.write_mermaid()
136+
versions.write_svg()
105137

106138

107139
if __name__ == "__main__":

0 commit comments

Comments
 (0)