"""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 '

Keine Daten im Zeitraum.

' 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''] # horizontale Gitterlinien + Y-Beschriftung for k in range(5): v = step * k yy = y(v) parts.append(f'') parts.append(f'{v}') # 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'{escape(m["month"])}') def polyline(key, color): pts = " ".join(f"{x(i):.1f},{y(m[key]):.1f}" for i, m in enumerate(monthly)) out = [f''] for i, m in enumerate(monthly): out.append(f'') return "".join(out) parts.append(polyline("created", C_CREATED)) parts.append(polyline("closed", C_CLOSED)) # Legende parts.append(f'' f'' f'erstellt' f'' f'geschlossen' f'') parts.append("") return "".join(parts)