fix: heatmap linear colorscale, cell annotations, autosize (Task A.2)

- Replace non-linear 7-stop colorscale with linear 5-stop in both
  create_heatmap_figure() and create_trust_heatmap_figure()
- Add cell text annotations formatted per metric (patients: N, cost: £Nk, cost_pp_pa: £N)
- Set zmin=0 explicitly for correct color mapping
- Remove fixed width, use autosize=True via _base_layout()
- Replace l=200 fixed margin with l=8 + yaxis automargin=True
- Add subtitle annotation when 25-drug cap is reached
- Reduce xgap/ygap from 2 to 1 when >15 drug columns
- Apply _base_layout() to both heatmap functions for consistent styling
This commit is contained in:
Andrew Charlwood
2026-02-07 02:36:02 +00:00
parent 8b980a755f
commit 56ca11ea30
2 changed files with 91 additions and 52 deletions
+3 -3
View File
@@ -53,7 +53,7 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
- **Checkpoint**: `python run_dash.py` starts, icicle chart unchanged visually
### A.2 Fix heatmap colorscale + cell annotations (Patient Pathways)
- [ ] In `create_heatmap_figure()` (~L1189):
- [x] In `create_heatmap_figure()` (~L1189):
1. Replace non-linear colorscale with linear 5-stop: `[0.0 #E3F2FD, 0.25 #90CAF9, 0.5 #42A5F5, 0.75 #1E88E5, 1.0 #003087]`
2. Add `text=text_values, texttemplate="%{text}"` with formatted values per metric (patients: `"N"`, cost: `"£Nk"`, cost_pp_pa: `"£N"`)
3. Set `zmin=0` explicitly
@@ -61,8 +61,8 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
5. Replace `l=200` with `l=8` + `yaxis automargin=True`
6. Add subtitle annotation when 25-drug cap is hit: `"Showing top 25 of N drugs"`
7. Reduce `xgap/ygap` from 2→1 when >15 columns
- [ ] Apply same fixes to `create_trust_heatmap_figure()` (~L1582)
- [ ] Apply `_base_layout()` to both heatmap functions
- [x] Apply same fixes to `create_trust_heatmap_figure()` (~L1582)
- [x] Apply `_base_layout()` to both heatmap functions
- **Checkpoint**: Heatmaps show linear color gradient, cell text visible, no fixed width overflow
### A.3 Fix legend overflow in 4 charts
+88 -49
View File
@@ -1273,7 +1273,9 @@ def create_heatmap_figure(
# Cap columns to top 25 drugs for readability
max_drugs = 25
total_drug_count = len(drugs)
drugs = drugs[:max_drugs]
capped = total_drug_count > max_drugs
metric_labels = {
"patients": "Patients",
@@ -1282,13 +1284,15 @@ def create_heatmap_figure(
}
metric_label = metric_labels.get(metric, "Patients")
# Build 2D arrays for z-values and hover text
# Build 2D arrays for z-values, hover text, and cell annotations
z_values = []
hover_texts = []
text_values = []
for d in directories:
row_z = []
row_hover = []
row_text = []
dir_data = matrix.get(d, {})
for drug in drugs:
cell = dir_data.get(drug)
@@ -1305,31 +1309,45 @@ def create_heatmap_figure(
f"Total cost: £{cost:,.0f}<br>"
f"Cost p.a.: £{cpp:,.0f}"
)
# Cell annotation text, formatted per metric
if metric == "cost":
row_text.append(f"£{cost / 1000:.0f}k" if cost >= 1000 else f"£{cost:.0f}")
elif metric == "cost_pp_pa":
row_text.append(f"£{cpp:,.0f}")
else:
row_text.append(f"{patients:,}")
else:
row_z.append(0)
row_hover.append(
f"<b>{drug}</b><br>{d}<br>No patients"
)
row_text.append("")
z_values.append(row_z)
hover_texts.append(row_hover)
text_values.append(row_text)
# NHS blue colorscale for the heatmap
# Linear 5-stop NHS blue colorscale
colorscale = [
[0.0, "#F0F4F8"],
[0.01, "#E3F2FD"],
[0.1, "#90CAF9"],
[0.3, "#42A5F5"],
[0.5, "#1E88E5"],
[0.7, "#0066CC"],
[0.0, "#E3F2FD"],
[0.25, "#90CAF9"],
[0.5, "#42A5F5"],
[0.75, "#1E88E5"],
[1.0, "#003087"],
]
n_drugs = len(drugs)
gap = 1 if n_drugs > 15 else 2
fig = go.Figure(
data=go.Heatmap(
z=z_values,
x=drugs,
y=directories,
colorscale=colorscale,
zmin=0,
text=text_values,
texttemplate="%{text}",
textfont=dict(size=10),
hovertext=hover_texts,
hovertemplate="%{hovertext}<extra></extra>",
colorbar=dict(
@@ -1340,8 +1358,8 @@ def create_heatmap_figure(
thickness=15,
len=0.8,
),
xgap=2,
ygap=2,
xgap=gap,
ygap=gap,
)
)
@@ -1349,22 +1367,11 @@ def create_heatmap_figure(
if title:
chart_title = f"{chart_title}{title}"
n_drugs = len(drugs)
n_dirs = len(directories)
fig_width = max(700, 80 + n_drugs * 55)
fig_height = max(400, 80 + n_dirs * 40)
fig.update_layout(
title=dict(
text=chart_title,
font=dict(
family="Source Sans 3, system-ui, sans-serif",
size=18,
color="#003087",
),
x=0.5,
xanchor="center",
),
layout = _base_layout(chart_title)
layout.update(
xaxis=dict(
title="",
tickfont=dict(size=11, color="#425563"),
@@ -1375,14 +1382,22 @@ def create_heatmap_figure(
title="",
tickfont=dict(size=12, color="#425563"),
autorange="reversed",
automargin=True,
),
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=fig_width,
margin=dict(t=60, l=8, r=80, b=120),
height=fig_height,
)
fig.update_layout(**layout)
# Add subtitle when drug cap is reached
if capped:
fig.add_annotation(
text=f"Showing top {max_drugs} of {total_drug_count} drugs",
xref="paper", yref="paper",
x=0.5, y=1.02,
showarrow=False,
font=dict(size=12, color=ANNOTATION_COLOR),
)
return fig
@@ -1661,12 +1676,14 @@ def create_trust_heatmap_figure(
if not trusts or not drugs:
return go.Figure()
total_drug_count = len(drugs)
drugs = drugs[:25]
capped = total_drug_count > 25
metric_labels = {
"patients": "Patients",
"cost": "Total Cost (\u00a3)",
"cost_pp_pa": "Cost per Patient p.a. (\u00a3)",
"cost": "Total Cost (£)",
"cost_pp_pa": "Cost per Patient p.a. (£)",
}
metric_label = metric_labels.get(metric, "Patients")
@@ -1675,10 +1692,12 @@ def create_trust_heatmap_figure(
z_values = []
hover_texts = []
text_values = []
for t in trusts:
row_z = []
row_hover = []
row_text = []
trust_data = matrix.get(t, {})
for drug in drugs:
cell = trust_data.get(drug)
@@ -1692,57 +1711,77 @@ def create_trust_heatmap_figure(
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}"
f"Total cost: £{cost:,.0f}<br>"
f"Cost p.a.: £{cpp:,.0f}"
)
if metric == "cost":
row_text.append(f"£{cost / 1000:.0f}k" if cost >= 1000 else f"£{cost:.0f}")
elif metric == "cost_pp_pa":
row_text.append(f"£{cpp:,.0f}")
else:
row_text.append(f"{patients:,}")
else:
row_z.append(0)
row_hover.append(f"<b>{drug}</b><br>{short_trust(t)}<br>No patients")
row_text.append("")
z_values.append(row_z)
hover_texts.append(row_hover)
text_values.append(row_text)
# Linear 5-stop NHS blue colorscale
colorscale = [
[0.0, "#F0F4F8"], [0.01, "#E3F2FD"], [0.1, "#90CAF9"],
[0.3, "#42A5F5"], [0.5, "#1E88E5"], [0.7, "#0066CC"], [1.0, "#003087"],
[0.0, "#E3F2FD"],
[0.25, "#90CAF9"],
[0.5, "#42A5F5"],
[0.75, "#1E88E5"],
[1.0, "#003087"],
]
display_trusts = [short_trust(t) for t in trusts]
n_drugs = len(drugs)
gap = 1 if n_drugs > 15 else 2
fig = go.Figure(
data=go.Heatmap(
z=z_values, x=drugs, y=display_trusts,
colorscale=colorscale,
zmin=0,
text=text_values,
texttemplate="%{text}",
textfont=dict(size=10),
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,
xgap=gap, ygap=gap,
)
)
chart_title = f"Trust \u00d7 Drug \u2014 {metric_label}"
chart_title = f"Trust × Drug — {metric_label}"
if title:
chart_title = f"{chart_title} \u2014 {title}"
chart_title = f"{chart_title} {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",
),
layout = _base_layout(chart_title)
layout.update(
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),
yaxis=dict(title="", tickfont=dict(size=12, color="#425563"), autorange="reversed", automargin=True),
margin=dict(t=60, l=8, r=80, b=120),
height=max(300, 80 + n_trusts * 50),
)
fig.update_layout(**layout)
if capped:
fig.add_annotation(
text=f"Showing top 25 of {total_drug_count} drugs",
xref="paper", yref="paper",
x=0.5, y=1.02,
showarrow=False,
font=dict(size=12, color=ANNOTATION_COLOR),
)
return fig