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:
@@ -11,6 +11,7 @@ from reporting import (
|
||||
classify_bolus,
|
||||
completeness_warning,
|
||||
daily_gmi,
|
||||
daily_stats,
|
||||
day_of_week_pattern,
|
||||
detect_events,
|
||||
effective_basal_series,
|
||||
@@ -618,6 +619,75 @@ def test_target_glucose_series_handles_none_profile():
|
||||
assert result.empty
|
||||
|
||||
|
||||
def test_daily_stats_computes_glucose_and_insulin_breakdown():
|
||||
day_start = pd.Timestamp("2026-01-01")
|
||||
day_end = pd.Timestamp("2026-01-02")
|
||||
glucose = _series(
|
||||
[("2026-01-01 08:00", 3.2), ("2026-01-01 12:00", 14.1), ("2026-01-01 20:00", 6.5)]
|
||||
)
|
||||
treatments = pd.DataFrame(
|
||||
[
|
||||
{
|
||||
"timestamp": pd.Timestamp("2026-01-01 08:00"),
|
||||
"eventType": "Meal Bolus",
|
||||
"insulin": 4.0,
|
||||
"carbs": 40.0,
|
||||
"type": "",
|
||||
"absolute": None,
|
||||
"duration": None,
|
||||
},
|
||||
{
|
||||
"timestamp": pd.Timestamp("2026-01-01 12:00"),
|
||||
"eventType": "Correction Bolus",
|
||||
"insulin": 0.3,
|
||||
"carbs": None,
|
||||
"type": "SMB",
|
||||
"absolute": None,
|
||||
"duration": None,
|
||||
},
|
||||
{
|
||||
"timestamp": pd.Timestamp("2026-01-01 06:00"),
|
||||
"eventType": "Temp Basal",
|
||||
"insulin": None,
|
||||
"carbs": None,
|
||||
"type": "",
|
||||
"absolute": 2.0,
|
||||
"duration": 60,
|
||||
},
|
||||
]
|
||||
)
|
||||
profile = {"basal": [{"minutes": 0, "value": 0.8}]}
|
||||
|
||||
result = daily_stats(glucose, treatments, profile, day_start, day_end)
|
||||
|
||||
assert result["mean_mmol"] == pytest.approx((3.2 + 14.1 + 6.5) / 3, abs=0.05)
|
||||
assert result["min_mmol"] == pytest.approx(3.2)
|
||||
assert result["max_mmol"] == pytest.approx(14.1)
|
||||
assert result["bolus_insulin"] == pytest.approx(4.0)
|
||||
assert result["smb_insulin"] == pytest.approx(0.3)
|
||||
# basaal: 23h @ 0.8 profieltempo + 1h @ 2.0 (temp basal 06:00-07:00) = 18.4 + 2.0 = 20.4
|
||||
assert result["basal_insulin"] == pytest.approx(20.4, abs=0.05)
|
||||
assert result["total_insulin"] == pytest.approx(4.0 + 0.3 + 20.4, abs=0.05)
|
||||
assert result["hypo_count"] == 1
|
||||
assert result["hyper_count"] == 1
|
||||
|
||||
|
||||
def test_daily_stats_handles_empty_glucose_and_treatments():
|
||||
day_start = pd.Timestamp("2026-01-01")
|
||||
day_end = pd.Timestamp("2026-01-02")
|
||||
empty_glucose = pd.DataFrame(columns=["timestamp", "mmol"])
|
||||
empty_treatments = pd.DataFrame(columns=["timestamp", "eventType", "insulin", "carbs", "type", "absolute", "duration"])
|
||||
|
||||
result = daily_stats(empty_glucose, empty_treatments, None, day_start, day_end)
|
||||
|
||||
assert result["mean_mmol"] == 0.0
|
||||
assert result["min_mmol"] == 0.0
|
||||
assert result["max_mmol"] == 0.0
|
||||
assert result["total_insulin"] == 0.0
|
||||
assert result["hypo_count"] == 0
|
||||
assert result["hyper_count"] == 0
|
||||
|
||||
|
||||
def test_site_interval_stats_splits_by_site_change():
|
||||
glucose = _series(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user