Add basal/bolus/carb/target overlays to the Live and Dagoverzicht charts
willemjan/glucose-dashboard/pipeline/pr-feature/insulin-profile This commit looks good
willemjan/glucose-dashboard/pipeline/pr-feature/insulin-profile This commit looks good
Overlay the full insulin picture onto the interactive glucose charts: basal rate as a semi-transparent step-area (profile schedule overridden by active temp basal), bolus doses as size-scaled dots split into food/correction/SMB categories (food and correction labeled with units, SMB left unlabeled), carbs as bars, and the pump's target glucose as a line — each with its own color, alpha, and legend entry. Shared trace- building helpers live in ui_common.py so both pages (and the PDF report in a follow-up PR) render identically. Builds on the pump profile work in #29 (profile_data.py, load_profile()).
This commit is contained in:
+154
@@ -209,6 +209,160 @@ def build_tir_stacked_bar_figure(tir: dict, colors: dict) -> go.Figure:
|
||||
return fig
|
||||
|
||||
|
||||
BOLUS_CATEGORY_LABELS = {
|
||||
"food": "Bolus (voeding)",
|
||||
"correction": "Bolus (correctie)",
|
||||
"smb": "SMB",
|
||||
}
|
||||
BOLUS_CATEGORY_COLOR_KEYS = {
|
||||
"food": "bolus_food",
|
||||
"correction": "bolus_correction",
|
||||
"smb": "bolus_smb",
|
||||
}
|
||||
# Tekenvolgorde van bolus-categorieën: SMB eerst (onderop, meestal talrijker en
|
||||
# kleiner), voeding/correctie erbovenop (grotere, gelabelde stippen blijven
|
||||
# zichtbaar in plaats van bedolven te raken onder de SMB-stippen).
|
||||
BOLUS_CATEGORY_DRAW_ORDER = ["smb", "correction", "food"]
|
||||
|
||||
|
||||
def add_basal_area_trace(
|
||||
fig: go.Figure,
|
||||
basal: pd.DataFrame,
|
||||
colors: dict,
|
||||
*,
|
||||
row: int | None = None,
|
||||
secondary_y: bool = False,
|
||||
x_col: str = "timestamp",
|
||||
) -> None:
|
||||
"""Basaal-insuline als semi-transparant stapvlak (line_shape='hv'), met alpha
|
||||
zodat onderliggende data zichtbaar blijft. Verwacht kolommen 'rate' en
|
||||
`x_col` (standaard 'timestamp'; de Dagoverzicht-pagina geeft hier 'hour'
|
||||
aan omdat elke dag daar op een gedeelde 0-24u-as staat i.p.v. echte
|
||||
datums — zie reporting.effective_basal_series). Bedoeld voor een
|
||||
secundaire y-as (E/uur past niet op de mmol/L-schaal van de glucoselijn).
|
||||
"""
|
||||
if basal.empty:
|
||||
return
|
||||
kwargs: dict = {"row": row, "col": 1} if row is not None else {}
|
||||
if secondary_y:
|
||||
kwargs["secondary_y"] = True
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=basal[x_col],
|
||||
y=basal["rate"],
|
||||
mode="lines",
|
||||
line={"shape": "hv", "color": colors["basal"], "width": 1},
|
||||
fill="tozeroy",
|
||||
fillcolor=hex_to_rgba(colors["basal"], 0.3),
|
||||
opacity=0.8,
|
||||
name="Basaal",
|
||||
hovertemplate="%{y:.2f} E/uur<extra></extra>",
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def add_carb_bars_trace(
|
||||
fig: go.Figure,
|
||||
carbs: pd.DataFrame,
|
||||
colors: dict,
|
||||
*,
|
||||
row: int | None = None,
|
||||
secondary_y: bool = False,
|
||||
x_col: str = "timestamp",
|
||||
) -> None:
|
||||
"""Koolhydraten als staafjes op het moment van invoer. Verwacht kolommen
|
||||
'carbs' en `x_col` (zie add_basal_area_trace voor waarom dit configureerbaar
|
||||
is). Zelfde secundaire as als de basaal-area (allebei geen mmol/L-grootheid),
|
||||
met een eigen kleur en alpha zodat beide onderscheidbaar blijven."""
|
||||
if carbs.empty:
|
||||
return
|
||||
kwargs: dict = {"row": row, "col": 1} if row is not None else {}
|
||||
if secondary_y:
|
||||
kwargs["secondary_y"] = True
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=carbs[x_col],
|
||||
y=carbs["carbs"],
|
||||
marker_color=colors["carbs"],
|
||||
opacity=0.55,
|
||||
name="Koolhydraten",
|
||||
hovertemplate="%{y:.0f} g<extra></extra>",
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def add_bolus_marker_traces(
|
||||
fig: go.Figure,
|
||||
bolus: pd.DataFrame,
|
||||
colors: dict,
|
||||
lane_y: float,
|
||||
*,
|
||||
row: int | None = None,
|
||||
x_col: str = "timestamp",
|
||||
) -> None:
|
||||
"""Eén Scatter-trace per boluscategorie (food/correction/smb), gepositioneerd
|
||||
op een vaste 'lane'-hoogte op de primaire (mmol/L-)as — net als de bestaande
|
||||
insuline/carbs-lanes op de Live-pagina. De hoogte zelf draagt geen
|
||||
kwantitatieve betekenis; de markergrootte (geschaald naar toegediende
|
||||
eenheden) en hoverwaarde wel. Voeding/correctie krijgen een tekstlabel met
|
||||
de dosis; SMB's niet (te talrijk/te klein voor leesbare labels). Verwacht
|
||||
kolommen 'insulin'/'category' en `x_col` (zie reporting.classify_bolus).
|
||||
"""
|
||||
# Datumformattering in de hover werkt alleen op een echte datetime-as; de
|
||||
# Dagoverzicht-pagina geeft hier 'hour' (een kaal getal 0-24) aan, dus
|
||||
# daar laten we het tijdstip weg en toont de hover alleen de dosis.
|
||||
hover_prefix = "%{x|%d-%m %H:%M} · " if x_col == "timestamp" else ""
|
||||
kwargs: dict = {"row": row, "col": 1} if row is not None else {}
|
||||
for category in BOLUS_CATEGORY_DRAW_ORDER:
|
||||
subset = bolus[bolus["category"] == category] if not bolus.empty else bolus
|
||||
if subset.empty:
|
||||
continue
|
||||
show_label = category != "smb"
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=subset[x_col],
|
||||
y=[lane_y] * len(subset),
|
||||
mode="markers+text" if show_label else "markers",
|
||||
marker={
|
||||
"size": (subset["insulin"] * 8).clip(lower=6, upper=30),
|
||||
"color": colors[BOLUS_CATEGORY_COLOR_KEYS[category]],
|
||||
"opacity": 0.75,
|
||||
},
|
||||
text=[f"{v:.1f}E" for v in subset["insulin"]] if show_label else None,
|
||||
textposition="top center",
|
||||
textfont={"size": 9, "color": colors["text"]},
|
||||
name=BOLUS_CATEGORY_LABELS[category],
|
||||
customdata=subset["insulin"],
|
||||
hovertemplate=f"{hover_prefix}%{{customdata:.2f}} E<extra></extra>",
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def add_target_line_trace(
|
||||
fig: go.Figure, target: pd.DataFrame, colors: dict, *, row: int | None = None, x_col: str = "timestamp"
|
||||
) -> None:
|
||||
"""Streefglucose uit het pompprofiel als lijn op de primaire (mmol/L-)as.
|
||||
Verwacht kolommen 'target' en `x_col` (zie reporting.target_glucose_series)."""
|
||||
if target.empty:
|
||||
return
|
||||
kwargs: dict = {"row": row, "col": 1} if row is not None else {}
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=target[x_col],
|
||||
y=target["target"],
|
||||
mode="lines",
|
||||
line={"shape": "hv", "color": colors["target"], "width": 1.5, "dash": "dashdot"},
|
||||
opacity=0.9,
|
||||
name="Streefwaarde (pomp)",
|
||||
hovertemplate="%{y:.1f} mmol/L<extra></extra>",
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def render_completeness_warning(glucose: pd.DataFrame, start: date, end: date) -> None:
|
||||
"""Toon reporting.completeness_warning()'s oordeel als st.warning, indien nodig.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user