first commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user