from datetime import timedelta 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 BOLUS_CATEGORY_SMB, classify_bolus, daily_stats, effective_basal_series, target_glucose_series from ui_common import ( MMOL_HOVERTEMPLATE, add_basal_area_trace, add_bolus_marker_traces, add_carb_bars_trace, add_target_line_trace, add_threshold_lines, get_colors, load_day_data, load_profile, 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 dag per keer: boven glucose met koolhydraten/bolus-overlay, onder basaal " "en SMB — apart gehouden omdat één gecombineerde grafiek te veel informatie bevatte." ) # De periode-knop bepaalt alleen het navigeerbare venster (hoe ver terug je kunt # bladeren), niet welke data wordt opgehaald — dat gebeurt hieronder voor precies # de geselecteerde dag (load_day_data), zodat het wisselen tussen dagen snel blijft. window_start, window_end, days = select_period_days(default_label="14 dagen") state_key = "dagoverzicht_selected_day" if state_key not in st.session_state: st.session_state[state_key] = window_end if not (window_start <= st.session_state[state_key] <= window_end): st.session_state[state_key] = window_end selected_day = st.session_state[state_key] col_prev, col_date, col_next = st.columns([1, 3, 1]) with col_prev: if st.button("← Vorige dag", disabled=selected_day <= window_start, width="stretch"): selected_day = selected_day - timedelta(days=1) st.session_state[state_key] = selected_day with col_next: if st.button("Volgende dag →", disabled=selected_day >= window_end, width="stretch"): selected_day = selected_day + timedelta(days=1) st.session_state[state_key] = selected_day with col_date: date_options = [window_end - timedelta(days=i) for i in range(days)] chosen = st.selectbox( "Datum", options=date_options, index=date_options.index(selected_day), format_func=lambda d: d.strftime("%A %d-%m-%Y"), label_visibility="collapsed", ) if chosen != selected_day: selected_day = chosen st.session_state[state_key] = selected_day try: glucose, treatments = load_day_data(selected_day) 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 op deze dag.") st.stop() render_completeness_warning(glucose, selected_day, selected_day) colors = get_colors() day_start = pd.Timestamp(selected_day) day_end = day_start + pd.Timedelta(1, unit="D") # treatments bevat een lookback-marge vóór middernacht (zie # nightscout_client.fetch_treatments_for_day) zodat een temp basal die al # vóór deze dag begon toch meetelt — effective_basal_series() clipt dat # zelf aan [day_start, day_end). Voor de koolhydraten/bolus-overlays willen # we die marge juist NIET, anders duiken gisteravond se stippen op vandaag op. day_treatments = treatments[(treatments["timestamp"] >= day_start) & (treatments["timestamp"] < day_end)] carbs_day = classify_treatments(day_treatments)["carbs"] bolus_day = classify_bolus(day_treatments) basal_day = effective_basal_series(profile, treatments, day_start, day_end) target_day = target_glucose_series(profile, day_start, day_end) def _close_step_to_day_end(df: pd.DataFrame, value_col: str) -> pd.DataFrame: """Voeg een laatste punt op day_end 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([{"timestamp": day_end, value_col: last_value}]) return pd.concat([df, closing_row], ignore_index=True) basal_day = _close_step_to_day_end(basal_day, "rate") target_day = _close_step_to_day_end(target_day, "target") def build_day_figure(glucose_day, carbs_day, bolus_day, basal_day, target_day, colors) -> go.Figure: fig = make_subplots( rows=2, cols=1, shared_xaxes=True, row_heights=[0.68, 0.32], vertical_spacing=0.08, specs=[[{"secondary_y": True}], [{"secondary_y": False}]], ) # Rij 1: glucose + streefwaarde + koolhydraten + voeding-/correctiebolus. fig.add_hrect( y0=TARGET_LOW, y1=TARGET_HIGH, fillcolor=colors["band"], line_width=0, layer="below", row=1, col=1 ) add_target_line_trace(fig, target_day, colors, row=1) add_carb_bars_trace(fig, carbs_day, colors, row=1, secondary_y=True) top_bolus = bolus_day[bolus_day["category"] != BOLUS_CATEGORY_SMB] if not bolus_day.empty else bolus_day lane_y = glucose_day["mmol"].min() - 1.0 if not glucose_day.empty else TARGET_LOW - 1.0 add_bolus_marker_traces(fig, top_bolus, colors, lane_y, row=1) fig.add_trace( go.Scatter( x=glucose_day["timestamp"], y=glucose_day["mmol"], mode="lines", line={"color": colors["glucose"], "width": 2}, name="Glucose", hovertemplate=MMOL_HOVERTEMPLATE, ), row=1, col=1, ) add_threshold_lines(fig, colors, row=1) fig.update_yaxes(title_text="mmol/L", gridcolor=colors["grid"], row=1, col=1, secondary_y=False) fig.update_yaxes(title_text="Koolhydraten (g)", showgrid=False, row=1, col=1, secondary_y=True) # Rij 2: basaal + SMB — apart van rij 1 zodat de glucosegrafiek niet # overladen raakt met insulinedetails. add_basal_area_trace(fig, basal_day, colors, row=2) smb_bolus = bolus_day[bolus_day["category"] == BOLUS_CATEGORY_SMB] if not bolus_day.empty else bolus_day smb_lane_y = (basal_day["rate"].max() * 1.15) if not basal_day.empty else 1.0 add_bolus_marker_traces(fig, smb_bolus, colors, smb_lane_y, row=2) fig.update_yaxes( title_text="Basaal (E/uur)", gridcolor=colors["grid"], rangemode="tozero", row=2, col=1 ) fig.update_xaxes(range=[day_start, day_end], dtick=4 * 3600 * 1000, 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=560, showlegend=True, legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "left", "x": 0}, margin={"l": 10, "r": 10, "t": 40, "b": 10}, ) return fig fig = build_day_figure(glucose, carbs_day, bolus_day, basal_day, target_day, colors) st.plotly_chart(fig, width="stretch") stats = daily_stats(glucose, day_treatments, profile, day_start, day_end) col1, col2, col3, col4 = st.columns(4) col1.metric( "Glucose (gem./min/max)", f"{stats['mean_mmol']:.1f} mmol/L", delta=f"{stats['min_mmol']:.1f} - {stats['max_mmol']:.1f}", delta_color="off", ) col2.metric("Totale insuline", f"{stats['total_insulin']:.1f} E") col3.metric("Basaal / bolus / SMB", f"{stats['basal_insulin']:.1f} / {stats['bolus_insulin']:.1f} / {stats['smb_insulin']:.1f} E") col4.metric( "Hypo's / hyper's", f"{stats['hypo_count']} / {stats['hyper_count']}", delta=f"{stats['hypo_duration_minutes']:.0f} / {stats['hyper_duration_minutes']:.0f} min", delta_color="off", ) st.caption( "Boven: stippen = bolusinsuline (voeding/correctie, grootte naar dosis) · staafjes = koolhydraten · " "streepstippellijn = streefwaarde uit pompprofiel. Onder: vlak = basaal · stippen = SMB." )