added getting data from nightscout
This commit is contained in:
@@ -1,34 +1,241 @@
|
||||
from datetime import date
|
||||
|
||||
import plotly.graph_objects as go
|
||||
import requests
|
||||
import streamlit as st
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from data_processing import filter_by_date, hourly_average, load_data
|
||||
from data_processing import (
|
||||
TARGET_HIGH,
|
||||
TARGET_LOW,
|
||||
classify_treatments,
|
||||
summarize_time_in_range,
|
||||
)
|
||||
from nightscout_client import NightscoutConfigError, fetch_entries, fetch_treatments
|
||||
|
||||
st.set_page_config(page_title="Glucose dashboard", page_icon="📊", layout="centered")
|
||||
load_dotenv() # laadt .env als die bestaat; bestaande env vars blijven leidend
|
||||
|
||||
st.title("Glucose per uur")
|
||||
st.set_page_config(page_title="Glucose dashboard", page_icon="📊", layout="wide")
|
||||
|
||||
df = load_data()
|
||||
st.title("Glucose dashboard")
|
||||
|
||||
min_date = df["timestamp"].dt.date.min() if not df.empty else date.today()
|
||||
max_date = df["timestamp"].dt.date.max() if not df.empty else date.today()
|
||||
HOURS_OPTIONS = {"6 uur": 6, "12 uur": 12, "24 uur": 24, "48 uur": 48}
|
||||
|
||||
selected_date = st.date_input(
|
||||
"Kies een datum",
|
||||
value=max_date,
|
||||
min_value=min_date,
|
||||
max_value=max_date,
|
||||
# Kleuren per rol, licht/donker — zie de dataviz-conventies in dit project:
|
||||
# categorale kleuren voor identiteit (glucose/insuline/carbs), status-kleuren
|
||||
# (vast, nooit hergebruikt) voor te laag/te hoog.
|
||||
PALETTE = {
|
||||
"light": {
|
||||
"glucose": "#2a78d6",
|
||||
"insulin": "#1baf7a",
|
||||
"carbs": "#eb6834",
|
||||
"low": "#d03b3b",
|
||||
"high": "#fab219",
|
||||
"band": "rgba(12,163,12,0.08)",
|
||||
"grid": "#e1e0d9",
|
||||
"muted": "#898781",
|
||||
"text": "#52514e",
|
||||
},
|
||||
"dark": {
|
||||
"glucose": "#3987e5",
|
||||
"insulin": "#199e70",
|
||||
"carbs": "#d95926",
|
||||
"low": "#d03b3b",
|
||||
"high": "#fab219",
|
||||
"band": "rgba(12,163,12,0.15)",
|
||||
"grid": "#2c2c2a",
|
||||
"muted": "#898781",
|
||||
"text": "#c3c2b7",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
MMOL_HOVERTEMPLATE = "%{x|%d-%m %H:%M} · %{y:.1f} mmol/L<extra></extra>"
|
||||
|
||||
|
||||
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<extra></extra>",
|
||||
)
|
||||
)
|
||||
|
||||
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<extra></extra>",
|
||||
)
|
||||
)
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
day_data = filter_by_date(df, selected_date)
|
||||
theme_type = "light"
|
||||
try:
|
||||
theme_type = st.context.theme.type or "light"
|
||||
except AttributeError:
|
||||
pass
|
||||
colors = PALETTE["dark" if theme_type == "dark" else "light"]
|
||||
|
||||
if day_data.empty:
|
||||
st.info("Geen metingen gevonden voor deze datum.")
|
||||
else:
|
||||
hourly = hourly_average(day_data)
|
||||
st.bar_chart(
|
||||
hourly.set_index("hour")["glucose_value"],
|
||||
x_label="Uur van de dag",
|
||||
y_label="Gemiddelde glucosewaarde",
|
||||
)
|
||||
st.caption(f"{len(day_data)} metingen op {selected_date.strftime('%d-%m-%Y')}")
|
||||
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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user