fix: trust palette for cost waterfall + Viridis dosing gradient (Task A.4)

This commit is contained in:
Andrew Charlwood
2026-02-07 02:49:01 +00:00
parent 060cea2a03
commit 950d93b16b
3 changed files with 19 additions and 52 deletions
+3 -3
View File
@@ -79,9 +79,9 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
### A.4 Fix trust comparison color differentiation ### A.4 Fix trust comparison color differentiation
- [x] In `create_trust_duration_figure()`: replace `nhs_colours` list with `TRUST_PALETTE` (done in A.3) - [x] In `create_trust_duration_figure()`: replace `nhs_colours` list with `TRUST_PALETTE` (done in A.3)
- [ ] Add `is_trust_comparison=False` param to `create_cost_waterfall_figure()` — use `TRUST_PALETTE` when True - [x] Add `is_trust_comparison=False` param to `create_cost_waterfall_figure()` — use `TRUST_PALETTE` when True
- [ ] Update `tc_cost_waterfall` callback in `dash_app/callbacks/trust_comparison.py` (~L165) to pass `is_trust_comparison=True` - [x] Update `tc_cost_waterfall` callback in `dash_app/callbacks/trust_comparison.py` to pass `is_trust_comparison=True`
- [ ] Fix `_dosing_by_drug()` blue→blue interpolation: replace with `plotly.colors.sample_colorscale("Viridis", ...)` for meaningful gradient - [x] Fix `_dosing_by_drug()` blue→blue interpolation: replaced with `plotly.colors.sample_colorscale("Viridis", ...)` for meaningful gradient
- [x] Replace `nhs_colours` in `create_trust_market_share_figure()` with `DRUG_PALETTE` for drug traces (done in A.3) - [x] Replace `nhs_colours` in `create_trust_market_share_figure()` with `DRUG_PALETTE` for drug traces (done in A.3)
- [x] Apply `_base_layout()` to all affected functions (done in A.3 for trust_market_share and trust_duration) - [x] Apply `_base_layout()` to all affected functions (done in A.3 for trust_market_share and trust_duration)
- **Checkpoint**: Trust Comparison charts have 7 visually distinct trust colors; dosing has meaningful gradient - **Checkpoint**: Trust Comparison charts have 7 visually distinct trust colors; dosing has meaningful gradient
+1 -1
View File
@@ -164,7 +164,7 @@ def register_trust_comparison_callbacks(app):
# Reuse existing waterfall figure — map trust_name to directory key # Reuse existing waterfall figure — map trust_name to directory key
mapped = [{"directory": d["trust_name"], "patients": d["patients"], mapped = [{"directory": d["trust_name"], "patients": d["patients"],
"total_cost": d["total_cost"], "cost_pp": d["cost_pp"]} for d in data] "total_cost": d["total_cost"], "cost_pp": d["cost_pp"]} for d in data]
return create_cost_waterfall_figure(mapped, _tc_title(app_state)) return create_cost_waterfall_figure(mapped, _tc_title(app_state), is_trust_comparison=True)
# 3. Dosing — drug dosing intervals by trust # 3. Dosing — drug dosing intervals by trust
@app.callback( @app.callback(
+15 -48
View File
@@ -632,6 +632,7 @@ def create_cost_effectiveness_figure(
def create_cost_waterfall_figure( def create_cost_waterfall_figure(
data: list[dict], data: list[dict],
title: str = "", title: str = "",
is_trust_comparison: bool = False,
) -> go.Figure: ) -> go.Figure:
"""Create waterfall chart showing cost per patient by directorate/indication. """Create waterfall chart showing cost per patient by directorate/indication.
@@ -652,15 +653,8 @@ def create_cost_waterfall_figure(
patients_list = [d["patients"] for d in data] patients_list = [d["patients"] for d in data]
total_costs = [d["total_cost"] for d in data] total_costs = [d["total_cost"] for d in data]
# NHS colour palette for bars palette = TRUST_PALETTE if is_trust_comparison else DRUG_PALETTE
nhs_colours = [ bar_colours = [palette[i % len(palette)] for i in range(len(data))]
"#005EB8", "#003087", "#41B6E6", "#0066CC", "#1E88E5",
"#4FC3F7", "#009639", "#ED8B00", "#768692", "#425563",
"#DA291C", "#7C2855",
]
# Assign colours cycling through palette
bar_colours = [nhs_colours[i % len(nhs_colours)] for i in range(len(data))]
hover_texts = [] hover_texts = []
for d in data: for d in data:
@@ -699,7 +693,7 @@ def create_cost_waterfall_figure(
text=f"n={pts:,}", text=f"n={pts:,}",
showarrow=False, showarrow=False,
yshift=-18, yshift=-18,
font=dict(size=10, color="#768692", family="Source Sans 3"), font=dict(size=10, color=ANNOTATION_COLOR, family=CHART_FONT_FAMILY),
) )
# Grand total line # Grand total line
@@ -715,7 +709,7 @@ def create_cost_waterfall_figure(
annotation_text=f"Weighted avg: £{weighted_avg:,.0f}", annotation_text=f"Weighted avg: £{weighted_avg:,.0f}",
annotation_position="top right", annotation_position="top right",
annotation_font=dict( annotation_font=dict(
size=11, color="#DA291C", family="Source Sans 3" size=11, color="#DA291C", family=CHART_FONT_FAMILY
), ),
) )
@@ -724,17 +718,8 @@ def create_cost_waterfall_figure(
else "Cost per Patient by Directorate" else "Cost per Patient by Directorate"
) )
fig.update_layout( layout = _base_layout(display_title)
title=dict( layout.update(
text=display_title,
font=dict(
family="Source Sans 3, system-ui, sans-serif",
size=18,
color="#1E293B",
),
x=0.5,
xanchor="center",
),
xaxis=dict( xaxis=dict(
title="", title="",
tickangle=-45 if len(data) > 6 else 0, tickangle=-45 if len(data) > 6 else 0,
@@ -745,30 +730,16 @@ def create_cost_waterfall_figure(
title="£ per patient", title="£ per patient",
tickprefix="£", tickprefix="£",
tickformat=",", tickformat=",",
gridcolor="#E2E8F0", gridcolor=GRID_COLOR,
zeroline=True, zeroline=True,
zerolinecolor="#CBD5E1", zerolinecolor="#CBD5E1",
), ),
margin=dict(t=60, l=8, r=24, b=40), margin=dict(t=60, l=8, r=24, b=40),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
autosize=True,
showlegend=False, showlegend=False,
hoverlabel=dict( height=500,
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(450, 500),
bargap=0.25, bargap=0.25,
) )
fig.update_layout(**layout)
return fig return fig
@@ -1005,16 +976,12 @@ def _dosing_by_drug(data: list[dict], colours: list[str]) -> go.Figure:
f"Patients: {tp:,}" f"Patients: {tp:,}"
) )
# Colour bars by interval: lower = more frequent dosing = NHS blue, higher = lighter # Colour bars by interval: lower = more frequent dosing, higher = less frequent
# Use Viridis colorscale for meaningful gradient (replaces blue→blue interpolation)
import plotly.colors as pc
max_interval = max(intervals) if intervals else 1 max_interval = max(intervals) if intervals else 1
bar_colours = [] ratios = [iv / max_interval if max_interval > 0 else 0 for iv in intervals]
for iv in intervals: bar_colours = pc.sample_colorscale("Viridis", ratios)
ratio = iv / max_interval if max_interval > 0 else 0
# Interpolate NHS blue (#005EB8) to light blue (#41B6E6)
r = int(0x00 + (0x41 - 0x00) * ratio)
g = int(0x5E + (0xB6 - 0x5E) * ratio)
b = int(0xB8 + (0xE6 - 0xB8) * ratio)
bar_colours.append(f"rgb({r},{g},{b})")
fig = go.Figure() fig = go.Figure()
fig.add_trace(go.Bar( fig.add_trace(go.Bar(