first commit
This commit is contained in:
125
app/render/svg.py
Normal file
125
app/render/svg.py
Normal file
@@ -0,0 +1,125 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user