import plotly.graph_objects as go import requests import streamlit as st from auth import require_auth from data_processing import ( TARGET_HIGH, TARGET_LOW, classify_treatments, summarize_time_in_range, ) from nightscout_client import NightscoutConfigError, fetch_entries, fetch_treatments from ui_common import MMOL_HOVERTEMPLATE, get_colors st.set_page_config(page_title="Glucose dashboard — Live", page_icon="📊", layout="wide") require_auth() st.title("Glucose dashboard") HOURS_OPTIONS = {"6 uur": 6, "12 uur": 12, "24 uur": 24, "48 uur": 48} @st.cache_data(ttl=60, show_spinner="Data ophalen van Nightscout...") def load_nightscout_data(hours: int): glucose = fetch_entries(hours=hours) treatments = fetch_treatments(hours=hours) return glucose, treatments def build_chart(glucose, insulin, carbs, site_changes, sensor_changes, colors) -> go.Figure: glucose_min = min(glucose["mmol"].min(), TARGET_LOW) glucose_max = max(glucose["mmol"].max(), TARGET_HIGH) y_top = glucose_max + 1.0 lane_base = glucose_min - 2.5 y_bottom = lane_base - 0.5 insulin_lane_y = lane_base + 1.5 carbs_lane_y = lane_base + 0.7 fig = go.Figure() # Streefbereik als achtergrondband, geen losse legenda-entry nodig. fig.add_hrect( y0=TARGET_LOW, y1=TARGET_HIGH, fillcolor=colors["band"], line_width=0, layer="below", ) fig.add_trace( go.Scatter( x=glucose["timestamp"], y=glucose["mmol"], mode="lines", name="Glucose", line={"color": colors["glucose"], "width": 2, "shape": "spline"}, hovertemplate=MMOL_HOVERTEMPLATE, ) ) low_points = glucose[glucose["mmol"] < TARGET_LOW] high_points = glucose[glucose["mmol"] > TARGET_HIGH] if not low_points.empty: fig.add_trace( go.Scatter( x=low_points["timestamp"], y=low_points["mmol"], mode="markers", name=f"Laag (<{TARGET_LOW})", marker={"color": colors["low"], "size": 6}, hovertemplate=MMOL_HOVERTEMPLATE, ) ) if not high_points.empty: fig.add_trace( go.Scatter( x=high_points["timestamp"], y=high_points["mmol"], mode="markers", name=f"Hoog (>{TARGET_HIGH})", marker={"color": colors["high"], "size": 6}, hovertemplate=MMOL_HOVERTEMPLATE, ) ) if not insulin.empty: fig.add_trace( go.Scatter( x=insulin["timestamp"], y=[insulin_lane_y] * len(insulin), mode="markers", name="Insuline", marker={"color": colors["insulin"], "size": 10, "symbol": "triangle-up"}, customdata=insulin["insulin"], hovertemplate="%{x|%d-%m %H:%M} · %{customdata:.1f} E", ) ) if not carbs.empty: fig.add_trace( go.Scatter( x=carbs["timestamp"], y=[carbs_lane_y] * len(carbs), mode="markers", name="Koolhydraten", marker={"color": colors["carbs"], "size": 10, "symbol": "circle"}, customdata=carbs["carbs"], hovertemplate="%{x|%d-%m %H:%M} · %{customdata:.0f} g", ) ) # Let op: annotation_text/-position via add_vline() zelf triggert een bug # in plotly's shapeannotation-helper met pandas Timestamp-x-waarden, dus # de lijn en het label worden hier los toegevoegd. for _, row in site_changes.iterrows(): fig.add_vline(x=row["timestamp"], line_dash="dot", line_color=colors["muted"], opacity=0.6) fig.add_annotation( x=row["timestamp"], y=y_top, yanchor="bottom", text="Pomp", showarrow=False, font={"color": colors["muted"], "size": 10}, ) for _, row in sensor_changes.iterrows(): fig.add_vline(x=row["timestamp"], line_dash="dash", line_color=colors["muted"], opacity=0.6) fig.add_annotation( x=row["timestamp"], y=y_top, yanchor="bottom", text="Sensor", showarrow=False, font={"color": colors["muted"], "size": 10}, ) fig.update_layout( paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font_color=colors["text"], legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "left", "x": 0}, margin={"l": 10, "r": 10, "t": 10, "b": 10}, height=480, yaxis={ "title": "mmol/L", "range": [y_bottom, y_top], "gridcolor": colors["grid"], "zeroline": False, }, xaxis={"gridcolor": colors["grid"]}, ) return fig with st.sidebar: st.header("Instellingen") hours_label = st.radio("Periode", list(HOURS_OPTIONS.keys()), index=2) hours = HOURS_OPTIONS[hours_label] if st.button("Vernieuwen"): st.cache_data.clear() try: glucose, treatments = load_nightscout_data(hours) except NightscoutConfigError as exc: st.error(str(exc)) st.stop() except (requests.RequestException, ValueError) as exc: # netwerk-/API-fouten van Nightscout 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() events = classify_treatments(treatments) tir = summarize_time_in_range(glucose) latest = glucose.iloc[-1] previous = glucose.iloc[-2] if len(glucose) > 1 else None delta = round(latest["mmol"] - previous["mmol"], 1) if previous is not None else None col1, col2, col3, col4 = st.columns(4) col1.metric("Actuele glucose", f"{latest['mmol']:.1f} mmol/L", delta=delta, delta_color="off") col2.metric("Tijd in bereik", f"{tir['in_range']:.0f}%") col3.metric("Tijd laag", f"{tir['low']:.0f}%") col4.metric("Tijd hoog", f"{tir['high']:.0f}%") st.caption( f"Laatste meting: {latest['timestamp'].strftime('%d-%m-%Y %H:%M')} · " f"streefbereik {TARGET_LOW}-{TARGET_HIGH} mmol/L" ) colors = get_colors() fig = build_chart( glucose, events["insulin"], events["carbs"], events["site_changes"], events["sensor_changes"], colors, ) st.plotly_chart(fig, width="stretch") st.caption( "Driehoekjes = insuline · cirkels = koolhydraten · stippellijn = pompwissel · " "streepjeslijn = sensorwissel" )