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:
@@ -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
|
- **Checkpoint**: `python run_dash.py` starts, icicle chart unchanged visually
|
||||||
|
|
||||||
### A.2 Fix heatmap colorscale + cell annotations (Patient Pathways)
|
### 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]`
|
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"`)
|
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
|
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`
|
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"`
|
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
|
7. Reduce `xgap/ygap` from 2→1 when >15 columns
|
||||||
- [ ] Apply same fixes to `create_trust_heatmap_figure()` (~L1582)
|
- [x] Apply same fixes to `create_trust_heatmap_figure()` (~L1582)
|
||||||
- [ ] Apply `_base_layout()` to both heatmap functions
|
- [x] Apply `_base_layout()` to both heatmap functions
|
||||||
- **Checkpoint**: Heatmaps show linear color gradient, cell text visible, no fixed width overflow
|
- **Checkpoint**: Heatmaps show linear color gradient, cell text visible, no fixed width overflow
|
||||||
|
|
||||||
### A.3 Fix legend overflow in 4 charts
|
### A.3 Fix legend overflow in 4 charts
|
||||||
|
|||||||
@@ -1273,7 +1273,9 @@ def create_heatmap_figure(
|
|||||||
|
|
||||||
# Cap columns to top 25 drugs for readability
|
# Cap columns to top 25 drugs for readability
|
||||||
max_drugs = 25
|
max_drugs = 25
|
||||||
|
total_drug_count = len(drugs)
|
||||||
drugs = drugs[:max_drugs]
|
drugs = drugs[:max_drugs]
|
||||||
|
capped = total_drug_count > max_drugs
|
||||||
|
|
||||||
metric_labels = {
|
metric_labels = {
|
||||||
"patients": "Patients",
|
"patients": "Patients",
|
||||||
@@ -1282,13 +1284,15 @@ def create_heatmap_figure(
|
|||||||
}
|
}
|
||||||
metric_label = metric_labels.get(metric, "Patients")
|
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 = []
|
z_values = []
|
||||||
hover_texts = []
|
hover_texts = []
|
||||||
|
text_values = []
|
||||||
|
|
||||||
for d in directories:
|
for d in directories:
|
||||||
row_z = []
|
row_z = []
|
||||||
row_hover = []
|
row_hover = []
|
||||||
|
row_text = []
|
||||||
dir_data = matrix.get(d, {})
|
dir_data = matrix.get(d, {})
|
||||||
for drug in drugs:
|
for drug in drugs:
|
||||||
cell = dir_data.get(drug)
|
cell = dir_data.get(drug)
|
||||||
@@ -1305,31 +1309,45 @@ def create_heatmap_figure(
|
|||||||
f"Total cost: £{cost:,.0f}<br>"
|
f"Total cost: £{cost:,.0f}<br>"
|
||||||
f"Cost p.a.: £{cpp:,.0f}"
|
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:
|
else:
|
||||||
row_z.append(0)
|
row_z.append(0)
|
||||||
row_hover.append(
|
row_hover.append(
|
||||||
f"<b>{drug}</b><br>{d}<br>No patients"
|
f"<b>{drug}</b><br>{d}<br>No patients"
|
||||||
)
|
)
|
||||||
|
row_text.append("")
|
||||||
z_values.append(row_z)
|
z_values.append(row_z)
|
||||||
hover_texts.append(row_hover)
|
hover_texts.append(row_hover)
|
||||||
|
text_values.append(row_text)
|
||||||
|
|
||||||
# NHS blue colorscale for the heatmap
|
# Linear 5-stop NHS blue colorscale
|
||||||
colorscale = [
|
colorscale = [
|
||||||
[0.0, "#F0F4F8"],
|
[0.0, "#E3F2FD"],
|
||||||
[0.01, "#E3F2FD"],
|
[0.25, "#90CAF9"],
|
||||||
[0.1, "#90CAF9"],
|
[0.5, "#42A5F5"],
|
||||||
[0.3, "#42A5F5"],
|
[0.75, "#1E88E5"],
|
||||||
[0.5, "#1E88E5"],
|
|
||||||
[0.7, "#0066CC"],
|
|
||||||
[1.0, "#003087"],
|
[1.0, "#003087"],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
n_drugs = len(drugs)
|
||||||
|
gap = 1 if n_drugs > 15 else 2
|
||||||
|
|
||||||
fig = go.Figure(
|
fig = go.Figure(
|
||||||
data=go.Heatmap(
|
data=go.Heatmap(
|
||||||
z=z_values,
|
z=z_values,
|
||||||
x=drugs,
|
x=drugs,
|
||||||
y=directories,
|
y=directories,
|
||||||
colorscale=colorscale,
|
colorscale=colorscale,
|
||||||
|
zmin=0,
|
||||||
|
text=text_values,
|
||||||
|
texttemplate="%{text}",
|
||||||
|
textfont=dict(size=10),
|
||||||
hovertext=hover_texts,
|
hovertext=hover_texts,
|
||||||
hovertemplate="%{hovertext}<extra></extra>",
|
hovertemplate="%{hovertext}<extra></extra>",
|
||||||
colorbar=dict(
|
colorbar=dict(
|
||||||
@@ -1340,8 +1358,8 @@ def create_heatmap_figure(
|
|||||||
thickness=15,
|
thickness=15,
|
||||||
len=0.8,
|
len=0.8,
|
||||||
),
|
),
|
||||||
xgap=2,
|
xgap=gap,
|
||||||
ygap=2,
|
ygap=gap,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1349,22 +1367,11 @@ def create_heatmap_figure(
|
|||||||
if title:
|
if title:
|
||||||
chart_title = f"{chart_title} — {title}"
|
chart_title = f"{chart_title} — {title}"
|
||||||
|
|
||||||
n_drugs = len(drugs)
|
|
||||||
n_dirs = len(directories)
|
n_dirs = len(directories)
|
||||||
fig_width = max(700, 80 + n_drugs * 55)
|
|
||||||
fig_height = max(400, 80 + n_dirs * 40)
|
fig_height = max(400, 80 + n_dirs * 40)
|
||||||
|
|
||||||
fig.update_layout(
|
layout = _base_layout(chart_title)
|
||||||
title=dict(
|
layout.update(
|
||||||
text=chart_title,
|
|
||||||
font=dict(
|
|
||||||
family="Source Sans 3, system-ui, sans-serif",
|
|
||||||
size=18,
|
|
||||||
color="#003087",
|
|
||||||
),
|
|
||||||
x=0.5,
|
|
||||||
xanchor="center",
|
|
||||||
),
|
|
||||||
xaxis=dict(
|
xaxis=dict(
|
||||||
title="",
|
title="",
|
||||||
tickfont=dict(size=11, color="#425563"),
|
tickfont=dict(size=11, color="#425563"),
|
||||||
@@ -1375,14 +1382,22 @@ def create_heatmap_figure(
|
|||||||
title="",
|
title="",
|
||||||
tickfont=dict(size=12, color="#425563"),
|
tickfont=dict(size=12, color="#425563"),
|
||||||
autorange="reversed",
|
autorange="reversed",
|
||||||
|
automargin=True,
|
||||||
),
|
),
|
||||||
plot_bgcolor="rgba(0,0,0,0)",
|
margin=dict(t=60, l=8, r=80, b=120),
|
||||||
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,
|
|
||||||
height=fig_height,
|
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
|
return fig
|
||||||
|
|
||||||
@@ -1661,12 +1676,14 @@ def create_trust_heatmap_figure(
|
|||||||
if not trusts or not drugs:
|
if not trusts or not drugs:
|
||||||
return go.Figure()
|
return go.Figure()
|
||||||
|
|
||||||
|
total_drug_count = len(drugs)
|
||||||
drugs = drugs[:25]
|
drugs = drugs[:25]
|
||||||
|
capped = total_drug_count > 25
|
||||||
|
|
||||||
metric_labels = {
|
metric_labels = {
|
||||||
"patients": "Patients",
|
"patients": "Patients",
|
||||||
"cost": "Total Cost (\u00a3)",
|
"cost": "Total Cost (£)",
|
||||||
"cost_pp_pa": "Cost per Patient p.a. (\u00a3)",
|
"cost_pp_pa": "Cost per Patient p.a. (£)",
|
||||||
}
|
}
|
||||||
metric_label = metric_labels.get(metric, "Patients")
|
metric_label = metric_labels.get(metric, "Patients")
|
||||||
|
|
||||||
@@ -1675,10 +1692,12 @@ def create_trust_heatmap_figure(
|
|||||||
|
|
||||||
z_values = []
|
z_values = []
|
||||||
hover_texts = []
|
hover_texts = []
|
||||||
|
text_values = []
|
||||||
|
|
||||||
for t in trusts:
|
for t in trusts:
|
||||||
row_z = []
|
row_z = []
|
||||||
row_hover = []
|
row_hover = []
|
||||||
|
row_text = []
|
||||||
trust_data = matrix.get(t, {})
|
trust_data = matrix.get(t, {})
|
||||||
for drug in drugs:
|
for drug in drugs:
|
||||||
cell = trust_data.get(drug)
|
cell = trust_data.get(drug)
|
||||||
@@ -1692,57 +1711,77 @@ def create_trust_heatmap_figure(
|
|||||||
f"<b>{drug}</b><br>"
|
f"<b>{drug}</b><br>"
|
||||||
f"{short_trust(t)}<br>"
|
f"{short_trust(t)}<br>"
|
||||||
f"Patients: {patients:,}<br>"
|
f"Patients: {patients:,}<br>"
|
||||||
f"Total cost: \u00a3{cost:,.0f}<br>"
|
f"Total cost: £{cost:,.0f}<br>"
|
||||||
f"Cost p.a.: \u00a3{cpp:,.0f}"
|
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:
|
else:
|
||||||
row_z.append(0)
|
row_z.append(0)
|
||||||
row_hover.append(f"<b>{drug}</b><br>{short_trust(t)}<br>No patients")
|
row_hover.append(f"<b>{drug}</b><br>{short_trust(t)}<br>No patients")
|
||||||
|
row_text.append("")
|
||||||
z_values.append(row_z)
|
z_values.append(row_z)
|
||||||
hover_texts.append(row_hover)
|
hover_texts.append(row_hover)
|
||||||
|
text_values.append(row_text)
|
||||||
|
|
||||||
|
# Linear 5-stop NHS blue colorscale
|
||||||
colorscale = [
|
colorscale = [
|
||||||
[0.0, "#F0F4F8"], [0.01, "#E3F2FD"], [0.1, "#90CAF9"],
|
[0.0, "#E3F2FD"],
|
||||||
[0.3, "#42A5F5"], [0.5, "#1E88E5"], [0.7, "#0066CC"], [1.0, "#003087"],
|
[0.25, "#90CAF9"],
|
||||||
|
[0.5, "#42A5F5"],
|
||||||
|
[0.75, "#1E88E5"],
|
||||||
|
[1.0, "#003087"],
|
||||||
]
|
]
|
||||||
|
|
||||||
display_trusts = [short_trust(t) for t in trusts]
|
display_trusts = [short_trust(t) for t in trusts]
|
||||||
|
n_drugs = len(drugs)
|
||||||
|
gap = 1 if n_drugs > 15 else 2
|
||||||
|
|
||||||
fig = go.Figure(
|
fig = go.Figure(
|
||||||
data=go.Heatmap(
|
data=go.Heatmap(
|
||||||
z=z_values, x=drugs, y=display_trusts,
|
z=z_values, x=drugs, y=display_trusts,
|
||||||
colorscale=colorscale,
|
colorscale=colorscale,
|
||||||
|
zmin=0,
|
||||||
|
text=text_values,
|
||||||
|
texttemplate="%{text}",
|
||||||
|
textfont=dict(size=10),
|
||||||
hovertext=hover_texts,
|
hovertext=hover_texts,
|
||||||
hovertemplate="%{hovertext}<extra></extra>",
|
hovertemplate="%{hovertext}<extra></extra>",
|
||||||
colorbar=dict(
|
colorbar=dict(
|
||||||
title=dict(text=metric_label, font=dict(size=12, color="#425563")),
|
title=dict(text=metric_label, font=dict(size=12, color="#425563")),
|
||||||
thickness=15, len=0.8,
|
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:
|
if title:
|
||||||
chart_title = f"{chart_title} \u2014 {title}"
|
chart_title = f"{chart_title} — {title}"
|
||||||
|
|
||||||
n_drugs = len(drugs)
|
|
||||||
n_trusts = len(trusts)
|
n_trusts = len(trusts)
|
||||||
|
|
||||||
fig.update_layout(
|
layout = _base_layout(chart_title)
|
||||||
title=dict(
|
layout.update(
|
||||||
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"),
|
xaxis=dict(title="", tickfont=dict(size=11, color="#425563"), tickangle=-45, side="bottom"),
|
||||||
yaxis=dict(title="", tickfont=dict(size=12, color="#425563"), autorange="reversed"),
|
yaxis=dict(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)",
|
margin=dict(t=60, l=8, r=80, b=120),
|
||||||
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),
|
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
|
return fig
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user