Files
git-activity-merger/app/render/svg.py
Space-Banane a54d1cfeaf first commit
2026-05-29 19:15:00 +02:00

126 lines
4.4 KiB
Python

from datetime import date, timedelta
THEMES: dict[str, dict[str, str | list[str]]] = {
"light": {
"bg": "#ffffff",
"text": "#24292f",
"muted": "#57606a",
"empty": "#ebedf0",
"levels": ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
},
"dark": {
"bg": "#0d1117",
"text": "#c9d1d9",
"muted": "#8b949e",
"empty": "#161b22",
"levels": ["#161b22", "#0e4429", "#006d32", "#26a641", "#39d353"],
},
}
def _intensity_level(value: int, max_value: int) -> int:
if value <= 0 or max_value <= 0:
return 0
ratio = value / max_value
if ratio < 0.25:
return 1
if ratio < 0.5:
return 2
if ratio < 0.75:
return 3
return 4
def _build_calendar_days(from_date: date, to_date: date) -> list[date]:
days: list[date] = []
current = from_date
while current <= to_date:
days.append(current)
current += timedelta(days=1)
return days
def render_activity_svg(
daily_totals: dict[str, int],
from_date: date,
to_date: date,
days_count: int,
total_contributions: int,
theme: str,
) -> str:
palette = THEMES[theme if theme in THEMES else "light"]
levels = palette["levels"]
day_size = 11
gap = 3
left_padding = 42
top_padding = 30
month_label_height = 14
grid_top = top_padding + month_label_height
start = from_date - timedelta(days=(from_date.weekday() + 1) % 7)
end = to_date + timedelta(days=(5 - to_date.weekday()) % 7 + 1)
all_days = _build_calendar_days(start, end)
week_count = max(1, len(all_days) // 7)
grid_width = week_count * (day_size + gap)
grid_height = 7 * (day_size + gap)
width = left_padding + grid_width + 20
height = grid_top + grid_height + 34
max_value = max(daily_totals.values(), default=0)
lines: list[str] = []
lines.append(f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" role="img" aria-label="Contribution graph">')
lines.append(f'<rect width="100%" height="100%" fill="{palette["bg"]}" />')
lines.append(
f'<text x="{left_padding}" y="16" fill="{palette["text"]}" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="12">{total_contributions} contributions in the last {days_count} days</text>'
)
month_x_positions: dict[str, int] = {}
for idx, day in enumerate(all_days):
if day.day == 1:
month_x_positions[day.strftime("%b")] = left_padding + (idx // 7) * (day_size + gap)
for label, x in month_x_positions.items():
lines.append(
f'<text x="{x}" y="{top_padding + 10}" fill="{palette["muted"]}" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="10">{label}</text>'
)
weekday_labels = {1: "Mon", 3: "Wed", 5: "Fri"}
for row, label in weekday_labels.items():
y = grid_top + row * (day_size + gap) + 9
lines.append(
f'<text x="8" y="{y}" fill="{palette["muted"]}" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="10">{label}</text>'
)
for idx, day in enumerate(all_days):
week = idx // 7
row = (day.weekday() + 1) % 7
x = left_padding + week * (day_size + gap)
y = grid_top + row * (day_size + gap)
value = daily_totals.get(day.isoformat(), 0)
level = _intensity_level(value, max_value)
fill = levels[level]
lines.append(
f'<rect x="{x}" y="{y}" width="{day_size}" height="{day_size}" rx="2" ry="2" fill="{fill}" />'
)
legend_y = grid_top + grid_height + 16
legend_x = left_padding + grid_width - 120
lines.append(
f'<text x="{legend_x}" y="{legend_y}" fill="{palette["muted"]}" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="10">Less</text>'
)
for i, color in enumerate(levels):
lx = legend_x + 26 + i * (day_size + 2)
lines.append(
f'<rect x="{lx}" y="{legend_y - 9}" width="{day_size}" height="{day_size}" rx="2" ry="2" fill="{color}" />'
)
lines.append(
f'<text x="{legend_x + 26 + len(levels) * (day_size + 2) + 4}" y="{legend_y}" fill="{palette["muted"]}" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" font-size="10">More</text>'
)
lines.append("</svg>")
return "".join(lines)