"""Chart.js-ähnliche Payloads → PNG (Matplotlib). Von PDF- und Bundle-Rendering gemeinsam genutzt.""" from __future__ import annotations import io from typing import Any import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt def _color_to_rgb(hex_or_rgba: str) -> tuple[float, float, float]: s = (hex_or_rgba or "#333333").strip() if s.startswith("#") and len(s) >= 7: try: r = int(s[1:3], 16) / 255.0 g = int(s[3:5], 16) / 255.0 b = int(s[5:7], 16) / 255.0 return (r, g, b) except ValueError: pass return (0.12, 0.62, 0.46) def chart_payload_to_png(payload: dict[str, Any], fig_width_in: float = 6.2, fig_height_in: float = 3.4) -> bytes: chart_type = payload.get("chart_type") or "line" data = payload.get("data") or {} labels = data.get("labels") or [] datasets = data.get("datasets") or [] fig, ax = plt.subplots(figsize=(fig_width_in, fig_height_in), dpi=120) ax.set_facecolor("#fafaf9") fig.patch.set_facecolor("#ffffff") if chart_type == "pie" and datasets: ds0 = datasets[0] values = ds0.get("data") or [] colors = ds0.get("backgroundColor") or ["#1D9E75", "#378ADD", "#D85A30"] if labels and values and len(labels) == len(values): ax.pie(values, labels=labels, autopct="%1.0f%%", colors=colors[: len(values)], startangle=90) ax.axis("equal") else: ax.text(0.5, 0.5, "Keine Daten", ha="center", va="center", transform=ax.transAxes) elif chart_type in ("line", "bar", "scatter") and datasets: x = range(len(labels)) if labels else [] for i, ds in enumerate(datasets): y = ds.get("data") or [] if not y: continue lab = ds.get("label") or f"Serie {i + 1}" col = _color_to_rgb(str(ds.get("borderColor") or ds.get("backgroundColor") or "#1D9E75")) if chart_type == "bar": yv = y[: len(labels)] if labels else y bg = ds.get("backgroundColor") if isinstance(bg, list): cols = [_color_to_rgb(str(c)) for c in bg[: len(yv)]] else: cols = [_color_to_rgb(str(bg or "#1D9E75"))] * len(yv) ax.bar(list(range(len(yv))), yv, label=lab, color=cols[: len(yv)], alpha=0.88) else: ax.plot( list(x)[: len(y)], y, label=lab, color=col, linewidth=1.6, marker="o", markersize=2, ) if labels and chart_type != "bar": step = max(1, len(labels) // 8) ax.set_xticks(list(x)[::step]) ax.set_xticklabels([labels[j] for j in range(0, len(labels), step)], rotation=25, fontsize=7) elif labels and chart_type == "bar": ax.set_xticks(list(x)) ax.set_xticklabels(labels, rotation=30, fontsize=7) ax.legend(loc="upper right", fontsize=7) ax.grid(True, alpha=0.25) ax.set_xmargin(0.02) else: ax.text(0.5, 0.5, "Diagrammtyp nicht unterstützt oder leer", ha="center", va="center", transform=ax.transAxes) fig.tight_layout() buf = io.BytesIO() fig.savefig(buf, format="png", bbox_inches="tight", facecolor=fig.get_facecolor()) plt.close(fig) buf.seek(0) return buf.read()