feat(profile): embed commit heatmap
This commit is contained in:
148
scripts/generate_heatmap_svg.py
Normal file
148
scripts/generate_heatmap_svg.py
Normal file
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
COLORS = [
|
||||
"#ebedf0",
|
||||
"#9be9a8",
|
||||
"#40c463",
|
||||
"#30a14e",
|
||||
"#216e39",
|
||||
]
|
||||
|
||||
CELL = 12
|
||||
GAP = 2
|
||||
RECT = CELL - GAP
|
||||
LEFT_PAD = 30
|
||||
TOP_PAD = 20
|
||||
RIGHT_PAD = 10
|
||||
BOTTOM_PAD = 10
|
||||
FONT_FAMILY = "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif"
|
||||
LABEL_COLOR = "#57606a"
|
||||
|
||||
|
||||
def load_heatmap(path):
|
||||
with path.open() as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def date_range(start, end):
|
||||
current = start
|
||||
while current <= end:
|
||||
yield current
|
||||
current += timedelta(days=1)
|
||||
|
||||
|
||||
def previous_sunday(day):
|
||||
return day - timedelta(days=(day.weekday() + 1) % 7)
|
||||
|
||||
|
||||
def next_saturday(day):
|
||||
return day + timedelta(days=(5 - day.weekday()) % 7)
|
||||
|
||||
|
||||
def intensity(count, max_count):
|
||||
if count <= 0 or max_count <= 0:
|
||||
return 0
|
||||
return max(1, min(4, math.ceil((count / max_count) * 4)))
|
||||
|
||||
|
||||
def weekday_row(day):
|
||||
return (day.weekday() + 1) % 7
|
||||
|
||||
|
||||
def month_labels(start, end):
|
||||
labels = [(0, start.strftime("%b"))]
|
||||
total_weeks = ((end - start).days // 7) + 1
|
||||
|
||||
for week in range(total_weeks):
|
||||
week_start = start + timedelta(days=week * 7)
|
||||
for offset in range(7):
|
||||
day = week_start + timedelta(days=offset)
|
||||
if day.day == 1 and week != 0:
|
||||
labels.append((week, day.strftime("%b")))
|
||||
break
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
def build_rect(day, start, counts, max_count):
|
||||
date_str = day.isoformat()
|
||||
count = counts.get(date_str, 0)
|
||||
level = intensity(count, max_count)
|
||||
x = LEFT_PAD + (((day - start).days // 7) * CELL)
|
||||
y = TOP_PAD + (weekday_row(day) * CELL)
|
||||
|
||||
commit_label = "commit" if count == 1 else "commits"
|
||||
|
||||
return (
|
||||
f'<rect x="{x}" y="{y}" width="{RECT}" height="{RECT}" rx="2" ry="2" '
|
||||
f'fill="{COLORS[level]}">'
|
||||
f"<title>{date_str}: {count} {commit_label}</title>"
|
||||
"</rect>"
|
||||
)
|
||||
|
||||
|
||||
def generate_svg(data):
|
||||
counts = {entry["date"]: entry["count"] for entry in data["days"]}
|
||||
max_count = data["max_daily_commits"]
|
||||
|
||||
start = datetime.strptime(data["from_date"], "%Y-%m-%d").date()
|
||||
end = datetime.strptime(data["to_date"], "%Y-%m-%d").date()
|
||||
aligned_start = previous_sunday(start)
|
||||
aligned_end = next_saturday(end)
|
||||
weeks = ((aligned_end - aligned_start).days // 7) + 1
|
||||
|
||||
width = LEFT_PAD + (weeks * CELL) + RIGHT_PAD
|
||||
height = TOP_PAD + (7 * CELL) + BOTTOM_PAD
|
||||
|
||||
svg = [
|
||||
f'<svg width="{width}" height="{height}" viewBox="0 0 {width} {height}" '
|
||||
'xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Commit activity heatmap">',
|
||||
'<style>',
|
||||
f'text {{ font: 10px {FONT_FAMILY}; fill: {LABEL_COLOR}; }}',
|
||||
'</style>',
|
||||
]
|
||||
|
||||
for week, label in month_labels(aligned_start, aligned_end):
|
||||
x = LEFT_PAD + (week * CELL)
|
||||
svg.append(f'<text x="{x}" y="10">{label}</text>')
|
||||
|
||||
for label, row in (("Mon", 1), ("Wed", 3), ("Fri", 5)):
|
||||
y = TOP_PAD + (row * CELL) + 8
|
||||
svg.append(f'<text x="0" y="{y}">{label}</text>')
|
||||
|
||||
for day in date_range(aligned_start, aligned_end):
|
||||
svg.append(build_rect(day, aligned_start, counts, max_count))
|
||||
|
||||
svg.append("</svg>")
|
||||
return "\n".join(svg) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) not in {2, 3}:
|
||||
print(
|
||||
"Usage: generate_heatmap_svg.py <heatmap.json> [output.svg]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
input_path = Path(sys.argv[1])
|
||||
output = generate_svg(load_heatmap(input_path))
|
||||
|
||||
if len(sys.argv) == 3:
|
||||
output_path = Path(sys.argv[2])
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(output)
|
||||
else:
|
||||
sys.stdout.write(output)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user