Files
personal-portfolio/portfolio_app/pages/toronto/tabs/housing.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

210 lines
9.0 KiB
Python

"""Housing tab for Toronto Neighbourhood Dashboard.
Displays affordability metrics, rent trends, and housing indicators.
"""
import dash_mantine_components as dmc
from dash import dcc
def create_housing_tab() -> dmc.Stack:
"""Create the Housing tab layout.
Layout:
- Choropleth map (affordability index) | KPI cards
- Rent trend line chart | Dwelling types breakdown
Returns:
Tab content as a Mantine Stack component.
"""
return dmc.Stack(
[
# Main content: Map + KPIs
dmc.Grid(
[
# Choropleth map
dmc.GridCol(
dmc.Paper(
[
dmc.Group(
[
dmc.Title(
"Housing Affordability",
order=4,
size="h5",
),
dmc.Select(
id="housing-metric-select",
data=[
{
"value": "affordability_index",
"label": "Affordability Index",
},
{
"value": "avg_rent_2bed",
"label": "Avg Rent (2BR)",
},
{
"value": "rent_to_income_pct",
"label": "Rent-to-Income %",
},
{
"value": "vacancy_rate",
"label": "Vacancy Rate",
},
],
value="affordability_index",
size="sm",
w=180,
),
],
justify="space-between",
mb="sm",
),
dcc.Graph(
id="housing-choropleth",
config={
"scrollZoom": True,
"displayModeBar": False,
},
style={"height": "450px"},
),
],
p="md",
radius="sm",
withBorder=True,
),
span={"base": 12, "lg": 8},
),
# KPI cards
dmc.GridCol(
dmc.Stack(
[
dmc.Paper(
[
dmc.Text(
"City Avg 2BR Rent", size="xs", c="dimmed"
),
dmc.Title(
id="housing-city-rent",
children="$2,450",
order=2,
),
dmc.Text(
id="housing-rent-change",
children="+4.2% YoY",
size="sm",
c="red",
),
],
p="md",
radius="sm",
withBorder=True,
),
dmc.Paper(
[
dmc.Text(
"City Avg Vacancy", size="xs", c="dimmed"
),
dmc.Title(
id="housing-city-vacancy",
children="1.8%",
order=2,
),
dmc.Text(
"Below healthy rate (3%)",
size="sm",
c="orange",
),
],
p="md",
radius="sm",
withBorder=True,
),
dmc.Paper(
[
dmc.Text(
"Selected Neighbourhood",
size="xs",
c="dimmed",
),
dmc.Title(
id="housing-selected-name",
children="Click map to select",
order=4,
size="h5",
),
dmc.Stack(
id="housing-selected-details",
children=[
dmc.Text("", c="dimmed"),
],
gap="xs",
),
],
p="md",
radius="sm",
withBorder=True,
),
],
gap="md",
),
span={"base": 12, "lg": 4},
),
],
gutter="md",
),
# Supporting charts
dmc.Grid(
[
# Rent trend
dmc.GridCol(
dmc.Paper(
[
dmc.Title(
"Rent Trends (5 Year)",
order=4,
size="h5",
mb="sm",
),
dcc.Graph(
id="housing-trend-chart",
config={"displayModeBar": False},
style={"height": "300px"},
),
],
p="md",
radius="sm",
withBorder=True,
),
span={"base": 12, "md": 6},
),
# Dwelling types
dmc.GridCol(
dmc.Paper(
[
dmc.Title(
"Dwelling Types",
order=4,
size="h5",
mb="sm",
),
dcc.Graph(
id="housing-types-chart",
config={"displayModeBar": False},
style={"height": "300px"},
),
],
p="md",
radius="sm",
withBorder=True,
),
span={"base": 12, "md": 6},
),
],
gutter="md",
),
],
gap="md",
)