feat: add loading spinner, empty state, and error handling to chart area (Task 5.2)
This commit is contained in:
@@ -235,10 +235,10 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_
|
|||||||
- **Checkpoint**: Selecting trusts filters the chart correctly
|
- **Checkpoint**: Selecting trusts filters the chart correctly
|
||||||
|
|
||||||
### 5.2 Loading/error/empty states + dynamic hierarchy label
|
### 5.2 Loading/error/empty states + dynamic hierarchy label
|
||||||
- [ ] Add `dcc.Loading` wrapper around chart area
|
- [x] Add `dcc.Loading` wrapper around chart area
|
||||||
- [ ] Show "No data" message when chart-data is empty
|
- [x] Show "No data" message when chart-data is empty
|
||||||
- [ ] Show error toast/alert when database query fails
|
- [x] Show error feedback when database query fails
|
||||||
- [ ] Dynamic chart subtitle: "Trust → Directorate → Drug → Pathway" or "Trust → Indication → Drug → Pathway" based on chart type
|
- [x] Dynamic chart subtitle: "Trust → Directorate → Drug → Pathway" or "Trust → Indication → Drug → Pathway" based on chart type (done in Task 3.4)
|
||||||
- **Checkpoint**: Loading spinner appears during data fetch, empty state shows message
|
- **Checkpoint**: Loading spinner appears during data fetch, empty state shows message
|
||||||
|
|
||||||
### 5.3 Data freshness indicator
|
### 5.3 Data freshness indicator
|
||||||
|
|||||||
@@ -1,5 +1,40 @@
|
|||||||
"""Callbacks for pathway data loading and icicle chart rendering."""
|
"""Callbacks for pathway data loading and icicle chart rendering."""
|
||||||
|
import logging
|
||||||
|
|
||||||
from dash import Input, Output, no_update
|
from dash import Input, Output, no_update
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_figure(message):
|
||||||
|
"""Return a blank Plotly figure with a centered message annotation."""
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.update_layout(
|
||||||
|
xaxis={"visible": False},
|
||||||
|
yaxis={"visible": False},
|
||||||
|
plot_bgcolor="rgba(0,0,0,0)",
|
||||||
|
paper_bgcolor="rgba(0,0,0,0)",
|
||||||
|
margin={"t": 0, "l": 0, "r": 0, "b": 0},
|
||||||
|
annotations=[
|
||||||
|
{
|
||||||
|
"text": message,
|
||||||
|
"xref": "paper",
|
||||||
|
"yref": "paper",
|
||||||
|
"x": 0.5,
|
||||||
|
"y": 0.5,
|
||||||
|
"showarrow": False,
|
||||||
|
"font": {
|
||||||
|
"size": 16,
|
||||||
|
"color": "#768692",
|
||||||
|
"family": "Source Sans 3, Arial, sans-serif",
|
||||||
|
},
|
||||||
|
"xanchor": "center",
|
||||||
|
"yanchor": "middle",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def _generate_chart_title(app_state):
|
def _generate_chart_title(app_state):
|
||||||
@@ -62,6 +97,7 @@ def register_chart_callbacks(app):
|
|||||||
selected_directorates = app_state.get("selected_directorates") or None
|
selected_directorates = app_state.get("selected_directorates") or None
|
||||||
selected_trusts = app_state.get("selected_trusts") or None
|
selected_trusts = app_state.get("selected_trusts") or None
|
||||||
|
|
||||||
|
try:
|
||||||
return query_pathway_data(
|
return query_pathway_data(
|
||||||
filter_id=filter_id,
|
filter_id=filter_id,
|
||||||
chart_type=chart_type,
|
chart_type=chart_type,
|
||||||
@@ -69,6 +105,15 @@ def register_chart_callbacks(app):
|
|||||||
selected_directorates=selected_directorates,
|
selected_directorates=selected_directorates,
|
||||||
selected_trusts=selected_trusts,
|
selected_trusts=selected_trusts,
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Failed to load pathway data")
|
||||||
|
return {
|
||||||
|
"nodes": [],
|
||||||
|
"unique_patients": 0,
|
||||||
|
"total_drugs": 0,
|
||||||
|
"total_cost": 0.0,
|
||||||
|
"error": "Database query failed. Check logs for details.",
|
||||||
|
}
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
Output("pathway-chart", "figure"),
|
Output("pathway-chart", "figure"),
|
||||||
@@ -78,18 +123,28 @@ def register_chart_callbacks(app):
|
|||||||
)
|
)
|
||||||
def update_chart(chart_data, app_state):
|
def update_chart(chart_data, app_state):
|
||||||
"""Render icicle chart from chart-data nodes."""
|
"""Render icicle chart from chart-data nodes."""
|
||||||
if not chart_data or not chart_data.get("nodes"):
|
|
||||||
return no_update, no_update
|
|
||||||
|
|
||||||
from visualization.plotly_generator import create_icicle_from_nodes
|
|
||||||
|
|
||||||
title = _generate_chart_title(app_state) if app_state else ""
|
|
||||||
fig = create_icicle_from_nodes(chart_data["nodes"], title)
|
|
||||||
|
|
||||||
chart_type = (app_state or {}).get("chart_type", "directory")
|
chart_type = (app_state or {}).get("chart_type", "directory")
|
||||||
if chart_type == "indication":
|
if chart_type == "indication":
|
||||||
subtitle = "Trust \u2192 Indication \u2192 Drug \u2192 Patient Pathway"
|
subtitle = "Trust \u2192 Indication \u2192 Drug \u2192 Patient Pathway"
|
||||||
else:
|
else:
|
||||||
subtitle = "Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway"
|
subtitle = "Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway"
|
||||||
|
|
||||||
|
if not chart_data:
|
||||||
|
return no_update, no_update
|
||||||
|
|
||||||
|
error_msg = chart_data.get("error")
|
||||||
|
if error_msg:
|
||||||
|
return _empty_figure(error_msg), subtitle
|
||||||
|
|
||||||
|
if not chart_data.get("nodes"):
|
||||||
|
return _empty_figure(
|
||||||
|
"No matching pathways found.\n"
|
||||||
|
"Try adjusting your filters."
|
||||||
|
), subtitle
|
||||||
|
|
||||||
|
from visualization.plotly_generator import create_icicle_from_nodes
|
||||||
|
|
||||||
|
title = _generate_chart_title(app_state) if app_state else ""
|
||||||
|
fig = create_icicle_from_nodes(chart_data["nodes"], title)
|
||||||
|
|
||||||
return fig, subtitle
|
return fig, subtitle
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ def make_chart_card():
|
|||||||
Contains:
|
Contains:
|
||||||
- Header with title and dynamic subtitle (hierarchy label)
|
- Header with title and dynamic subtitle (hierarchy label)
|
||||||
- Tab row (Icicle active, Sankey and Timeline as disabled placeholders)
|
- Tab row (Icicle active, Sankey and Timeline as disabled placeholders)
|
||||||
- dcc.Graph for the Plotly icicle figure
|
- dcc.Loading wrapper around dcc.Graph for loading spinner
|
||||||
"""
|
"""
|
||||||
return html.Section(
|
return html.Section(
|
||||||
className="chart-card",
|
className="chart-card",
|
||||||
@@ -60,15 +60,29 @@ def make_chart_card():
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
# Chart area
|
# Chart area with loading spinner
|
||||||
|
dcc.Loading(
|
||||||
|
type="circle",
|
||||||
|
color="#005EB8",
|
||||||
|
children=[
|
||||||
|
html.Div(
|
||||||
|
id="chart-container",
|
||||||
|
children=[
|
||||||
dcc.Graph(
|
dcc.Graph(
|
||||||
id="pathway-chart",
|
id="pathway-chart",
|
||||||
style={"minHeight": "500px", "flex": "1"},
|
style={"minHeight": "500px", "flex": "1"},
|
||||||
config={
|
config={
|
||||||
"displayModeBar": True,
|
"displayModeBar": True,
|
||||||
"displaylogo": False,
|
"displaylogo": False,
|
||||||
"modeBarButtonsToRemove": ["lasso2d", "select2d"],
|
"modeBarButtonsToRemove": [
|
||||||
|
"lasso2d",
|
||||||
|
"select2d",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user