Files
EpiSupport-Zammad-Scraper/app/charts.py
T
2026-06-29 09:52:02 +02:00

73 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)