first commit

This commit is contained in:
2026-06-29 09:52:02 +02:00
commit da2f7c3c86
31 changed files with 2306 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
"""Server-seitige SVG-Diagramme ohne JavaScript/CDN, voll offline-fähig."""
from html import escape
C_CREATED = "#0b6e4f"
C_CLOSED = "#c0392b"
C_GRID = "#e1e7ec"
C_AXIS = "#7b8794"
def monthly_chart(monthly, width=760, height=260):
"""Liniendiagramm: erstellte vs. geschlossene Tickets pro Monat."""
if not monthly:
return '<p class="muted">Keine Daten im Zeitraum.</p>'
pad_l, pad_r, pad_t, pad_b = 38, 12, 14, 42
plot_w = width - pad_l - pad_r
plot_h = height - pad_t - pad_b
n = len(monthly)
max_v = max(max(m["created"], m["closed"]) for m in monthly) or 1
# auf "schöne" Obergrenze runden
step = max(1, -(-max_v // 4)) # ceil
y_max = step * 4
def x(i):
return pad_l + (plot_w * (i / (n - 1)) if n > 1 else plot_w / 2)
def y(v):
return pad_t + plot_h - (v / y_max) * plot_h
parts = [f'<svg viewBox="0 0 {width} {height}" class="chart" role="img" '
f'aria-label="Tickets pro Monat" preserveAspectRatio="xMidYMid meet">']
# horizontale Gitterlinien + Y-Beschriftung
for k in range(5):
v = step * k
yy = y(v)
parts.append(f'<line x1="{pad_l}" y1="{yy:.1f}" x2="{width - pad_r}" '
f'y2="{yy:.1f}" stroke="{C_GRID}" stroke-width="1"/>')
parts.append(f'<text x="{pad_l - 6}" y="{yy + 3:.1f}" text-anchor="end" '
f'font-size="10" fill="{C_AXIS}">{v}</text>')
# X-Beschriftung (höchstens ~12 Labels)
label_every = max(1, n // 12)
for i, m in enumerate(monthly):
if i % label_every == 0 or i == n - 1:
parts.append(f'<text x="{x(i):.1f}" y="{height - pad_b + 16}" '
f'text-anchor="middle" font-size="9" fill="{C_AXIS}" '
f'transform="rotate(0)">{escape(m["month"])}</text>')
def polyline(key, color):
pts = " ".join(f"{x(i):.1f},{y(m[key]):.1f}" for i, m in enumerate(monthly))
out = [f'<polyline points="{pts}" fill="none" stroke="{color}" '
f'stroke-width="2" stroke-linejoin="round"/>']
for i, m in enumerate(monthly):
out.append(f'<circle cx="{x(i):.1f}" cy="{y(m[key]):.1f}" r="2.5" '
f'fill="{color}"/>')
return "".join(out)
parts.append(polyline("created", C_CREATED))
parts.append(polyline("closed", C_CLOSED))
# Legende
parts.append(f'<g font-size="11">'
f'<rect x="{pad_l}" y="{height - 14}" width="10" height="10" fill="{C_CREATED}"/>'
f'<text x="{pad_l + 14}" y="{height - 5}" fill="{C_AXIS}">erstellt</text>'
f'<rect x="{pad_l + 80}" y="{height - 14}" width="10" height="10" fill="{C_CLOSED}"/>'
f'<text x="{pad_l + 94}" y="{height - 5}" fill="{C_AXIS}">geschlossen</text>'
f'</g>')
parts.append("</svg>")
return "".join(parts)