feat: pathway depth distribution chart tab (Task C.2)
Horizontal bar chart showing patients who stopped at each treatment line depth (exclusive counts, not cumulative like the funnel).
This commit is contained in:
@@ -1139,6 +1139,76 @@ def get_retention_funnel(
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_pathway_depth_distribution(
|
||||
db_path: Path,
|
||||
date_filter_id: str,
|
||||
chart_type: str,
|
||||
directory: Optional[str] = None,
|
||||
trust: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""Count patients who STOPPED at each treatment line depth.
|
||||
|
||||
Unlike the retention funnel (cumulative), this shows exclusive counts:
|
||||
patients at depth N minus patients at depth N+1 = stopped at depth N.
|
||||
|
||||
Returns list of dicts sorted by depth ascending:
|
||||
[{depth: 1, label: "1 drug only", patients: N, pct: 45.2}, ...]
|
||||
"""
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
where = ["date_filter_id = ?", "chart_type = ?", "level >= 3"]
|
||||
params: list = [date_filter_id, chart_type]
|
||||
|
||||
if directory:
|
||||
where.append("directory = ?")
|
||||
params.append(directory)
|
||||
if trust:
|
||||
where.append("trust_name = ?")
|
||||
params.append(trust)
|
||||
|
||||
query = f"""
|
||||
SELECT level, SUM(value) AS patients
|
||||
FROM pathway_nodes
|
||||
WHERE {' AND '.join(where)}
|
||||
GROUP BY level
|
||||
ORDER BY level
|
||||
"""
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
|
||||
if not rows:
|
||||
return []
|
||||
|
||||
# Build list of (depth, cumulative_patients)
|
||||
levels = []
|
||||
for r in rows:
|
||||
depth = r["level"] - 2 # level 3 → depth 1
|
||||
patients = r["patients"] or 0
|
||||
levels.append((depth, patients))
|
||||
|
||||
# Subtract next level to get "stopped at this depth"
|
||||
total_patients = levels[0][1] if levels else 0
|
||||
result = []
|
||||
for i, (depth, patients) in enumerate(levels):
|
||||
next_patients = levels[i + 1][1] if i + 1 < len(levels) else 0
|
||||
stopped = patients - next_patients
|
||||
|
||||
label = f"{depth} drug{'s' if depth > 1 else ''} only"
|
||||
pct = round(stopped / total_patients * 100, 1) if total_patients else 0
|
||||
result.append({
|
||||
"depth": depth,
|
||||
"label": label,
|
||||
"patients": stopped,
|
||||
"pct": pct,
|
||||
})
|
||||
|
||||
return result
|
||||
except sqlite3.Error:
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_directorate_summary(
|
||||
db_path: Path,
|
||||
date_filter_id: str,
|
||||
|
||||
@@ -1820,3 +1820,74 @@ def create_retention_funnel_figure(
|
||||
fig.update_layout(**layout)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_pathway_depth_figure(
|
||||
data: list[dict],
|
||||
title: str = "",
|
||||
) -> go.Figure:
|
||||
"""Create a horizontal bar chart showing patients who stopped at each treatment depth.
|
||||
|
||||
Args:
|
||||
data: List of dicts with keys: depth, label, patients, pct
|
||||
title: Chart title from filter state.
|
||||
|
||||
Returns:
|
||||
Plotly Figure with horizontal bar trace.
|
||||
"""
|
||||
if not data:
|
||||
return go.Figure()
|
||||
|
||||
display_title = f"Pathway Depth Distribution — {title}" if title else "Pathway Depth Distribution"
|
||||
|
||||
labels = [d["label"] for d in data]
|
||||
patients = [d["patients"] for d in data]
|
||||
pcts = [d["pct"] for d in data]
|
||||
|
||||
# NHS blue gradient: darkest for depth 1 (most patients) → lightest
|
||||
bar_colors = [
|
||||
"#003087",
|
||||
"#005EB8",
|
||||
"#1E88E5",
|
||||
"#42A5F5",
|
||||
"#90CAF9",
|
||||
]
|
||||
colors = bar_colors[: len(data)]
|
||||
if len(colors) < len(data):
|
||||
colors.extend(["#E3F2FD"] * (len(data) - len(colors)))
|
||||
|
||||
fig = go.Figure(
|
||||
go.Bar(
|
||||
y=labels,
|
||||
x=patients,
|
||||
orientation="h",
|
||||
text=[f"{p:,} ({pct}%)" for p, pct in zip(patients, pcts)],
|
||||
textposition="auto",
|
||||
textfont=dict(family=CHART_FONT_FAMILY, size=13),
|
||||
marker=dict(color=colors),
|
||||
hovertemplate=(
|
||||
"<b>%{y}</b><br>"
|
||||
"Patients: %{x:,}<br>"
|
||||
"<extra></extra>"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
layout = _base_layout(display_title)
|
||||
layout.update(
|
||||
margin=dict(t=60, l=8, r=24, b=40),
|
||||
yaxis=dict(
|
||||
automargin=True,
|
||||
autorange="reversed",
|
||||
title="",
|
||||
),
|
||||
xaxis=dict(
|
||||
title="Patients",
|
||||
gridcolor=GRID_COLOR,
|
||||
),
|
||||
height=max(300, len(data) * 70 + 120),
|
||||
bargap=0.3,
|
||||
)
|
||||
fig.update_layout(**layout)
|
||||
|
||||
return fig
|
||||
|
||||
Reference in New Issue
Block a user