feat: Trust Comparison 6-chart dashboard with real data (Task 10.8)
- Add 3 new visualization functions to plotly_generator.py: create_trust_market_share_figure, create_trust_heatmap_figure, create_trust_duration_figure - Replace 6 placeholder callbacks in trust_comparison.py with real implementations using trust-comparison queries + figure builders - Cost Waterfall reuses existing figure function via key mapping - Dosing reuses existing create_dosing_figure with group_by="trust" - Cost Effectiveness reuses existing function scoped to directorate - All 6 charts respond to date filter and chart type toggle - Validated with both directory (RHEUMATOLOGY) and indication (asthma)
This commit is contained in:
@@ -533,18 +533,18 @@ Additionally: KPI row removed, fraction KPIs moved to header, global filter sub-
|
||||
- **Checkpoint**: Landing page shows directorate buttons, clicking one transitions to dashboard state, back button works
|
||||
|
||||
### 10.8 Trust Comparison 6-chart dashboard
|
||||
- [ ] Build 6-chart dashboard layout per design from 10.1
|
||||
- [ ] All 6 charts scoped to the selected directorate:
|
||||
- [x] Build 6-chart dashboard layout per design from 10.1
|
||||
- [x] All 6 charts scoped to the selected directorate:
|
||||
1. **Market Share**: Drug breakdown per trust (using `get_trust_market_share`)
|
||||
2. **Cost Waterfall**: Per-trust cost within directorate (using `get_trust_cost_waterfall`)
|
||||
3. **Dosing**: Drug dosing intervals by trust (using `get_trust_dosing`)
|
||||
4. **Heatmap**: Trust × drug matrix (using `get_trust_heatmap`)
|
||||
5. **Duration**: Drug durations by trust (using `get_trust_durations`)
|
||||
6. **Cost Effectiveness**: Pathway costs within directorate, NOT split by trust (using `get_directorate_pathway_costs`)
|
||||
- [ ] Create new visualization functions in `src/visualization/plotly_generator.py` where existing ones don't fit the trust-comparison perspective (may need `create_trust_market_share_figure`, `create_trust_heatmap_figure`, etc., or parameterize existing functions)
|
||||
- [ ] All 6 charts respond to date filter and chart type toggle (global filters)
|
||||
- [ ] Dashboard title shows selected directorate name
|
||||
- [ ] Use `dcc.Loading` wrappers for each chart
|
||||
- [x] Create new visualization functions in `src/visualization/plotly_generator.py` where existing ones don't fit the trust-comparison perspective (may need `create_trust_market_share_figure`, `create_trust_heatmap_figure`, etc., or parameterize existing functions)
|
||||
- [x] All 6 charts respond to date filter and chart type toggle (global filters)
|
||||
- [x] Dashboard title shows selected directorate name
|
||||
- [x] Use `dcc.Loading` wrappers for each chart
|
||||
- **Checkpoint**: All 6 charts render for a selected directorate, comparing drugs across trusts. Charts update when date filter or chart type changes.
|
||||
|
||||
### 10.9 Patient Pathways filter relocation
|
||||
|
||||
@@ -91,41 +91,170 @@ def register_trust_comparison_callbacks(app):
|
||||
else:
|
||||
return show, hide, ""
|
||||
|
||||
# Dashboard chart rendering will be added in Task 10.8.
|
||||
# For now, register empty figure placeholders for the 6 chart IDs
|
||||
# so the dcc.Graph components don't error on load.
|
||||
_tc_chart_ids = [
|
||||
"tc-chart-market-share",
|
||||
"tc-chart-cost-waterfall",
|
||||
"tc-chart-dosing",
|
||||
"tc-chart-heatmap",
|
||||
"tc-chart-duration",
|
||||
"tc-chart-cost-effectiveness",
|
||||
]
|
||||
# --- Trust Comparison dashboard charts (6 charts) ---
|
||||
|
||||
for chart_id in _tc_chart_ids:
|
||||
def _tc_empty(message):
|
||||
"""Return a blank figure with a centered message for TC dashboard."""
|
||||
fig = go.Figure()
|
||||
fig.update_layout(
|
||||
xaxis={"visible": False}, yaxis={"visible": False},
|
||||
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
|
||||
margin={"t": 0, "l": 0, "r": 0, "b": 0}, height=300,
|
||||
annotations=[{
|
||||
"text": message, "xref": "paper", "yref": "paper",
|
||||
"x": 0.5, "y": 0.5, "showarrow": False,
|
||||
"font": {"size": 14, "color": "#768692", "family": "Source Sans 3"},
|
||||
"xanchor": "center", "yanchor": "middle",
|
||||
}],
|
||||
)
|
||||
return fig
|
||||
|
||||
def _tc_title(app_state):
|
||||
"""Generate a short title suffix from global filter state."""
|
||||
chart_type = (app_state or {}).get("chart_type", "directory")
|
||||
label = "By Indication" if chart_type == "indication" else "By Directory"
|
||||
initiated = (app_state or {}).get("initiated", "all")
|
||||
last_seen = (app_state or {}).get("last_seen", "6mo")
|
||||
i_labels = {"all": "All years", "1yr": "Last 1 yr", "2yr": "Last 2 yrs"}
|
||||
l_labels = {"6mo": "6 mo", "12mo": "12 mo"}
|
||||
return f"{label} | {i_labels.get(initiated, 'All')} / {l_labels.get(last_seen, '6 mo')}"
|
||||
|
||||
# 1. Market Share — drug breakdown per trust
|
||||
@app.callback(
|
||||
Output(chart_id, "figure"),
|
||||
Output("tc-chart-market-share", "figure"),
|
||||
Input("app-state", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def _placeholder_chart(app_state, _cid=chart_id):
|
||||
"""Placeholder — returns empty figure until Task 10.8 implements real charts."""
|
||||
def tc_market_share(app_state):
|
||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||
if not selected:
|
||||
return no_update
|
||||
fig = go.Figure()
|
||||
fig.update_layout(
|
||||
template="plotly_white",
|
||||
margin=dict(l=20, r=20, t=30, b=20),
|
||||
height=300,
|
||||
annotations=[
|
||||
dict(
|
||||
text="Chart will be implemented in Task 10.8",
|
||||
xref="paper", yref="paper",
|
||||
x=0.5, y=0.5, showarrow=False,
|
||||
font=dict(size=14, color="#999"),
|
||||
from dash_app.data.queries import get_trust_market_share
|
||||
from visualization.plotly_generator import create_trust_market_share_figure
|
||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||
chart_type = app_state.get("chart_type", "directory")
|
||||
try:
|
||||
data = get_trust_market_share(filter_id, chart_type, selected)
|
||||
except Exception:
|
||||
return _tc_empty("Failed to load market share data.")
|
||||
if not data:
|
||||
return _tc_empty("No market share data for this selection.")
|
||||
return create_trust_market_share_figure(data, _tc_title(app_state))
|
||||
|
||||
# 2. Cost Waterfall — cost per patient by trust
|
||||
@app.callback(
|
||||
Output("tc-chart-cost-waterfall", "figure"),
|
||||
Input("app-state", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
],
|
||||
def tc_cost_waterfall(app_state):
|
||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||
if not selected:
|
||||
return no_update
|
||||
from dash_app.data.queries import get_trust_cost_waterfall
|
||||
from visualization.plotly_generator import create_cost_waterfall_figure
|
||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||
chart_type = app_state.get("chart_type", "directory")
|
||||
try:
|
||||
data = get_trust_cost_waterfall(filter_id, chart_type, selected)
|
||||
except Exception:
|
||||
return _tc_empty("Failed to load cost data.")
|
||||
if not data:
|
||||
return _tc_empty("No cost data for this selection.")
|
||||
# Reuse existing waterfall figure — map trust_name to directory key
|
||||
mapped = [{"directory": d["trust_name"], "patients": d["patients"],
|
||||
"total_cost": d["total_cost"], "cost_pp": d["cost_pp"]} for d in data]
|
||||
return create_cost_waterfall_figure(mapped, _tc_title(app_state))
|
||||
|
||||
# 3. Dosing — drug dosing intervals by trust
|
||||
@app.callback(
|
||||
Output("tc-chart-dosing", "figure"),
|
||||
Input("app-state", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
return fig
|
||||
def tc_dosing(app_state):
|
||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||
if not selected:
|
||||
return no_update
|
||||
from dash_app.data.queries import get_trust_dosing
|
||||
from visualization.plotly_generator import create_dosing_figure
|
||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||
chart_type = app_state.get("chart_type", "directory")
|
||||
try:
|
||||
data = get_trust_dosing(filter_id, chart_type, selected)
|
||||
except Exception:
|
||||
return _tc_empty("Failed to load dosing data.")
|
||||
if not data:
|
||||
return _tc_empty("No dosing data for this selection.")
|
||||
# Add directory field expected by _dosing_by_trust helper
|
||||
for d in data:
|
||||
d["directory"] = selected
|
||||
return create_dosing_figure(data, _tc_title(app_state), group_by="trust")
|
||||
|
||||
# 4. Heatmap — trust x drug matrix
|
||||
@app.callback(
|
||||
Output("tc-chart-heatmap", "figure"),
|
||||
Input("app-state", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def tc_heatmap(app_state):
|
||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||
if not selected:
|
||||
return no_update
|
||||
from dash_app.data.queries import get_trust_heatmap
|
||||
from visualization.plotly_generator import create_trust_heatmap_figure
|
||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||
chart_type = app_state.get("chart_type", "directory")
|
||||
try:
|
||||
data = get_trust_heatmap(filter_id, chart_type, selected)
|
||||
except Exception:
|
||||
return _tc_empty("Failed to load heatmap data.")
|
||||
if not data.get("trusts") or not data.get("drugs"):
|
||||
return _tc_empty("No heatmap data for this selection.")
|
||||
return create_trust_heatmap_figure(data, _tc_title(app_state))
|
||||
|
||||
# 5. Duration — drug durations by trust
|
||||
@app.callback(
|
||||
Output("tc-chart-duration", "figure"),
|
||||
Input("app-state", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def tc_duration(app_state):
|
||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||
if not selected:
|
||||
return no_update
|
||||
from dash_app.data.queries import get_trust_durations
|
||||
from visualization.plotly_generator import create_trust_duration_figure
|
||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||
chart_type = app_state.get("chart_type", "directory")
|
||||
try:
|
||||
data = get_trust_durations(filter_id, chart_type, selected)
|
||||
except Exception:
|
||||
return _tc_empty("Failed to load duration data.")
|
||||
if not data:
|
||||
return _tc_empty("No duration data for this selection.")
|
||||
return create_trust_duration_figure(data, _tc_title(app_state))
|
||||
|
||||
# 6. Cost Effectiveness — pathway costs within directorate (NOT split by trust)
|
||||
@app.callback(
|
||||
Output("tc-chart-cost-effectiveness", "figure"),
|
||||
Input("app-state", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def tc_cost_effectiveness(app_state):
|
||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||
if not selected:
|
||||
return no_update
|
||||
from dash_app.data.queries import get_pathway_costs
|
||||
from data_processing.parsing import calculate_retention_rate
|
||||
from visualization.plotly_generator import create_cost_effectiveness_figure
|
||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||
chart_type = app_state.get("chart_type", "directory")
|
||||
try:
|
||||
data = get_pathway_costs(filter_id, chart_type, directory=selected)
|
||||
except Exception:
|
||||
return _tc_empty("Failed to load pathway cost data.")
|
||||
if not data:
|
||||
return _tc_empty("No pathway cost data for this selection.")
|
||||
retention = calculate_retention_rate(data)
|
||||
return create_cost_effectiveness_figure(data, retention, _tc_title(app_state))
|
||||
|
||||
@@ -1473,3 +1473,309 @@ def create_duration_figure(
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
# --- Trust Comparison chart functions ---
|
||||
|
||||
|
||||
def create_trust_market_share_figure(
|
||||
data: list[dict],
|
||||
title: str = "",
|
||||
) -> go.Figure:
|
||||
"""Create horizontal stacked bar chart showing drug market share per trust.
|
||||
|
||||
Unlike create_market_share_figure (which groups by directorate), this groups
|
||||
by trust within a single directorate — used by Trust Comparison dashboard.
|
||||
|
||||
Args:
|
||||
data: List of dicts from get_trust_market_share() with keys:
|
||||
trust_name, drug, patients, proportion, cost, cost_pp_pa.
|
||||
title: Chart title suffix.
|
||||
"""
|
||||
if not data:
|
||||
return go.Figure()
|
||||
|
||||
nhs_colours = [
|
||||
"#003087", "#005EB8", "#0072CE", "#1E88E5", "#41B6E6",
|
||||
"#4FC3F7", "#768692", "#AE2573", "#006747", "#ED8B00",
|
||||
"#8A1538", "#330072", "#009639", "#DA291C", "#00A499",
|
||||
]
|
||||
|
||||
seen_trusts = []
|
||||
for d in data:
|
||||
t = d["trust_name"]
|
||||
if t not in seen_trusts:
|
||||
seen_trusts.append(t)
|
||||
|
||||
seen_drugs = []
|
||||
for d in data:
|
||||
if d["drug"] not in seen_drugs:
|
||||
seen_drugs.append(d["drug"])
|
||||
|
||||
drug_colour_map = {drug: nhs_colours[i % len(nhs_colours)] for i, drug in enumerate(seen_drugs)}
|
||||
lookup = {(d["trust_name"], d["drug"]): d for d in data}
|
||||
|
||||
def short_trust(name):
|
||||
return name.replace(" NHS FOUNDATION TRUST", "").replace(" HOSPITALS", "")
|
||||
|
||||
display_trusts = list(reversed(seen_trusts))
|
||||
|
||||
traces = []
|
||||
for drug in seen_drugs:
|
||||
y_vals = []
|
||||
x_vals = []
|
||||
hover_texts = []
|
||||
for trust in display_trusts:
|
||||
row = lookup.get((trust, drug))
|
||||
y_vals.append(short_trust(trust))
|
||||
if row:
|
||||
x_vals.append(row["proportion"] * 100)
|
||||
hover_texts.append(
|
||||
f"<b>{drug}</b><br>"
|
||||
f"{short_trust(trust)}<br>"
|
||||
f"Patients: {row['patients']:,}<br>"
|
||||
f"Share: {row['proportion']:.1%}<br>"
|
||||
f"Cost: \u00a3{row['cost']:,.0f}<br>"
|
||||
f"Cost p.p.p.a: \u00a3{row['cost_pp_pa']:,.0f}"
|
||||
)
|
||||
else:
|
||||
x_vals.append(0)
|
||||
hover_texts.append("")
|
||||
|
||||
traces.append(go.Bar(
|
||||
name=drug, y=y_vals, x=x_vals, orientation="h",
|
||||
marker_color=drug_colour_map[drug],
|
||||
hovertemplate="%{customdata}<extra></extra>",
|
||||
customdata=hover_texts,
|
||||
))
|
||||
|
||||
display_title = f"Drug Market Share by Trust \u2014 {title}" if title else "Drug Market Share by Trust"
|
||||
|
||||
fig = go.Figure(data=traces)
|
||||
fig.update_layout(
|
||||
barmode="stack",
|
||||
title=dict(
|
||||
text=display_title,
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif", size=16, color="#1E293B"),
|
||||
x=0.5, xanchor="center",
|
||||
),
|
||||
xaxis=dict(title="% of patients", ticksuffix="%", range=[0, 105], gridcolor="#E2E8F0", zeroline=False),
|
||||
yaxis=dict(title="", automargin=True),
|
||||
legend=dict(
|
||||
title="Drug", orientation="h", yanchor="top", y=-0.15,
|
||||
xanchor="center", x=0.5, font=dict(family="Source Sans 3", size=11),
|
||||
),
|
||||
margin=dict(t=50, l=8, r=24, b=100),
|
||||
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
|
||||
autosize=True,
|
||||
hoverlabel=dict(
|
||||
bgcolor="#FFFFFF", bordercolor="#CBD5E1",
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif", size=13, color="#1E293B"),
|
||||
),
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif"),
|
||||
height=max(300, len(seen_trusts) * 60 + 200),
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_trust_heatmap_figure(
|
||||
data: dict,
|
||||
title: str = "",
|
||||
metric: str = "patients",
|
||||
) -> go.Figure:
|
||||
"""Create a trust x drug heatmap for a single directorate.
|
||||
|
||||
Args:
|
||||
data: Dict from get_trust_heatmap() with keys:
|
||||
trusts (list), drugs (list),
|
||||
matrix ({trust_name: {drug: {patients, cost, cost_pp_pa}}}).
|
||||
title: Chart title suffix.
|
||||
metric: Colour metric — "patients", "cost", or "cost_pp_pa".
|
||||
"""
|
||||
trusts = data.get("trusts", [])
|
||||
drugs = data.get("drugs", [])
|
||||
matrix = data.get("matrix", {})
|
||||
|
||||
if not trusts or not drugs:
|
||||
return go.Figure()
|
||||
|
||||
drugs = drugs[:25]
|
||||
|
||||
metric_labels = {
|
||||
"patients": "Patients",
|
||||
"cost": "Total Cost (\u00a3)",
|
||||
"cost_pp_pa": "Cost per Patient p.a. (\u00a3)",
|
||||
}
|
||||
metric_label = metric_labels.get(metric, "Patients")
|
||||
|
||||
def short_trust(name):
|
||||
return name.replace(" NHS FOUNDATION TRUST", "").replace(" HOSPITALS", "")
|
||||
|
||||
z_values = []
|
||||
hover_texts = []
|
||||
|
||||
for t in trusts:
|
||||
row_z = []
|
||||
row_hover = []
|
||||
trust_data = matrix.get(t, {})
|
||||
for drug in drugs:
|
||||
cell = trust_data.get(drug)
|
||||
if cell:
|
||||
val = cell.get(metric, cell.get("patients", 0))
|
||||
patients = cell.get("patients", 0)
|
||||
cost = cell.get("cost", 0)
|
||||
cpp = cell.get("cost_pp_pa", 0)
|
||||
row_z.append(val if val else 0)
|
||||
row_hover.append(
|
||||
f"<b>{drug}</b><br>"
|
||||
f"{short_trust(t)}<br>"
|
||||
f"Patients: {patients:,}<br>"
|
||||
f"Total cost: \u00a3{cost:,.0f}<br>"
|
||||
f"Cost p.a.: \u00a3{cpp:,.0f}"
|
||||
)
|
||||
else:
|
||||
row_z.append(0)
|
||||
row_hover.append(f"<b>{drug}</b><br>{short_trust(t)}<br>No patients")
|
||||
z_values.append(row_z)
|
||||
hover_texts.append(row_hover)
|
||||
|
||||
colorscale = [
|
||||
[0.0, "#F0F4F8"], [0.01, "#E3F2FD"], [0.1, "#90CAF9"],
|
||||
[0.3, "#42A5F5"], [0.5, "#1E88E5"], [0.7, "#0066CC"], [1.0, "#003087"],
|
||||
]
|
||||
|
||||
display_trusts = [short_trust(t) for t in trusts]
|
||||
|
||||
fig = go.Figure(
|
||||
data=go.Heatmap(
|
||||
z=z_values, x=drugs, y=display_trusts,
|
||||
colorscale=colorscale,
|
||||
hovertext=hover_texts,
|
||||
hovertemplate="%{hovertext}<extra></extra>",
|
||||
colorbar=dict(
|
||||
title=dict(text=metric_label, font=dict(size=12, color="#425563")),
|
||||
thickness=15, len=0.8,
|
||||
),
|
||||
xgap=2, ygap=2,
|
||||
)
|
||||
)
|
||||
|
||||
chart_title = f"Trust \u00d7 Drug \u2014 {metric_label}"
|
||||
if title:
|
||||
chart_title = f"{chart_title} \u2014 {title}"
|
||||
|
||||
n_drugs = len(drugs)
|
||||
n_trusts = len(trusts)
|
||||
|
||||
fig.update_layout(
|
||||
title=dict(
|
||||
text=chart_title,
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif", size=16, color="#003087"),
|
||||
x=0.5, xanchor="center",
|
||||
),
|
||||
xaxis=dict(title="", tickfont=dict(size=11, color="#425563"), tickangle=-45, side="bottom"),
|
||||
yaxis=dict(title="", tickfont=dict(size=12, color="#425563"), autorange="reversed"),
|
||||
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif"),
|
||||
margin=dict(t=60, l=200, r=80, b=120),
|
||||
width=max(700, 80 + n_drugs * 55),
|
||||
height=max(300, 80 + n_trusts * 50),
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_trust_duration_figure(
|
||||
data: list[dict],
|
||||
title: str = "",
|
||||
) -> go.Figure:
|
||||
"""Create grouped horizontal bar chart showing drug durations by trust.
|
||||
|
||||
Args:
|
||||
data: List of dicts from get_trust_durations() with keys:
|
||||
drug, trust_name, avg_days, patients.
|
||||
title: Chart title suffix.
|
||||
"""
|
||||
if not data:
|
||||
return go.Figure()
|
||||
|
||||
nhs_colours = [
|
||||
"#005EB8", "#003087", "#41B6E6", "#0066CC", "#1E88E5",
|
||||
"#4FC3F7", "#009639", "#ED8B00", "#768692", "#AE2573",
|
||||
]
|
||||
|
||||
seen_drugs = []
|
||||
for d in data:
|
||||
if d["drug"] not in seen_drugs:
|
||||
seen_drugs.append(d["drug"])
|
||||
|
||||
seen_trusts = []
|
||||
for d in data:
|
||||
t = d["trust_name"]
|
||||
if t not in seen_trusts:
|
||||
seen_trusts.append(t)
|
||||
|
||||
def short_trust(name):
|
||||
return name.replace(" NHS FOUNDATION TRUST", "").replace(" HOSPITALS", "")
|
||||
|
||||
trust_colour_map = {t: nhs_colours[i % len(nhs_colours)] for i, t in enumerate(seen_trusts)}
|
||||
lookup = {(d["drug"], d["trust_name"]): d for d in data}
|
||||
|
||||
display_drugs = list(reversed(seen_drugs))
|
||||
|
||||
traces = []
|
||||
for trust in seen_trusts:
|
||||
y_vals = []
|
||||
x_vals = []
|
||||
hover_texts = []
|
||||
for drug in display_drugs:
|
||||
row = lookup.get((drug, trust))
|
||||
y_vals.append(drug)
|
||||
if row:
|
||||
years = row["avg_days"] / 365.25
|
||||
x_vals.append(row["avg_days"])
|
||||
hover_texts.append(
|
||||
f"<b>{drug}</b><br>"
|
||||
f"{short_trust(trust)}<br>"
|
||||
f"Avg duration: {row['avg_days']:,.0f} days ({years:.1f} yrs)<br>"
|
||||
f"Patients: {row['patients']:,}"
|
||||
)
|
||||
else:
|
||||
x_vals.append(0)
|
||||
hover_texts.append("")
|
||||
|
||||
traces.append(go.Bar(
|
||||
name=short_trust(trust), y=y_vals, x=x_vals, orientation="h",
|
||||
marker_color=trust_colour_map[trust],
|
||||
hovertemplate="%{customdata}<extra></extra>",
|
||||
customdata=hover_texts,
|
||||
))
|
||||
|
||||
display_title = f"Treatment Duration by Trust \u2014 {title}" if title else "Treatment Duration by Trust"
|
||||
|
||||
fig = go.Figure(data=traces)
|
||||
fig.update_layout(
|
||||
barmode="group",
|
||||
title=dict(
|
||||
text=display_title,
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif", size=16, color="#003087"),
|
||||
x=0.5, xanchor="center",
|
||||
),
|
||||
xaxis=dict(
|
||||
title="Average Duration (days)", titlefont=dict(size=13, color="#425563"),
|
||||
gridcolor="rgba(0,0,0,0.06)", zeroline=True, zerolinecolor="rgba(0,0,0,0.1)",
|
||||
),
|
||||
yaxis=dict(title="", automargin=True, tickfont=dict(size=11, color="#425563")),
|
||||
legend=dict(
|
||||
title="Trust", orientation="h", yanchor="top", y=-0.12,
|
||||
xanchor="center", x=0.5, font=dict(size=11),
|
||||
),
|
||||
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(family="Source Sans 3, system-ui, sans-serif"),
|
||||
margin=dict(t=60, l=200, r=40, b=100),
|
||||
height=max(350, len(seen_drugs) * 35 + 200),
|
||||
bargap=0.15, bargroupgap=0.05,
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
Reference in New Issue
Block a user