- Add floating pill-shaped sidebar with navigation icons - Implement dark/light theme toggle with localStorage persistence - Update all figure factories for transparent backgrounds - Use carto-darkmatter map style for choropleths - Add methodology link button to Toronto dashboard header - Add back to dashboard button on methodology page - Remove social links from home page (now in sidebar) - Update CLAUDE.md to Sprint 7 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
295 lines
8.5 KiB
Python
295 lines
8.5 KiB
Python
"""Toronto Housing Dashboard page."""
|
|
|
|
import dash
|
|
import dash_mantine_components as dmc
|
|
from dash import dcc, html
|
|
from dash_iconify import DashIconify
|
|
|
|
from portfolio_app.components import (
|
|
create_map_controls,
|
|
create_metric_cards_row,
|
|
create_time_slider,
|
|
create_year_selector,
|
|
)
|
|
|
|
dash.register_page(__name__, path="/toronto", name="Toronto Housing")
|
|
|
|
# Metric options for the purchase market
|
|
PURCHASE_METRIC_OPTIONS = [
|
|
{"label": "Average Price", "value": "avg_price"},
|
|
{"label": "Median Price", "value": "median_price"},
|
|
{"label": "Sales Volume", "value": "sales_count"},
|
|
{"label": "Days on Market", "value": "avg_dom"},
|
|
]
|
|
|
|
# Metric options for the rental market
|
|
RENTAL_METRIC_OPTIONS = [
|
|
{"label": "Average Rent", "value": "avg_rent"},
|
|
{"label": "Vacancy Rate", "value": "vacancy_rate"},
|
|
{"label": "Rental Universe", "value": "rental_universe"},
|
|
]
|
|
|
|
# Sample metrics for KPI cards (will be populated by callbacks)
|
|
SAMPLE_METRICS = [
|
|
{
|
|
"title": "Avg. Price",
|
|
"value": 1125000,
|
|
"delta": 2.3,
|
|
"prefix": "$",
|
|
"format_spec": ",.0f",
|
|
},
|
|
{
|
|
"title": "Sales Volume",
|
|
"value": 4850,
|
|
"delta": -5.1,
|
|
"format_spec": ",",
|
|
},
|
|
{
|
|
"title": "Avg. DOM",
|
|
"value": 18,
|
|
"delta": 3,
|
|
"suffix": " days",
|
|
"positive_is_good": False,
|
|
},
|
|
{
|
|
"title": "Avg. Rent",
|
|
"value": 2450,
|
|
"delta": 4.2,
|
|
"prefix": "$",
|
|
"format_spec": ",.0f",
|
|
},
|
|
]
|
|
|
|
|
|
def create_header() -> dmc.Group:
|
|
"""Create the dashboard header with title and controls."""
|
|
return dmc.Group(
|
|
[
|
|
dmc.Stack(
|
|
[
|
|
dmc.Title("Toronto Housing Dashboard", order=1),
|
|
dmc.Text(
|
|
"Real estate market analysis for the Greater Toronto Area",
|
|
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",
|
|
),
|
|
create_year_selector(
|
|
id_prefix="toronto",
|
|
min_year=2020,
|
|
default_year=2024,
|
|
label="Year",
|
|
),
|
|
],
|
|
gap="md",
|
|
),
|
|
],
|
|
justify="space-between",
|
|
align="flex-start",
|
|
)
|
|
|
|
|
|
def create_kpi_section() -> dmc.Box:
|
|
"""Create the KPI metrics row."""
|
|
return dmc.Box(
|
|
children=[
|
|
dmc.Title("Key Metrics", order=3, size="h4", mb="sm"),
|
|
html.Div(
|
|
id="toronto-kpi-cards",
|
|
children=[
|
|
create_metric_cards_row(SAMPLE_METRICS, id_prefix="toronto-kpi")
|
|
],
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def create_purchase_map_section() -> dmc.Grid:
|
|
"""Create the purchase market choropleth section."""
|
|
return dmc.Grid(
|
|
[
|
|
dmc.GridCol(
|
|
create_map_controls(
|
|
id_prefix="purchase-map",
|
|
metric_options=PURCHASE_METRIC_OPTIONS,
|
|
default_metric="avg_price",
|
|
),
|
|
span={"base": 12, "md": 3},
|
|
),
|
|
dmc.GridCol(
|
|
dmc.Paper(
|
|
children=[
|
|
dcc.Graph(
|
|
id="purchase-choropleth",
|
|
config={"scrollZoom": True},
|
|
style={"height": "500px"},
|
|
),
|
|
],
|
|
p="xs",
|
|
radius="sm",
|
|
withBorder=True,
|
|
),
|
|
span={"base": 12, "md": 9},
|
|
),
|
|
],
|
|
gutter="md",
|
|
)
|
|
|
|
|
|
def create_rental_map_section() -> dmc.Grid:
|
|
"""Create the rental market choropleth section."""
|
|
return dmc.Grid(
|
|
[
|
|
dmc.GridCol(
|
|
create_map_controls(
|
|
id_prefix="rental-map",
|
|
metric_options=RENTAL_METRIC_OPTIONS,
|
|
default_metric="avg_rent",
|
|
),
|
|
span={"base": 12, "md": 3},
|
|
),
|
|
dmc.GridCol(
|
|
dmc.Paper(
|
|
children=[
|
|
dcc.Graph(
|
|
id="rental-choropleth",
|
|
config={"scrollZoom": True},
|
|
style={"height": "500px"},
|
|
),
|
|
],
|
|
p="xs",
|
|
radius="sm",
|
|
withBorder=True,
|
|
),
|
|
span={"base": 12, "md": 9},
|
|
),
|
|
],
|
|
gutter="md",
|
|
)
|
|
|
|
|
|
def create_time_series_section() -> dmc.Grid:
|
|
"""Create the time series charts section."""
|
|
return dmc.Grid(
|
|
[
|
|
dmc.GridCol(
|
|
dmc.Paper(
|
|
children=[
|
|
dmc.Title("Price Trends", order=4, size="h5", mb="sm"),
|
|
dcc.Graph(
|
|
id="price-time-series",
|
|
config={"displayModeBar": False},
|
|
style={"height": "350px"},
|
|
),
|
|
],
|
|
p="md",
|
|
radius="sm",
|
|
withBorder=True,
|
|
),
|
|
span={"base": 12, "md": 6},
|
|
),
|
|
dmc.GridCol(
|
|
dmc.Paper(
|
|
children=[
|
|
dmc.Title("Sales Volume", order=4, size="h5", mb="sm"),
|
|
dcc.Graph(
|
|
id="volume-time-series",
|
|
config={"displayModeBar": False},
|
|
style={"height": "350px"},
|
|
),
|
|
],
|
|
p="md",
|
|
radius="sm",
|
|
withBorder=True,
|
|
),
|
|
span={"base": 12, "md": 6},
|
|
),
|
|
],
|
|
gutter="md",
|
|
)
|
|
|
|
|
|
def create_market_comparison_section() -> dmc.Paper:
|
|
"""Create the market comparison chart section."""
|
|
return dmc.Paper(
|
|
children=[
|
|
dmc.Group(
|
|
[
|
|
dmc.Title("Market Indicators", order=4, size="h5"),
|
|
create_time_slider(
|
|
id_prefix="market-comparison",
|
|
min_year=2020,
|
|
label="",
|
|
),
|
|
],
|
|
justify="space-between",
|
|
align="center",
|
|
mb="md",
|
|
),
|
|
dcc.Graph(
|
|
id="market-comparison-chart",
|
|
config={"displayModeBar": False},
|
|
style={"height": "400px"},
|
|
),
|
|
],
|
|
p="md",
|
|
radius="sm",
|
|
withBorder=True,
|
|
)
|
|
|
|
|
|
def create_data_notice() -> dmc.Alert:
|
|
"""Create a notice about data availability."""
|
|
return dmc.Alert(
|
|
children=[
|
|
dmc.Text(
|
|
"This dashboard uses TRREB and CMHC data. "
|
|
"Geographic boundaries require QGIS digitization to enable choropleth maps. "
|
|
"Sample data is shown below.",
|
|
size="sm",
|
|
),
|
|
],
|
|
title="Data Notice",
|
|
color="blue",
|
|
variant="light",
|
|
)
|
|
|
|
|
|
# Register callbacks
|
|
from portfolio_app.pages.toronto import callbacks # noqa: E402, F401
|
|
|
|
layout = dmc.Container(
|
|
dmc.Stack(
|
|
[
|
|
create_header(),
|
|
create_data_notice(),
|
|
create_kpi_section(),
|
|
dmc.Divider(my="md", label="Purchase Market", labelPosition="center"),
|
|
create_purchase_map_section(),
|
|
dmc.Divider(my="md", label="Rental Market", labelPosition="center"),
|
|
create_rental_map_section(),
|
|
dmc.Divider(my="md", label="Trends", labelPosition="center"),
|
|
create_time_series_section(),
|
|
create_market_comparison_section(),
|
|
dmc.Space(h=40),
|
|
],
|
|
gap="lg",
|
|
),
|
|
size="xl",
|
|
py="xl",
|
|
)
|