pd.Timedelta(days=1) triggers a DeprecationWarning about NumPy's "generic" timedelta unit in this pandas version; pd.Timedelta(1, unit="D") is equivalent and warning-free.
177 lines
6.3 KiB
Python
177 lines
6.3 KiB
Python
import pandas as pd
|
|
import plotly.graph_objects as go
|
|
import requests
|
|
import streamlit as st
|
|
from plotly.subplots import make_subplots
|
|
|
|
from auth import require_auth
|
|
from data_processing import TARGET_HIGH, TARGET_LOW, classify_treatments
|
|
from nightscout_client import NightscoutConfigError
|
|
from reporting import classify_bolus, effective_basal_series, target_glucose_series
|
|
from ui_common import (
|
|
add_basal_area_trace,
|
|
add_bolus_marker_traces,
|
|
add_carb_bars_trace,
|
|
add_target_line_trace,
|
|
add_threshold_lines,
|
|
get_colors,
|
|
load_profile,
|
|
load_range_data,
|
|
render_completeness_warning,
|
|
select_period_days,
|
|
)
|
|
|
|
st.set_page_config(page_title="Glucose dashboard — Dagoverzicht", page_icon="📊", layout="wide")
|
|
require_auth()
|
|
|
|
st.title("Dagoverzicht")
|
|
st.caption("Eén glucosegrafiek per kalenderdag, met basaal/bolus/koolhydraten-overlay (FR4).")
|
|
|
|
start, end, days = select_period_days(default_label="14 dagen")
|
|
if days > 31:
|
|
st.info(
|
|
"Bij langere periodes wordt de pagina lang en kan laden traag zijn — "
|
|
"voor het bekijken van individuele dagen is 14 of 30 dagen prettiger."
|
|
)
|
|
|
|
try:
|
|
glucose, treatments = load_range_data(start, end)
|
|
profile = load_profile()
|
|
except NightscoutConfigError as exc:
|
|
st.error(str(exc))
|
|
st.stop()
|
|
except (requests.RequestException, ValueError) as exc:
|
|
st.error(f"Kon geen data ophalen van Nightscout: {exc}")
|
|
st.stop()
|
|
|
|
if glucose.empty:
|
|
st.info("Geen glucosemetingen gevonden in deze periode.")
|
|
st.stop()
|
|
|
|
render_completeness_warning(glucose, start, end)
|
|
|
|
colors = get_colors()
|
|
glucose = glucose.copy()
|
|
glucose["date"] = glucose["timestamp"].dt.date
|
|
glucose["hour"] = glucose["timestamp"].dt.hour + glucose["timestamp"].dt.minute / 60
|
|
|
|
carbs_events = classify_treatments(treatments)["carbs"].copy()
|
|
if not carbs_events.empty:
|
|
carbs_events["date"] = carbs_events["timestamp"].dt.date
|
|
carbs_events["hour"] = carbs_events["timestamp"].dt.hour + carbs_events["timestamp"].dt.minute / 60
|
|
|
|
bolus_events = classify_bolus(treatments)
|
|
if not bolus_events.empty:
|
|
bolus_events["date"] = bolus_events["timestamp"].dt.date
|
|
bolus_events["hour"] = bolus_events["timestamp"].dt.hour + bolus_events["timestamp"].dt.minute / 60
|
|
|
|
|
|
def _close_step_to_day_end(df: pd.DataFrame, value_col: str) -> pd.DataFrame:
|
|
"""Voeg een laatste punt op hour=24 toe (zelfde waarde als het laatste
|
|
segment) zodat een stapvlak/-lijn de volle dag doortekent i.p.v. te
|
|
stoppen bij het laatste breekpunt."""
|
|
if df.empty:
|
|
return df
|
|
last_value = df.iloc[-1][value_col]
|
|
closing_row = pd.DataFrame([{"hour": 24.0, value_col: last_value}])
|
|
return pd.concat([df, closing_row], ignore_index=True)
|
|
|
|
|
|
dates = sorted(glucose["date"].unique())
|
|
row_height = 190
|
|
fig = make_subplots(
|
|
rows=len(dates),
|
|
cols=1,
|
|
shared_xaxes=True,
|
|
vertical_spacing=min(0.4 / max(len(dates) - 1, 1), 0.05),
|
|
specs=[[{"secondary_y": True}]] * len(dates),
|
|
)
|
|
|
|
for i, day in enumerate(dates, start=1):
|
|
day_df = glucose[glucose["date"] == day]
|
|
day_start = pd.Timestamp(day)
|
|
day_end = day_start + pd.Timedelta(1, unit="D")
|
|
|
|
fig.add_hrect(
|
|
y0=TARGET_LOW, y1=TARGET_HIGH, fillcolor=colors["band"], line_width=0, layer="below", row=i, col=1
|
|
)
|
|
# Handmatige titel-annotatie t.o.v. het domein van déze rij i.p.v.
|
|
# make_subplots' subplot_titles (die t.o.v. de hele figuur positioneert en
|
|
# daardoor bij veel dagen de gedeelde legenda bovenaan kan overlappen).
|
|
fig.add_annotation(
|
|
text=day.strftime("%A %d-%m-%Y"),
|
|
xref="x domain",
|
|
yref="y domain",
|
|
x=0.0,
|
|
y=1.15,
|
|
xanchor="left",
|
|
yanchor="bottom",
|
|
showarrow=False,
|
|
font={"size": 13, "color": colors["text"]},
|
|
row=i,
|
|
col=1,
|
|
)
|
|
|
|
basal_day = effective_basal_series(profile, treatments, day_start, day_end)
|
|
if not basal_day.empty:
|
|
basal_day["hour"] = (basal_day["timestamp"] - day_start).dt.total_seconds() / 3600
|
|
basal_day = _close_step_to_day_end(basal_day, "rate")
|
|
add_basal_area_trace(fig, basal_day, colors, row=i, secondary_y=True, x_col="hour")
|
|
|
|
target_day = target_glucose_series(profile, day_start, day_end)
|
|
if not target_day.empty:
|
|
target_day["hour"] = (target_day["timestamp"] - day_start).dt.total_seconds() / 3600
|
|
target_day = _close_step_to_day_end(target_day, "target")
|
|
add_target_line_trace(fig, target_day, colors, row=i, x_col="hour")
|
|
|
|
add_carb_bars_trace(fig, carbs_events[carbs_events["date"] == day], colors, row=i, secondary_y=True, x_col="hour")
|
|
|
|
lane_y = day_df["mmol"].min() - 1.0 if not day_df.empty else TARGET_LOW - 1.0
|
|
add_bolus_marker_traces(fig, bolus_events[bolus_events["date"] == day], colors, lane_y, row=i, x_col="hour")
|
|
|
|
fig.add_trace(
|
|
go.Scatter(
|
|
x=day_df["hour"],
|
|
y=day_df["mmol"],
|
|
mode="lines",
|
|
line={"color": colors["glucose"], "width": 2},
|
|
name="Glucose",
|
|
hovertemplate="%{y:.1f} mmol/L<extra></extra>",
|
|
),
|
|
row=i,
|
|
col=1,
|
|
secondary_y=False,
|
|
)
|
|
fig.update_yaxes(title_text="mmol/L", gridcolor=colors["grid"], row=i, col=1, secondary_y=False)
|
|
fig.update_yaxes(
|
|
title_text="Basaal (E/uur) · KH (g)", showgrid=False, row=i, col=1, secondary_y=True
|
|
)
|
|
add_threshold_lines(fig, colors, row=i)
|
|
|
|
# Elke dag herhaalt dezelfde spoornamen (Glucose/Basaal/Koolhydraten/...) — zonder
|
|
# deduplicatie zou de legenda 7x "Glucose" etc. tonen. Alleen de eerste keer per
|
|
# naam blijft in de legenda staan.
|
|
seen_names = set()
|
|
for trace in fig.data:
|
|
if trace.name in seen_names:
|
|
trace.showlegend = False
|
|
else:
|
|
seen_names.add(trace.name)
|
|
|
|
fig.update_xaxes(title_text="Uur van de dag", range=[0, 24], dtick=4, gridcolor=colors["grid"])
|
|
fig.update_layout(
|
|
paper_bgcolor="rgba(0,0,0,0)",
|
|
plot_bgcolor="rgba(0,0,0,0)",
|
|
font_color=colors["text"],
|
|
height=row_height * len(dates) + 40,
|
|
showlegend=True,
|
|
legend={"orientation": "h", "yanchor": "bottom", "y": 1.0, "xanchor": "left", "x": 0},
|
|
margin={"l": 10, "r": 10, "t": 40, "b": 10},
|
|
)
|
|
st.plotly_chart(fig, width="stretch")
|
|
st.caption(
|
|
f"{len(dates)} dagen · {len(glucose)} metingen · stippen = bolusinsuline "
|
|
"(voeding/correctie/SMB, grootte naar dosis) · vlak = basaal · staafjes = koolhydraten · "
|
|
"streepstippellijn = streefwaarde uit pompprofiel"
|
|
)
|