Redesign the PDF report: landscape pages, headers/footers, icon, daily stats
Every report section now gets its own landscape A4 page (or, for Dagoverzicht, one page per day) via a GlucoseReportPDF(FPDF) subclass with a real header (section title, report version, insulin/pump/sensor type from new REPORT_* env vars) and footer (page number, generation timestamp, Nightscout link) on every page, plus an original SVG icon in the header — inspired by the LibreView AGP report. New "Insulineprofiel" PDF section mirrors the dashboard page (basal bar chart, ISF/carb-ratio segments, DIA). Each Dagoverzicht day now draws the same basal/bolus/carb/target overlays as the interactive charts (matplotlib port of the Plotly helpers from #30) plus a new per-day stats block (reporting.daily_stats: avg/min/max glucose, total/basal/bolus/SMB insulin, hypo/hyper counts and durations). Builds on #29 (profile data) and #30 (overlay logic/colors).
This commit is contained in:
@@ -601,6 +601,73 @@ def target_glucose_series(profile: dict | None, day_start, day_end) -> pd.DataFr
|
||||
return pd.DataFrame(rows, columns=["timestamp", "target"])
|
||||
|
||||
|
||||
def _integrate_step_series(series: pd.DataFrame, value_col: str, day_end) -> float:
|
||||
"""Oppervlakte onder een stapfunctie-DataFrame (zoals effective_basal_series()):
|
||||
som van elke rij se waarde x duur (in uur) tot de volgende rij, of tot
|
||||
`day_end` voor de laatste rij. Gebruikt om een E/uur-stapfunctie om te
|
||||
rekenen naar een totaal aantal eenheden."""
|
||||
if series.empty:
|
||||
return 0.0
|
||||
rows = series.reset_index(drop=True)
|
||||
total = 0.0
|
||||
for i in range(len(rows)):
|
||||
start = rows.loc[i, "timestamp"]
|
||||
end = rows.loc[i + 1, "timestamp"] if i + 1 < len(rows) else day_end
|
||||
total += rows.loc[i, value_col] * (end - start).total_seconds() / 3600
|
||||
return total
|
||||
|
||||
|
||||
DAILY_STATS_KEYS = [
|
||||
"mean_mmol",
|
||||
"min_mmol",
|
||||
"max_mmol",
|
||||
"total_insulin",
|
||||
"basal_insulin",
|
||||
"bolus_insulin",
|
||||
"smb_insulin",
|
||||
"hypo_count",
|
||||
"hypo_duration_minutes",
|
||||
"hyper_count",
|
||||
"hyper_duration_minutes",
|
||||
]
|
||||
|
||||
|
||||
def daily_stats(day_glucose: pd.DataFrame, day_treatments: pd.DataFrame, profile: dict | None, day_start, day_end) -> dict:
|
||||
"""Samenvattende statistieken voor één kalenderdag, voor naast de dagelijkse
|
||||
grafiek in het PDF-rapport: gemiddelde/min/max glucose, totale/basaal-/bolus-/
|
||||
SMB-insuline (SMB apart gerapporteerd omdat dit zowel als basaal- als
|
||||
bolus-vervanging kan gelden, zie classify_bolus()), en het aantal + de totale
|
||||
duur van hypo- en hyperevents die dag.
|
||||
"""
|
||||
min_mmol, max_mmol = glucose_min_max(day_glucose)
|
||||
mean_mmol = round(day_glucose["mmol"].mean(), 1) if not day_glucose.empty else 0.0
|
||||
|
||||
bolus = classify_bolus(day_treatments)
|
||||
by_category = bolus.groupby("category")["insulin"].sum().to_dict() if not bolus.empty else {}
|
||||
bolus_units = by_category.get(BOLUS_CATEGORY_FOOD, 0.0) + by_category.get(BOLUS_CATEGORY_CORRECTION, 0.0)
|
||||
smb_units = by_category.get(BOLUS_CATEGORY_SMB, 0.0)
|
||||
|
||||
basal_series = effective_basal_series(profile, day_treatments, day_start, day_end)
|
||||
basal_units = _integrate_step_series(basal_series, "rate", day_end)
|
||||
|
||||
hypo_summary = summarize_events(detect_events(day_glucose, LOW, "below"), period_days=1)
|
||||
hyper_summary = summarize_events(detect_events(day_glucose, HIGH, "above"), period_days=1)
|
||||
|
||||
return {
|
||||
"mean_mmol": mean_mmol,
|
||||
"min_mmol": min_mmol,
|
||||
"max_mmol": max_mmol,
|
||||
"total_insulin": round(bolus_units + smb_units + basal_units, 1),
|
||||
"basal_insulin": round(basal_units, 1),
|
||||
"bolus_insulin": round(bolus_units, 1),
|
||||
"smb_insulin": round(smb_units, 1),
|
||||
"hypo_count": hypo_summary["count"],
|
||||
"hypo_duration_minutes": hypo_summary["total_minutes"],
|
||||
"hyper_count": hyper_summary["count"],
|
||||
"hyper_duration_minutes": hyper_summary["total_minutes"],
|
||||
}
|
||||
|
||||
|
||||
def basal_stats(treatments: pd.DataFrame, period_days: float) -> dict:
|
||||
"""Totale basaalinsuline uit Temp Basal-treatments (rate x duur), zonder pompprofiel.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user