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(days=1) 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", ), 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" )