Files
personal-portfolio/portfolio_app/pages/toronto/dashboard.py
lmiranda c9cf744d84 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>
2026-01-17 11:46:18 -05:00

207 lines
6.0 KiB
Python

"""Toronto Neighbourhood Dashboard page.
Displays neighbourhood-level data across 5 tabs: Overview, Housing, Safety,
Demographics, and Amenities. Each tab provides interactive choropleth maps,
KPI cards, and supporting charts.
"""
import dash
import dash_mantine_components as dmc
from dash import dcc
from dash_iconify import DashIconify
from portfolio_app.pages.toronto.tabs import (
create_amenities_tab,
create_demographics_tab,
create_housing_tab,
create_overview_tab,
create_safety_tab,
)
dash.register_page(__name__, path="/toronto", name="Toronto Neighbourhoods")
# Tab configuration
TAB_CONFIG = [
{
"value": "overview",
"label": "Overview",
"icon": "tabler:chart-pie",
"color": "blue",
},
{
"value": "housing",
"label": "Housing",
"icon": "tabler:home",
"color": "teal",
},
{
"value": "safety",
"label": "Safety",
"icon": "tabler:shield-check",
"color": "orange",
},
{
"value": "demographics",
"label": "Demographics",
"icon": "tabler:users",
"color": "violet",
},
{
"value": "amenities",
"label": "Amenities",
"icon": "tabler:trees",
"color": "green",
},
]
def create_header() -> dmc.Group:
"""Create the dashboard header with title and controls."""
return dmc.Group(
[
dmc.Stack(
[
dmc.Title("Toronto Neighbourhood Dashboard", order=1),
dmc.Text(
"Explore livability across 158 Toronto neighbourhoods",
c="dimmed",
),
],
gap="xs",
),
dmc.Group(
[
dcc.Link(
dmc.Button(
"Methodology",
leftSection=DashIconify(
icon="tabler:info-circle", width=18
),
variant="subtle",
color="gray",
),
href="/toronto/methodology",
),
dmc.Select(
id="toronto-year-select",
data=[
{"value": "2021", "label": "2021"},
{"value": "2022", "label": "2022"},
{"value": "2023", "label": "2023"},
],
value="2021",
label="Census Year",
size="sm",
w=120,
),
],
gap="md",
),
],
justify="space-between",
align="flex-start",
)
def create_neighbourhood_selector() -> dmc.Paper:
"""Create the neighbourhood search/select component."""
return dmc.Paper(
dmc.Group(
[
DashIconify(icon="tabler:search", width=20, color="gray"),
dmc.Select(
id="toronto-neighbourhood-select",
placeholder="Search neighbourhoods...",
searchable=True,
clearable=True,
data=[], # Populated by callback
style={"flex": 1},
),
dmc.Button(
"Compare",
id="toronto-compare-btn",
leftSection=DashIconify(icon="tabler:git-compare", width=16),
variant="light",
disabled=True,
),
],
gap="sm",
),
p="sm",
radius="sm",
withBorder=True,
)
def create_tab_navigation() -> dmc.Tabs:
"""Create the tab navigation with icons."""
return dmc.Tabs(
[
dmc.TabsList(
[
dmc.TabsTab(
dmc.Group(
[
DashIconify(icon=tab["icon"], width=18),
dmc.Text(tab["label"], size="sm"),
],
gap="xs",
),
value=tab["value"],
)
for tab in TAB_CONFIG
],
grow=True,
),
# Tab panels
dmc.TabsPanel(create_overview_tab(), value="overview", pt="md"),
dmc.TabsPanel(create_housing_tab(), value="housing", pt="md"),
dmc.TabsPanel(create_safety_tab(), value="safety", pt="md"),
dmc.TabsPanel(create_demographics_tab(), value="demographics", pt="md"),
dmc.TabsPanel(create_amenities_tab(), value="amenities", pt="md"),
],
id="toronto-tabs",
value="overview",
variant="default",
)
def create_data_notice() -> dmc.Alert:
"""Create a notice about data sources."""
return dmc.Alert(
children=[
dmc.Text(
"Data from Toronto Open Data (Census 2021, Crime Statistics) and "
"CMHC Rental Market Reports. Click neighbourhoods on the map for details.",
size="sm",
),
],
title="Data Sources",
color="blue",
variant="light",
icon=DashIconify(icon="tabler:info-circle", width=20),
)
# Store for selected neighbourhood
neighbourhood_store = dcc.Store(id="toronto-selected-neighbourhood", data=None)
# Register callbacks
from portfolio_app.pages.toronto import callbacks # noqa: E402, F401
layout = dmc.Container(
dmc.Stack(
[
neighbourhood_store,
create_header(),
create_data_notice(),
create_neighbourhood_selector(),
create_tab_navigation(),
dmc.Space(h=40),
],
gap="lg",
),
size="xl",
py="xl",
)