mitai-jinkendo/backend/report_chart_plotting.py
Lars 3ab5dae130
All checks were successful
Deploy Development / deploy (push) Successful in 1m1s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 20s
feat: add viz_bundle support to report generation and enhance schema
- Introduced the `viz_bundle` block type to the report profile schema, allowing for the inclusion of bundled visualizations in PDF reports.
- Updated the `build_structured_report_pdf` function to handle `VizBundleBlock` and append its content to the report.
- Enhanced the report catalog API to include details for the new `viz_bundle` block type.
- Added configuration editors for various visualization bundles in the frontend settings page.
- Updated tests to validate the new `viz_bundle` functionality and ensure proper handling of report profiles.
- Bumped application version to reflect these enhancements.
2026-04-29 11:46:34 +02:00

92 lines
3.4 KiB
Python

"""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()