feat: Complete Phase 5 dashboard implementation
Implement full 5-tab Toronto Neighbourhood Dashboard with real data connectivity: Dashboard Structure: - Overview tab with livability scores and rankings - Housing tab with affordability metrics - Safety tab with crime statistics - Demographics tab with population/income data - Amenities tab with parks, schools, transit Figure Factories (portfolio_app/figures/): - bar_charts.py: ranking, stacked, horizontal bars - scatter.py: scatter plots, bubble charts - radar.py: spider/radar charts - demographics.py: donut, age pyramid, income distribution Service Layer (portfolio_app/toronto/services/): - neighbourhood_service.py: queries dbt marts for all tab data - geometry_service.py: generates GeoJSON from PostGIS - Graceful error handling when database unavailable Callbacks (portfolio_app/pages/toronto/callbacks/): - map_callbacks.py: choropleth updates, map click handling - chart_callbacks.py: supporting chart updates - selection_callbacks.py: dropdown handlers, KPI updates Data Pipeline (scripts/data/): - load_toronto_data.py: orchestration script with CLI flags Lessons Learned: - Graceful error handling in service layers - Modular callback structure for multi-tab dashboards - Figure factory pattern for reusable charts Closes: #64, #65, #66, #67, #68, #69, #70 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
184
portfolio_app/figures/scatter.py
Normal file
184
portfolio_app/figures/scatter.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""Scatter plot figure factory for correlation views."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
|
||||
def create_scatter_figure(
|
||||
data: list[dict[str, Any]],
|
||||
x_column: str,
|
||||
y_column: str,
|
||||
name_column: str | None = None,
|
||||
size_column: str | None = None,
|
||||
color_column: str | None = None,
|
||||
title: str | None = None,
|
||||
x_title: str | None = None,
|
||||
y_title: str | None = None,
|
||||
trendline: bool = False,
|
||||
color_scale: str = "Blues",
|
||||
) -> go.Figure:
|
||||
"""Create scatter plot for correlation visualization.
|
||||
|
||||
Args:
|
||||
data: List of data records.
|
||||
x_column: Column name for x-axis values.
|
||||
y_column: Column name for y-axis values.
|
||||
name_column: Column name for point labels (hover).
|
||||
size_column: Column name for point sizes.
|
||||
color_column: Column name for color encoding.
|
||||
title: Optional chart title.
|
||||
x_title: X-axis title.
|
||||
y_title: Y-axis title.
|
||||
trendline: Whether to add OLS trendline.
|
||||
color_scale: Plotly color scale for continuous colors.
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
if not data:
|
||||
return _create_empty_figure(title or "Scatter Plot")
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Build hover_data
|
||||
hover_data = {}
|
||||
if name_column and name_column in df.columns:
|
||||
hover_data[name_column] = True
|
||||
|
||||
# Create scatter plot
|
||||
fig = px.scatter(
|
||||
df,
|
||||
x=x_column,
|
||||
y=y_column,
|
||||
size=size_column if size_column and size_column in df.columns else None,
|
||||
color=color_column if color_column and color_column in df.columns else None,
|
||||
color_continuous_scale=color_scale,
|
||||
hover_name=name_column,
|
||||
trendline="ols" if trendline else None,
|
||||
opacity=0.7,
|
||||
)
|
||||
|
||||
# Style the markers
|
||||
fig.update_traces(
|
||||
marker={
|
||||
"line": {"width": 1, "color": "rgba(255,255,255,0.3)"},
|
||||
},
|
||||
)
|
||||
|
||||
# Trendline styling
|
||||
if trendline:
|
||||
fig.update_traces(
|
||||
selector={"mode": "lines"},
|
||||
line={"color": "#FF9800", "dash": "dash", "width": 2},
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={
|
||||
"gridcolor": "rgba(128,128,128,0.2)",
|
||||
"title": x_title or x_column.replace("_", " ").title(),
|
||||
"zeroline": False,
|
||||
},
|
||||
yaxis={
|
||||
"gridcolor": "rgba(128,128,128,0.2)",
|
||||
"title": y_title or y_column.replace("_", " ").title(),
|
||||
"zeroline": False,
|
||||
},
|
||||
margin={"l": 10, "r": 10, "t": 40, "b": 10},
|
||||
showlegend=color_column is not None,
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_bubble_chart(
|
||||
data: list[dict[str, Any]],
|
||||
x_column: str,
|
||||
y_column: str,
|
||||
size_column: str,
|
||||
name_column: str | None = None,
|
||||
color_column: str | None = None,
|
||||
title: str | None = None,
|
||||
x_title: str | None = None,
|
||||
y_title: str | None = None,
|
||||
size_max: int = 50,
|
||||
) -> go.Figure:
|
||||
"""Create bubble chart with sized markers.
|
||||
|
||||
Args:
|
||||
data: List of data records.
|
||||
x_column: Column name for x-axis values.
|
||||
y_column: Column name for y-axis values.
|
||||
size_column: Column name for bubble sizes.
|
||||
name_column: Column name for labels.
|
||||
color_column: Column name for colors.
|
||||
title: Optional chart title.
|
||||
x_title: X-axis title.
|
||||
y_title: Y-axis title.
|
||||
size_max: Maximum marker size in pixels.
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
if not data:
|
||||
return _create_empty_figure(title or "Bubble Chart")
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
fig = px.scatter(
|
||||
df,
|
||||
x=x_column,
|
||||
y=y_column,
|
||||
size=size_column,
|
||||
color=color_column,
|
||||
hover_name=name_column,
|
||||
size_max=size_max,
|
||||
opacity=0.7,
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={
|
||||
"gridcolor": "rgba(128,128,128,0.2)",
|
||||
"title": x_title or x_column.replace("_", " ").title(),
|
||||
},
|
||||
yaxis={
|
||||
"gridcolor": "rgba(128,128,128,0.2)",
|
||||
"title": y_title or y_column.replace("_", " ").title(),
|
||||
},
|
||||
margin={"l": 10, "r": 10, "t": 40, "b": 10},
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def _create_empty_figure(title: str) -> go.Figure:
|
||||
"""Create an empty figure with a message."""
|
||||
fig = go.Figure()
|
||||
fig.add_annotation(
|
||||
text="No data available",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"size": 14, "color": "#888888"},
|
||||
)
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"visible": False},
|
||||
yaxis={"visible": False},
|
||||
)
|
||||
return fig
|
||||
Reference in New Issue
Block a user