From 08aa61f85e8e97c4ad8eedb89038531443ecd65d Mon Sep 17 00:00:00 2001 From: lmiranda Date: Sun, 11 Jan 2026 22:08:49 -0500 Subject: [PATCH] fix: rename Toronto page __init__.py to dashboard.py for Dash Pages Dash Pages does not auto-discover __init__.py files as page modules. Renamed to dashboard.py so the page registers correctly at /toronto. Co-Authored-By: Claude Opus 4.5 --- portfolio_app/pages/toronto/__init__.py | 283 +---------------------- portfolio_app/pages/toronto/dashboard.py | 282 ++++++++++++++++++++++ 2 files changed, 283 insertions(+), 282 deletions(-) create mode 100644 portfolio_app/pages/toronto/dashboard.py diff --git a/portfolio_app/pages/toronto/__init__.py b/portfolio_app/pages/toronto/__init__.py index 2fb8f7d..53e3907 100644 --- a/portfolio_app/pages/toronto/__init__.py +++ b/portfolio_app/pages/toronto/__init__.py @@ -1,282 +1 @@ -"""Toronto Housing Dashboard page.""" - -import dash -import dash_mantine_components as dmc -from dash import dcc, html - -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( - [ - 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", -) +"""Toronto Housing Dashboard pages.""" diff --git a/portfolio_app/pages/toronto/dashboard.py b/portfolio_app/pages/toronto/dashboard.py new file mode 100644 index 0000000..2fb8f7d --- /dev/null +++ b/portfolio_app/pages/toronto/dashboard.py @@ -0,0 +1,282 @@ +"""Toronto Housing Dashboard page.""" + +import dash +import dash_mantine_components as dmc +from dash import dcc, html + +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( + [ + 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", +)