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>
207 lines
6.0 KiB
Python
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",
|
|
)
|