feat: drug timeline Gantt chart tab (Task D.3)
This commit is contained in:
@@ -1373,6 +1373,76 @@ def get_drug_network(
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_drug_timeline(
|
||||
db_path: Path,
|
||||
date_filter_id: str,
|
||||
chart_type: str,
|
||||
directory: Optional[str] = None,
|
||||
trust: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""Get drug timeline data for Gantt-style chart.
|
||||
|
||||
Queries level 3 nodes and aggregates across trusts to get the earliest
|
||||
first_seen and latest last_seen per drug × directory.
|
||||
|
||||
Returns list of dicts sorted by directory then first_seen:
|
||||
[{"drug": "ADALIMUMAB", "directory": "RHEUMATOLOGY",
|
||||
"first_seen": "2019-04-04T00:00:00", "last_seen": "2025-12-30T00:00:00",
|
||||
"patients": 2053, "cost_pp_pa": 2170.5}, ...]
|
||||
"""
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
conditions = ["date_filter_id = ?", "chart_type = ?", "level = 3"]
|
||||
params: list = [date_filter_id, chart_type]
|
||||
|
||||
if directory:
|
||||
conditions.append("directory = ?")
|
||||
params.append(directory)
|
||||
if trust:
|
||||
conditions.append("trust_name = ?")
|
||||
params.append(trust)
|
||||
|
||||
where = " AND ".join(conditions)
|
||||
query = f"""
|
||||
SELECT
|
||||
labels AS drug,
|
||||
directory,
|
||||
MIN(first_seen) AS first_seen,
|
||||
MAX(last_seen) AS last_seen,
|
||||
SUM(value) AS patients,
|
||||
CASE
|
||||
WHEN SUM(value) > 0
|
||||
THEN ROUND(SUM(CAST(cost_pp_pa AS REAL) * value) / SUM(value), 2)
|
||||
ELSE 0
|
||||
END AS cost_pp_pa
|
||||
FROM pathway_nodes
|
||||
WHERE {where}
|
||||
AND first_seen IS NOT NULL AND first_seen != ''
|
||||
AND last_seen IS NOT NULL AND last_seen != ''
|
||||
GROUP BY labels, directory
|
||||
ORDER BY directory, first_seen
|
||||
"""
|
||||
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
return [
|
||||
{
|
||||
"drug": r["drug"],
|
||||
"directory": r["directory"],
|
||||
"first_seen": r["first_seen"],
|
||||
"last_seen": r["last_seen"],
|
||||
"patients": r["patients"] or 0,
|
||||
"cost_pp_pa": r["cost_pp_pa"] or 0,
|
||||
}
|
||||
for r in rows
|
||||
if r["patients"] and r["patients"] > 0
|
||||
]
|
||||
except sqlite3.Error:
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_directorate_summary(
|
||||
db_path: Path,
|
||||
date_filter_id: str,
|
||||
|
||||
@@ -2085,3 +2085,124 @@ def create_drug_network_figure(data: dict, title: str = "") -> go.Figure:
|
||||
fig.update_layout(**layout)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_drug_timeline_figure(data: list[dict], title: str = "") -> go.Figure:
|
||||
"""Create a Gantt-style timeline showing when each drug cohort was active.
|
||||
|
||||
Each horizontal bar spans from first_seen to last_seen for a drug,
|
||||
grouped by directory, with color indicating directory and text showing
|
||||
patient count.
|
||||
|
||||
Args:
|
||||
data: List of dicts with keys: drug, directory, first_seen, last_seen,
|
||||
patients, cost_pp_pa.
|
||||
title: Chart title.
|
||||
|
||||
Returns:
|
||||
Plotly Figure with horizontal bars.
|
||||
"""
|
||||
if not data:
|
||||
return go.Figure()
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
display_title = title or "Drug Timeline"
|
||||
|
||||
# Parse dates and sort by directory then first_seen
|
||||
for d in data:
|
||||
d["_fs"] = datetime.fromisoformat(d["first_seen"])
|
||||
d["_ls"] = datetime.fromisoformat(d["last_seen"])
|
||||
d["_duration_days"] = max(1, (d["_ls"] - d["_fs"]).days)
|
||||
|
||||
# Sort: by directory alphabetically, then by first_seen ascending
|
||||
data.sort(key=lambda d: (d["directory"], d["_fs"]))
|
||||
|
||||
# Assign colors by directory
|
||||
directories = list(dict.fromkeys(d["directory"] for d in data))
|
||||
dir_colors = {
|
||||
d: DRUG_PALETTE[i % len(DRUG_PALETTE)]
|
||||
for i, d in enumerate(directories)
|
||||
}
|
||||
|
||||
# Build y-axis labels: "Drug (Directory)" for multi-directory views, just "Drug" for single
|
||||
single_directory = len(directories) == 1
|
||||
y_labels = []
|
||||
for d in data:
|
||||
if single_directory:
|
||||
y_labels.append(d["drug"])
|
||||
else:
|
||||
y_labels.append(f"{d['drug']} ({d['directory']})")
|
||||
|
||||
# Build one trace per directory for legend grouping
|
||||
fig = go.Figure()
|
||||
dir_legend_shown = set()
|
||||
|
||||
for i, d in enumerate(data):
|
||||
show_legend = d["directory"] not in dir_legend_shown
|
||||
dir_legend_shown.add(d["directory"])
|
||||
|
||||
duration_ms = d["_duration_days"] * 86_400_000 # days → milliseconds
|
||||
patients = d["patients"]
|
||||
cost = d["cost_pp_pa"]
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
y=[y_labels[i]],
|
||||
x=[duration_ms],
|
||||
base=[d["_fs"]],
|
||||
orientation="h",
|
||||
marker=dict(
|
||||
color=dir_colors[d["directory"]],
|
||||
line=dict(width=0),
|
||||
),
|
||||
name=d["directory"],
|
||||
legendgroup=d["directory"],
|
||||
showlegend=show_legend,
|
||||
text=f"{patients:,}",
|
||||
textposition="inside",
|
||||
textfont=dict(color="white", size=10),
|
||||
hovertemplate=(
|
||||
f"<b>{d['drug']}</b><br>"
|
||||
f"Directory: {d['directory']}<br>"
|
||||
f"First seen: {d['_fs'].strftime('%b %Y')}<br>"
|
||||
f"Last seen: {d['_ls'].strftime('%b %Y')}<br>"
|
||||
f"Duration: {d['_duration_days']:,} days<br>"
|
||||
f"Patients: {patients:,}<br>"
|
||||
f"Cost p.a.: £{cost:,.0f}"
|
||||
"<extra></extra>"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Layout
|
||||
n_bars = len(data)
|
||||
bar_height = 28
|
||||
dynamic_height = max(400, n_bars * bar_height + 120)
|
||||
|
||||
n_dirs = len(directories)
|
||||
legend_margins = _smart_legend_margin(n_dirs)
|
||||
legend = _smart_legend(n_dirs, legend_title="Directory")
|
||||
|
||||
layout = _base_layout(display_title)
|
||||
layout.update(
|
||||
xaxis=dict(
|
||||
type="date",
|
||||
gridcolor=GRID_COLOR,
|
||||
dtick="M6",
|
||||
tickformat="%b\n%Y",
|
||||
),
|
||||
yaxis=dict(
|
||||
automargin=True,
|
||||
autorange="reversed",
|
||||
tickfont=dict(size=11),
|
||||
),
|
||||
barmode="overlay",
|
||||
height=dynamic_height,
|
||||
margin=dict(t=60, l=8, **legend_margins),
|
||||
legend=legend,
|
||||
bargap=0.3,
|
||||
)
|
||||
fig.update_layout(**layout)
|
||||
|
||||
return fig
|
||||
|
||||
Reference in New Issue
Block a user