@@ -9,7 +9,13 @@ from reporting import (
|
|||||||
sensor_active_percent,
|
sensor_active_percent,
|
||||||
tir_breakdown_5band,
|
tir_breakdown_5band,
|
||||||
)
|
)
|
||||||
from ui_common import load_range_data, render_completeness_warning, select_period_days
|
from ui_common import (
|
||||||
|
build_tir_stacked_bar_figure,
|
||||||
|
get_colors,
|
||||||
|
load_range_data,
|
||||||
|
render_completeness_warning,
|
||||||
|
select_period_days,
|
||||||
|
)
|
||||||
|
|
||||||
st.set_page_config(page_title="Glucose dashboard — Samenvatting", page_icon="📊", layout="wide")
|
st.set_page_config(page_title="Glucose dashboard — Samenvatting", page_icon="📊", layout="wide")
|
||||||
require_auth()
|
require_auth()
|
||||||
@@ -66,6 +72,8 @@ cols = st.columns(5)
|
|||||||
for col, band in zip(cols, band_labels, strict=True):
|
for col, band in zip(cols, band_labels, strict=True):
|
||||||
col.metric(band_labels[band], f"{tir[band]:.0f}%", help=f"Streefwaarde: {band_targets[band]}")
|
col.metric(band_labels[band], f"{tir[band]:.0f}%", help=f"Streefwaarde: {band_targets[band]}")
|
||||||
|
|
||||||
|
st.plotly_chart(build_tir_stacked_bar_figure(tir, get_colors()), width="stretch")
|
||||||
|
|
||||||
st.caption(
|
st.caption(
|
||||||
f"Periode: {start.strftime('%d-%m-%Y')} t/m {end.strftime('%d-%m-%Y')} "
|
f"Periode: {start.strftime('%d-%m-%Y')} t/m {end.strftime('%d-%m-%Y')} "
|
||||||
f"({len(glucose)} metingen) · streefwaarden volgens Battelino et al. 2019"
|
f"({len(glucose)} metingen) · streefwaarden volgens Battelino et al. 2019"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from data_processing import TARGET_HIGH, TARGET_LOW
|
|||||||
from nightscout_client import NightscoutConfigError
|
from nightscout_client import NightscoutConfigError
|
||||||
from reporting import agp_percentile_bands
|
from reporting import agp_percentile_bands
|
||||||
from ui_common import (
|
from ui_common import (
|
||||||
|
add_threshold_lines,
|
||||||
get_colors,
|
get_colors,
|
||||||
hex_to_rgba,
|
hex_to_rgba,
|
||||||
load_range_data,
|
load_range_data,
|
||||||
@@ -102,6 +103,7 @@ fig.update_layout(
|
|||||||
yaxis={"title": "mmol/L", "gridcolor": colors["grid"], "zeroline": False},
|
yaxis={"title": "mmol/L", "gridcolor": colors["grid"], "zeroline": False},
|
||||||
xaxis={"title": "Uur van de dag", "gridcolor": colors["grid"], "tickmode": "linear", "dtick": 2},
|
xaxis={"title": "Uur van de dag", "gridcolor": colors["grid"], "tickmode": "linear", "dtick": 2},
|
||||||
)
|
)
|
||||||
|
add_threshold_lines(fig, colors)
|
||||||
st.plotly_chart(fig, width="stretch")
|
st.plotly_chart(fig, width="stretch")
|
||||||
st.caption(
|
st.caption(
|
||||||
f"Periode: {start.strftime('%d-%m-%Y')} t/m {end.strftime('%d-%m-%Y')} ({len(glucose)} metingen)"
|
f"Periode: {start.strftime('%d-%m-%Y')} t/m {end.strftime('%d-%m-%Y')} ({len(glucose)} metingen)"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from auth import require_auth
|
|||||||
from data_processing import TARGET_HIGH, TARGET_LOW
|
from data_processing import TARGET_HIGH, TARGET_LOW
|
||||||
from nightscout_client import NightscoutConfigError
|
from nightscout_client import NightscoutConfigError
|
||||||
from ui_common import (
|
from ui_common import (
|
||||||
|
add_threshold_lines,
|
||||||
get_colors,
|
get_colors,
|
||||||
load_range_data,
|
load_range_data,
|
||||||
render_completeness_warning,
|
render_completeness_warning,
|
||||||
@@ -74,6 +75,7 @@ for i, day in enumerate(dates, start=1):
|
|||||||
col=1,
|
col=1,
|
||||||
)
|
)
|
||||||
fig.update_yaxes(title_text="mmol/L", gridcolor=colors["grid"], row=i, col=1)
|
fig.update_yaxes(title_text="mmol/L", gridcolor=colors["grid"], row=i, col=1)
|
||||||
|
add_threshold_lines(fig, colors, row=i)
|
||||||
|
|
||||||
fig.update_xaxes(title_text="Uur van de dag", range=[0, 24], dtick=4, gridcolor=colors["grid"])
|
fig.update_xaxes(title_text="Uur van de dag", range=[0, 24], dtick=4, gridcolor=colors["grid"])
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
|
|||||||
+30
-1
@@ -1,4 +1,12 @@
|
|||||||
from ui_common import hex_to_rgba
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
from ui_common import (
|
||||||
|
PALETTE,
|
||||||
|
THRESHOLD_LINES,
|
||||||
|
add_threshold_lines,
|
||||||
|
build_tir_stacked_bar_figure,
|
||||||
|
hex_to_rgba,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_hex_to_rgba_converts_correctly():
|
def test_hex_to_rgba_converts_correctly():
|
||||||
@@ -7,3 +15,24 @@ def test_hex_to_rgba_converts_correctly():
|
|||||||
|
|
||||||
def test_hex_to_rgba_handles_hash_optional():
|
def test_hex_to_rgba_handles_hash_optional():
|
||||||
assert hex_to_rgba("2a78d6", 0.3) == "rgba(42,120,214,0.3)"
|
assert hex_to_rgba("2a78d6", 0.3) == "rgba(42,120,214,0.3)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_threshold_lines_adds_one_line_per_clinical_threshold():
|
||||||
|
fig = go.Figure()
|
||||||
|
add_threshold_lines(fig, PALETTE["light"])
|
||||||
|
assert len(fig.layout.shapes) == len(THRESHOLD_LINES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_threshold_lines_targets_subplot_row_when_given():
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.add_trace(go.Scatter(x=[0], y=[0]))
|
||||||
|
add_threshold_lines(fig, PALETTE["light"], row=1)
|
||||||
|
assert len(fig.layout.shapes) == len(THRESHOLD_LINES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_tir_stacked_bar_figure_has_one_bar_trace_per_band():
|
||||||
|
tir = {"very_low": 1.0, "low": 3.0, "in_range": 80.0, "high": 12.0, "very_high": 4.0}
|
||||||
|
fig = build_tir_stacked_bar_figure(tir, PALETTE["light"])
|
||||||
|
assert len(fig.data) == 5
|
||||||
|
assert [trace.y[0] for trace in fig.data] == [1.0, 3.0, 80.0, 12.0, 4.0]
|
||||||
|
assert fig.layout.barmode == "stack"
|
||||||
|
|||||||
+92
-1
@@ -15,7 +15,15 @@ import plotly.graph_objects as go
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from nightscout_client import fetch_entries_range, fetch_treatments_range
|
from nightscout_client import fetch_entries_range, fetch_treatments_range
|
||||||
from reporting import completeness_warning, detect_events, summarize_events
|
from reporting import (
|
||||||
|
HIGH,
|
||||||
|
LOW,
|
||||||
|
VERY_HIGH,
|
||||||
|
VERY_LOW,
|
||||||
|
completeness_warning,
|
||||||
|
detect_events,
|
||||||
|
summarize_events,
|
||||||
|
)
|
||||||
|
|
||||||
MMOL_HOVERTEMPLATE = "%{x|%d-%m %H:%M} · %{y:.1f} mmol/L<extra></extra>"
|
MMOL_HOVERTEMPLATE = "%{x|%d-%m %H:%M} · %{y:.1f} mmol/L<extra></extra>"
|
||||||
|
|
||||||
@@ -41,6 +49,14 @@ PALETTE = {
|
|||||||
"muted": "#898781",
|
"muted": "#898781",
|
||||||
"text": "#52514e",
|
"text": "#52514e",
|
||||||
"good": "#0ca30c",
|
"good": "#0ca30c",
|
||||||
|
# Losstaand van "low"/"high" hierboven (die blijven gedeeld tussen
|
||||||
|
# andere grafieken): de 5-bands TIR-grafiek (gitea #14) heeft juist
|
||||||
|
# zichtbaar onderscheid tussen laag/zeer-laag en hoog/zeer-hoog nodig.
|
||||||
|
"tir_very_low": "#8b1a1a",
|
||||||
|
"tir_low": "#d03b3b",
|
||||||
|
"tir_in_range": "#0ca30c",
|
||||||
|
"tir_high": "#fab219",
|
||||||
|
"tir_very_high": "#c76f0a",
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"glucose": "#3987e5",
|
"glucose": "#3987e5",
|
||||||
@@ -55,9 +71,24 @@ PALETTE = {
|
|||||||
"muted": "#898781",
|
"muted": "#898781",
|
||||||
"text": "#c3c2b7",
|
"text": "#c3c2b7",
|
||||||
"good": "#0ca30c",
|
"good": "#0ca30c",
|
||||||
|
"tir_very_low": "#a32626",
|
||||||
|
"tir_low": "#d95d5d",
|
||||||
|
"tir_in_range": "#0ca30c",
|
||||||
|
"tir_high": "#fab219",
|
||||||
|
"tir_very_high": "#d9852e",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Klinische drempellijnen (Battelino et al. 2019), gedeeld tussen de AGP- en
|
||||||
|
# Dagoverzicht-pagina's (FR3, FR4 — gitea #15) zodat beide grafieken exact
|
||||||
|
# dezelfde 4 lijnen op dezelfde manier tonen.
|
||||||
|
THRESHOLD_LINES = [
|
||||||
|
(VERY_LOW, "3.0"),
|
||||||
|
(LOW, "3.9"),
|
||||||
|
(HIGH, "10.0"),
|
||||||
|
(VERY_HIGH, "13.9"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def hex_to_rgba(hex_color: str, alpha: float) -> str:
|
def hex_to_rgba(hex_color: str, alpha: float) -> str:
|
||||||
"""Zet een '#rrggbb'-kleur om naar een 'rgba(r,g,b,a)'-string voor Plotly-fills."""
|
"""Zet een '#rrggbb'-kleur om naar een 'rgba(r,g,b,a)'-string voor Plotly-fills."""
|
||||||
@@ -98,6 +129,66 @@ def load_range_data(start: date, end: date):
|
|||||||
return glucose, treatments
|
return glucose, treatments
|
||||||
|
|
||||||
|
|
||||||
|
def add_threshold_lines(fig: go.Figure, colors: dict, row: int | None = None) -> None:
|
||||||
|
"""Teken de 4 klinische drempellijnen (3.0/3.9/10.0/13.9 mmol/L) op een grafiek.
|
||||||
|
|
||||||
|
Gedeeld door de AGP- en Dagoverzicht-pagina's (gitea #15). `row` wordt
|
||||||
|
doorgegeven aan `add_hline` voor subplot-figuren (Dagoverzicht); voor een
|
||||||
|
los-staande figuur (AGP) blijft dit `None`.
|
||||||
|
"""
|
||||||
|
kwargs = {} if row is None else {"row": row, "col": 1}
|
||||||
|
for value, label in THRESHOLD_LINES:
|
||||||
|
fig.add_hline(
|
||||||
|
y=value,
|
||||||
|
line={"color": colors["muted"], "width": 1, "dash": "dot"},
|
||||||
|
annotation_text=label,
|
||||||
|
annotation_position="right",
|
||||||
|
annotation_font={"size": 9, "color": colors["muted"]},
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TIR_BAND_ORDER = ["very_low", "low", "in_range", "high", "very_high"]
|
||||||
|
TIR_BAND_LABELS = {
|
||||||
|
"very_low": "Zeer laag",
|
||||||
|
"low": "Laag",
|
||||||
|
"in_range": "In bereik",
|
||||||
|
"high": "Hoog",
|
||||||
|
"very_high": "Zeer hoog",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_tir_stacked_bar_figure(tir: dict, colors: dict) -> go.Figure:
|
||||||
|
"""Bouw de 5-bands tijd-in-bereik als één gestapelde staafgrafiek (gitea #14).
|
||||||
|
|
||||||
|
Eén staaf, geen x-as-label, percentage op de y-as; van onder naar boven
|
||||||
|
"Zeer laag" -> "Zeer hoog" — dezelfde volgorde als de klinische banden.
|
||||||
|
"""
|
||||||
|
fig = go.Figure()
|
||||||
|
for band in TIR_BAND_ORDER:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Bar(
|
||||||
|
x=[""],
|
||||||
|
y=[tir[band]],
|
||||||
|
name=TIR_BAND_LABELS[band],
|
||||||
|
marker_color=colors[f"tir_{band}"],
|
||||||
|
hovertemplate=f"{TIR_BAND_LABELS[band]}: %{{y:.0f}}%<extra></extra>",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.update_layout(
|
||||||
|
barmode="stack",
|
||||||
|
paper_bgcolor="rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor="rgba(0,0,0,0)",
|
||||||
|
font_color=colors["text"],
|
||||||
|
height=140,
|
||||||
|
margin={"l": 10, "r": 10, "t": 10, "b": 10},
|
||||||
|
yaxis={"title": "%", "range": [0, 100], "gridcolor": colors["grid"]},
|
||||||
|
xaxis={"visible": False},
|
||||||
|
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "left", "x": 0},
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def render_completeness_warning(glucose: pd.DataFrame, start: date, end: date) -> None:
|
def render_completeness_warning(glucose: pd.DataFrame, start: date, end: date) -> None:
|
||||||
"""Toon reporting.completeness_warning()'s oordeel als st.warning, indien nodig.
|
"""Toon reporting.completeness_warning()'s oordeel als st.warning, indien nodig.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user