feat: add icicle chart rendering with NHS colorscale and dynamic titles (Task 3.4)

- Add create_icicle_from_nodes() to src/visualization/plotly_generator.py
  accepting list-of-dicts from dcc.Store with NHS blue gradient colorscale,
  10-field customdata, and matching text/hover templates from Reflex version
- Add update_chart callback to dash_app/callbacks/chart.py rendering
  go.Icicle figure from chart-data store with dynamic subtitle
- Title generation helper mirrors Reflex _generate_pathway_chart_title()
This commit is contained in:
Andrew Charlwood
2026-02-06 13:44:13 +00:00
parent ac731780b5
commit 40ce7fc5f9
3 changed files with 196 additions and 5 deletions
+134
View File
@@ -110,6 +110,140 @@ def create_icicle_figure(ice_df: pd.DataFrame, title: str) -> go.Figure:
return fig
def create_icicle_from_nodes(nodes: list[dict], title: str = "") -> go.Figure:
"""
Create Plotly icicle figure from a list of pathway node dicts.
This is the dict-based entry point used by the Dash app. The nodes list
comes directly from the chart-data dcc.Store (JSON-serialized dicts with
underscore keys matching SQLite column names).
Args:
nodes: List of dicts with keys: parents, ids, labels, value, cost,
costpp, cost_pp_pa, colour, first_seen, last_seen,
first_seen_parent, last_seen_parent, average_spacing
title: Chart title (e.g. "By Directory | All years / Last 6 months")
Returns:
Plotly Figure object ready for dcc.Graph
"""
if not nodes:
return go.Figure()
parents = [d.get("parents", "") for d in nodes]
ids = [d.get("ids", "") for d in nodes]
labels = [d.get("labels", "") for d in nodes]
values = [d.get("value", 0) for d in nodes]
colours = [d.get("colour", 0.0) for d in nodes]
costs = [d.get("cost", 0.0) for d in nodes]
costpp = [d.get("costpp", 0.0) for d in nodes]
first_seen = [d.get("first_seen", "N/A") or "N/A" for d in nodes]
last_seen = [d.get("last_seen", "N/A") or "N/A" for d in nodes]
first_seen_parent = [d.get("first_seen_parent", "N/A") or "N/A" for d in nodes]
last_seen_parent = [d.get("last_seen_parent", "N/A") or "N/A" for d in nodes]
average_spacing = [d.get("average_spacing", "") or "" for d in nodes]
cost_pp_pa = [d.get("cost_pp_pa", 0.0) or 0.0 for d in nodes]
customdata = list(zip(
values, # [0]
colours, # [1]
costs, # [2]
costpp, # [3]
first_seen, # [4]
last_seen, # [5]
first_seen_parent, # [6]
last_seen_parent, # [7]
average_spacing, # [8]
cost_pp_pa, # [9]
))
# NHS blue gradient (Heritage Blue → Primary Blue → Vibrant Blue → Sky Blue → Pale Blue)
colorscale = [
[0.0, "#003087"],
[0.25, "#0066CC"],
[0.5, "#1E88E5"],
[0.75, "#4FC3F7"],
[1.0, "#E3F2FD"],
]
fig = go.Figure(
go.Icicle(
labels=labels,
ids=ids,
parents=parents,
values=values,
branchvalues="total",
marker=dict(
colors=colours,
colorscale=colorscale,
line=dict(width=1, color="#FFFFFF"),
),
maxdepth=3,
customdata=customdata,
texttemplate=(
"<b>%{label}</b> "
"<br><b>Total patients:</b> %{customdata[0]} (including children/further treatments)"
"<br><b>First seen:</b> %{customdata[4]}"
"<br><b>Last seen (including further treatments):</b> %{customdata[7]}"
"<br><b>Average treatment duration:</b> %{customdata[8]}"
"<br><b>Total cost:</b> \u00a3%{customdata[2]:.3~s}"
"<br><b>Average cost per patient:</b> \u00a3%{customdata[3]:.3~s}"
"<br><b>Average cost per patient per annum:</b> \u00a3%{customdata[9]:.3~s}"
),
hovertemplate=(
"<b>%{label}</b>"
"<br><b>Total patients:</b> %{customdata[0]} - %{customdata[1]:.3p} of patients in level"
"<br><b>Total cost:</b> \u00a3%{customdata[2]:.3~s}"
"<br><b>Average cost per patient:</b> \u00a3%{customdata[3]:.3~s}"
"<br><b>Average cost per patient per annum:</b> \u00a3%{customdata[9]:.3~s}"
"<br><b>First seen:</b> %{customdata[4]}"
"<br><b>Last seen (including further treatments):</b> %{customdata[7]}"
"<br><b>Average treatment duration:</b>"
"%{customdata[8]}"
"<extra></extra>"
),
textfont=dict(
family="Source Sans 3, system-ui, sans-serif",
size=12,
),
)
)
display_title = f"Patient Pathways \u2014 {title}" if title else "Patient Pathways"
fig.update_layout(
title=dict(
text=display_title,
font=dict(
family="Source Sans 3, system-ui, sans-serif",
size=18,
color="#1E293B",
),
x=0.5,
xanchor="center",
),
margin=dict(t=40, l=8, r=8, b=24),
hoverlabel=dict(
bgcolor="#FFFFFF",
bordercolor="#CBD5E1",
font=dict(
family="Source Sans 3, system-ui, sans-serif",
size=14,
color="#1E293B",
),
),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
autosize=True,
clickmode="event+select",
)
fig.update_traces(sort=False)
return fig
def save_figure_html(
fig: go.Figure, save_dir: str, title: str, open_browser: bool = False
) -> str: