From 8b980a755f2e82dc57f41d9cc988be299010ecc1 Mon Sep 17 00:00:00 2001 From: Andrew Charlwood Date: Sat, 7 Feb 2026 02:31:03 +0000 Subject: [PATCH] feat: add shared styling constants and _base_layout() helper (Task A.1) Add module-level constants (CHART_FONT_FAMILY, CHART_TITLE_SIZE, CHART_TITLE_COLOR, GRID_COLOR, ANNOTATION_COLOR, TRUST_PALETTE, DRUG_PALETTE) and _base_layout() helper to DRY shared layout properties across all chart functions. Apply to create_icicle_from_nodes as proof-of-concept. --- IMPLEMENTATION_PLAN.md | 6 +- progress.txt | 29 ++++++++ src/visualization/plotly_generator.py | 95 ++++++++++++++++++++++----- 3 files changed, 110 insertions(+), 20 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index b66a5fc..e1d4522 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -29,7 +29,7 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard. ## Phase A: Core Fixes + Shared Constants ### A.1 Extract shared styling constants + `_base_layout()` helper -- [ ] Add module-level constants to top of `src/visualization/plotly_generator.py`: +- [x] Add module-level constants to top of `src/visualization/plotly_generator.py`: ```python CHART_FONT_FAMILY = "Source Sans 3, system-ui, sans-serif" CHART_TITLE_SIZE = 18 @@ -48,8 +48,8 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard. "#F6B26B", "#8E7CC3", "#C27BA0", "#76A5AF", "#FFD966", ] ``` -- [ ] Create `_base_layout(title, **overrides)` helper returning a dict with shared layout properties (title font, hoverlabel, paper/plot bgcolor, autosize, font family) -- [ ] Apply `_base_layout()` to `create_icicle_from_nodes()` as a proof-of-concept (keep all existing behavior, just DRY the layout dict) +- [x] Create `_base_layout(title, **overrides)` helper returning a dict with shared layout properties (title font, hoverlabel, paper/plot bgcolor, autosize, font family) +- [x] Apply `_base_layout()` to `create_icicle_from_nodes()` as a proof-of-concept (keep all existing behavior, just DRY the layout dict) - **Checkpoint**: `python run_dash.py` starts, icicle chart unchanged visually ### A.2 Fix heatmap colorscale + cell annotations (Patient Pathways) diff --git a/progress.txt b/progress.txt index aae6d9b..b32e045 100644 --- a/progress.txt +++ b/progress.txt @@ -74,3 +74,32 @@ Working Dash application with 2 views (Patient Pathways + Trust Comparison), 13 - _dosing_by_drug interpolates from one blue to another blue ## Iteration Log + +## Iteration 1 — 2026-02-07 +### Task: A.1 — Extract shared styling constants + `_base_layout()` helper +### Why this task: +- A.1 is the foundation for all subsequent Phase A tasks (A.2-A.4 all reference `_base_layout()` and the palette constants). Must be done first. +### Status: COMPLETE +### What was done: +- Added 7 module-level constants after `logger` line: `CHART_FONT_FAMILY`, `CHART_TITLE_SIZE`, `CHART_TITLE_COLOR`, `GRID_COLOR`, `ANNOTATION_COLOR`, `TRUST_PALETTE` (7 colors), `DRUG_PALETTE` (15 colors) +- Created `_base_layout(title, **overrides)` helper returning dict with: title (font family/size/color, centered), hoverlabel (white bg, #CBD5E1 border, font), paper/plot bgcolor transparent, autosize=True, base font family +- Applied `_base_layout()` to `create_icicle_from_nodes()` — replaced 20-line explicit layout block with `_base_layout()` call + 3 overrides (margin, hoverlabel size=14, clickmode) +- Also replaced hardcoded `"Source Sans 3, system-ui, sans-serif"` in icicle textfont with `CHART_FONT_FAMILY` constant +### Validation results: +- Tier 1 (Code): all imports pass, `python run_dash.py` starts cleanly +- Tier 2 (Visual): icicle figure title correct ("Patient Pathways — By Directory"), font family/size/color all match expected values +### Files changed: +- `src/visualization/plotly_generator.py` — added constants + `_base_layout()` + refactored icicle layout +- `IMPLEMENTATION_PLAN.md` — marked A.1 subtasks [x] +### Committed: 63c1801 "feat: add shared styling constants and _base_layout() helper (Task A.1)" +### Patterns discovered: +- The `_base_layout()` returns a plain dict that gets unpacked via `fig.update_layout(**layout)`. Callers pass chart-specific overrides as kwargs. +- Icicle hoverlabel uses `size=14` (slightly larger than base `13`) — preserved as override. +- Constants are at module level, so all functions in the file can reference them directly. +- Line numbers in IMPLEMENTATION_PLAN.md are now stale (shifted ~70 lines due to constants/helper insertion). Future iterations should search by function name. +### Next iteration should: +- Start with Task A.2: Fix heatmap colorscale + cell annotations. Read `create_heatmap_figure()` and `create_trust_heatmap_figure()` functions by searching for their names (line numbers have shifted). +- The `_base_layout()` and palette constants are now available — A.2 should use `_base_layout()` for both heatmap functions. +- Key heatmap fixes: linear 5-stop colorscale, cell text annotations, zmin=0, autosize, automargin, subtitle when >25 drugs. +### Blocked items: +- None diff --git a/src/visualization/plotly_generator.py b/src/visualization/plotly_generator.py index 4b43d1e..d2b4d38 100644 --- a/src/visualization/plotly_generator.py +++ b/src/visualization/plotly_generator.py @@ -17,6 +17,78 @@ from core.logging_config import get_logger logger = get_logger(__name__) +# --------------------------------------------------------------------------- +# Shared styling constants +# --------------------------------------------------------------------------- + +CHART_FONT_FAMILY = "Source Sans 3, system-ui, sans-serif" +CHART_TITLE_SIZE = 18 +CHART_TITLE_COLOR = "#1E293B" +GRID_COLOR = "#E2E8F0" +ANNOTATION_COLOR = "#768692" + +# 7 maximally-distinct colours for trust-comparison charts +TRUST_PALETTE = [ + "#005EB8", # NHS Blue + "#DA291C", # Red + "#009639", # Green + "#ED8B00", # Orange + "#7C2855", # Plum + "#00A499", # Teal + "#330072", # Purple +] + +# 15 distinct colours for drug-level charts +DRUG_PALETTE = [ + "#005EB8", "#DA291C", "#009639", "#ED8B00", "#7C2855", + "#00A499", "#330072", "#E06666", "#6FA8DC", "#93C47D", + "#F6B26B", "#8E7CC3", "#C27BA0", "#76A5AF", "#FFD966", +] + + +def _base_layout(title: str, **overrides) -> dict: + """Return a dict of shared Plotly layout properties. + + All chart functions should call this to get consistent styling, then + update the result with chart-specific overrides. + + Args: + title: Display title for the chart. + **overrides: Any key accepted by ``fig.update_layout()``; these are + merged on top of the base dict so callers can override margins, + height, etc. + + Returns: + Dict ready to be unpacked into ``fig.update_layout(**layout)``. + """ + layout = dict( + title=dict( + text=title, + font=dict( + family=CHART_FONT_FAMILY, + size=CHART_TITLE_SIZE, + color=CHART_TITLE_COLOR, + ), + x=0.5, + xanchor="center", + ), + hoverlabel=dict( + bgcolor="#FFFFFF", + bordercolor="#CBD5E1", + font=dict( + family=CHART_FONT_FAMILY, + size=13, + color=CHART_TITLE_COLOR, + ), + ), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + autosize=True, + font=dict(family=CHART_FONT_FAMILY), + ) + layout.update(overrides) + return layout + def create_icicle_figure(ice_df: pd.DataFrame, title: str) -> go.Figure: """ @@ -204,7 +276,7 @@ def create_icicle_from_nodes(nodes: list[dict], title: str = "") -> go.Figure: "" ), textfont=dict( - family="Source Sans 3, system-ui, sans-serif", + family=CHART_FONT_FAMILY, size=12, ), ) @@ -212,32 +284,21 @@ def create_icicle_from_nodes(nodes: list[dict], title: str = "") -> go.Figure: 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", - ), + layout = _base_layout( + display_title, 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", + family=CHART_FONT_FAMILY, size=14, - color="#1E293B", + color=CHART_TITLE_COLOR, ), ), - paper_bgcolor="rgba(0,0,0,0)", - plot_bgcolor="rgba(0,0,0,0)", - autosize=True, clickmode="event+select", ) + fig.update_layout(**layout) fig.update_traces(sort=False)