refactor: multi-dashboard structural migration
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
- Rename dbt project from toronto_housing to portfolio - Restructure dbt models into domain subdirectories: - shared/ for cross-domain dimensions (dim_time) - staging/toronto/, intermediate/toronto/, marts/toronto/ - Update SQLAlchemy models for raw_toronto schema - Add explicit cross-schema FK relationships for FactRentals - Namespace figure factories under figures/toronto/ - Namespace notebooks under notebooks/toronto/ - Update Makefile with domain-specific targets and env loading - Update all documentation for multi-dashboard structure This enables adding new dashboard projects (e.g., /football, /energy) without structural conflicts or naming collisions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Any
|
||||
import dash_mantine_components as dmc
|
||||
from dash import dcc
|
||||
|
||||
from portfolio_app.figures.summary_cards import create_metric_card_figure
|
||||
from portfolio_app.figures.toronto.summary_cards import create_metric_card_figure
|
||||
|
||||
|
||||
class MetricCard:
|
||||
|
||||
@@ -1,61 +1,15 @@
|
||||
"""Plotly figure factories for data visualization."""
|
||||
"""Plotly figure factories for data visualization.
|
||||
|
||||
from .bar_charts import (
|
||||
create_horizontal_bar,
|
||||
create_ranking_bar,
|
||||
create_stacked_bar,
|
||||
)
|
||||
from .choropleth import (
|
||||
create_choropleth_figure,
|
||||
create_zone_map,
|
||||
)
|
||||
from .demographics import (
|
||||
create_age_pyramid,
|
||||
create_donut_chart,
|
||||
create_income_distribution,
|
||||
)
|
||||
from .radar import (
|
||||
create_comparison_radar,
|
||||
create_radar_figure,
|
||||
)
|
||||
from .scatter import (
|
||||
create_bubble_chart,
|
||||
create_scatter_figure,
|
||||
)
|
||||
from .summary_cards import create_metric_card_figure, create_summary_metrics
|
||||
from .time_series import (
|
||||
add_policy_markers,
|
||||
create_market_comparison_chart,
|
||||
create_price_time_series,
|
||||
create_time_series_with_events,
|
||||
create_volume_time_series,
|
||||
)
|
||||
Figure factories are organized by dashboard domain:
|
||||
- toronto/ : Toronto Neighbourhood Dashboard figures
|
||||
|
||||
Usage:
|
||||
from portfolio_app.figures.toronto import create_choropleth_figure
|
||||
from portfolio_app.figures.toronto import create_ranking_bar
|
||||
"""
|
||||
|
||||
from . import toronto
|
||||
|
||||
__all__ = [
|
||||
# Choropleth
|
||||
"create_choropleth_figure",
|
||||
"create_zone_map",
|
||||
# Time series
|
||||
"create_price_time_series",
|
||||
"create_volume_time_series",
|
||||
"create_market_comparison_chart",
|
||||
"create_time_series_with_events",
|
||||
"add_policy_markers",
|
||||
# Summary
|
||||
"create_metric_card_figure",
|
||||
"create_summary_metrics",
|
||||
# Bar charts
|
||||
"create_ranking_bar",
|
||||
"create_stacked_bar",
|
||||
"create_horizontal_bar",
|
||||
# Scatter plots
|
||||
"create_scatter_figure",
|
||||
"create_bubble_chart",
|
||||
# Radar charts
|
||||
"create_radar_figure",
|
||||
"create_comparison_radar",
|
||||
# Demographics
|
||||
"create_age_pyramid",
|
||||
"create_donut_chart",
|
||||
"create_income_distribution",
|
||||
"toronto",
|
||||
]
|
||||
|
||||
61
portfolio_app/figures/toronto/__init__.py
Normal file
61
portfolio_app/figures/toronto/__init__.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Plotly figure factories for Toronto dashboard visualizations."""
|
||||
|
||||
from .bar_charts import (
|
||||
create_horizontal_bar,
|
||||
create_ranking_bar,
|
||||
create_stacked_bar,
|
||||
)
|
||||
from .choropleth import (
|
||||
create_choropleth_figure,
|
||||
create_zone_map,
|
||||
)
|
||||
from .demographics import (
|
||||
create_age_pyramid,
|
||||
create_donut_chart,
|
||||
create_income_distribution,
|
||||
)
|
||||
from .radar import (
|
||||
create_comparison_radar,
|
||||
create_radar_figure,
|
||||
)
|
||||
from .scatter import (
|
||||
create_bubble_chart,
|
||||
create_scatter_figure,
|
||||
)
|
||||
from .summary_cards import create_metric_card_figure, create_summary_metrics
|
||||
from .time_series import (
|
||||
add_policy_markers,
|
||||
create_market_comparison_chart,
|
||||
create_price_time_series,
|
||||
create_time_series_with_events,
|
||||
create_volume_time_series,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Choropleth
|
||||
"create_choropleth_figure",
|
||||
"create_zone_map",
|
||||
# Time series
|
||||
"create_price_time_series",
|
||||
"create_volume_time_series",
|
||||
"create_market_comparison_chart",
|
||||
"create_time_series_with_events",
|
||||
"add_policy_markers",
|
||||
# Summary
|
||||
"create_metric_card_figure",
|
||||
"create_summary_metrics",
|
||||
# Bar charts
|
||||
"create_ranking_bar",
|
||||
"create_stacked_bar",
|
||||
"create_horizontal_bar",
|
||||
# Scatter plots
|
||||
"create_scatter_figure",
|
||||
"create_bubble_chart",
|
||||
# Radar charts
|
||||
"create_radar_figure",
|
||||
"create_comparison_radar",
|
||||
# Demographics
|
||||
"create_age_pyramid",
|
||||
"create_donut_chart",
|
||||
"create_income_distribution",
|
||||
]
|
||||
@@ -5,7 +5,7 @@ import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from dash import Input, Output, callback
|
||||
|
||||
from portfolio_app.figures import (
|
||||
from portfolio_app.figures.toronto import (
|
||||
create_donut_chart,
|
||||
create_horizontal_bar,
|
||||
create_radar_figure,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import plotly.graph_objects as go
|
||||
from dash import Input, Output, State, callback, no_update
|
||||
|
||||
from portfolio_app.figures import create_choropleth_figure, create_ranking_bar
|
||||
from portfolio_app.figures.toronto import create_choropleth_figure, create_ranking_bar
|
||||
from portfolio_app.toronto.services import (
|
||||
get_amenities_data,
|
||||
get_demographics_data,
|
||||
|
||||
@@ -8,11 +8,18 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
# Schema constants
|
||||
RAW_TORONTO_SCHEMA = "raw_toronto"
|
||||
|
||||
|
||||
class DimTime(Base):
|
||||
"""Time dimension table."""
|
||||
"""Time dimension table (shared across all projects).
|
||||
|
||||
Note: Stays in public schema as it's a shared dimension.
|
||||
"""
|
||||
|
||||
__tablename__ = "dim_time"
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
date_key: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
full_date: Mapped[date] = mapped_column(Date, nullable=False, unique=True)
|
||||
@@ -27,6 +34,7 @@ class DimCMHCZone(Base):
|
||||
"""CMHC zone dimension table with PostGIS geometry."""
|
||||
|
||||
__tablename__ = "dim_cmhc_zone"
|
||||
__table_args__ = {"schema": RAW_TORONTO_SCHEMA}
|
||||
|
||||
zone_key: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
zone_code: Mapped[str] = mapped_column(String(10), nullable=False, unique=True)
|
||||
@@ -41,6 +49,7 @@ class DimNeighbourhood(Base):
|
||||
"""
|
||||
|
||||
__tablename__ = "dim_neighbourhood"
|
||||
__table_args__ = {"schema": RAW_TORONTO_SCHEMA}
|
||||
|
||||
neighbourhood_id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
@@ -69,6 +78,7 @@ class DimPolicyEvent(Base):
|
||||
"""Policy event dimension for time-series annotation."""
|
||||
|
||||
__tablename__ = "dim_policy_event"
|
||||
__table_args__ = {"schema": RAW_TORONTO_SCHEMA}
|
||||
|
||||
event_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
event_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
|
||||
@@ -4,6 +4,7 @@ from sqlalchemy import ForeignKey, Index, Integer, Numeric, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .base import Base
|
||||
from .dimensions import RAW_TORONTO_SCHEMA
|
||||
|
||||
|
||||
class BridgeCMHCNeighbourhood(Base):
|
||||
@@ -14,6 +15,11 @@ class BridgeCMHCNeighbourhood(Base):
|
||||
"""
|
||||
|
||||
__tablename__ = "bridge_cmhc_neighbourhood"
|
||||
__table_args__ = (
|
||||
Index("ix_bridge_cmhc_zone", "cmhc_zone_code"),
|
||||
Index("ix_bridge_neighbourhood", "neighbourhood_id"),
|
||||
{"schema": RAW_TORONTO_SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
cmhc_zone_code: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
@@ -22,11 +28,6 @@ class BridgeCMHCNeighbourhood(Base):
|
||||
Numeric(5, 4), nullable=False
|
||||
) # 0.0000 to 1.0000
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_bridge_cmhc_zone", "cmhc_zone_code"),
|
||||
Index("ix_bridge_neighbourhood", "neighbourhood_id"),
|
||||
)
|
||||
|
||||
|
||||
class FactCensus(Base):
|
||||
"""Census statistics by neighbourhood and year.
|
||||
@@ -35,6 +36,10 @@ class FactCensus(Base):
|
||||
"""
|
||||
|
||||
__tablename__ = "fact_census"
|
||||
__table_args__ = (
|
||||
Index("ix_fact_census_neighbourhood_year", "neighbourhood_id", "census_year"),
|
||||
{"schema": RAW_TORONTO_SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
neighbourhood_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
@@ -66,10 +71,6 @@ class FactCensus(Base):
|
||||
Numeric(12, 2), nullable=True
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_fact_census_neighbourhood_year", "neighbourhood_id", "census_year"),
|
||||
)
|
||||
|
||||
|
||||
class FactCrime(Base):
|
||||
"""Crime statistics by neighbourhood and year.
|
||||
@@ -78,6 +79,11 @@ class FactCrime(Base):
|
||||
"""
|
||||
|
||||
__tablename__ = "fact_crime"
|
||||
__table_args__ = (
|
||||
Index("ix_fact_crime_neighbourhood_year", "neighbourhood_id", "year"),
|
||||
Index("ix_fact_crime_type", "crime_type"),
|
||||
{"schema": RAW_TORONTO_SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
neighbourhood_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
@@ -86,11 +92,6 @@ class FactCrime(Base):
|
||||
count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
rate_per_100k: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_fact_crime_neighbourhood_year", "neighbourhood_id", "year"),
|
||||
Index("ix_fact_crime_type", "crime_type"),
|
||||
)
|
||||
|
||||
|
||||
class FactAmenities(Base):
|
||||
"""Amenity counts by neighbourhood.
|
||||
@@ -99,6 +100,11 @@ class FactAmenities(Base):
|
||||
"""
|
||||
|
||||
__tablename__ = "fact_amenities"
|
||||
__table_args__ = (
|
||||
Index("ix_fact_amenities_neighbourhood_year", "neighbourhood_id", "year"),
|
||||
Index("ix_fact_amenities_type", "amenity_type"),
|
||||
{"schema": RAW_TORONTO_SCHEMA},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
neighbourhood_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
@@ -106,11 +112,6 @@ class FactAmenities(Base):
|
||||
count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
year: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_fact_amenities_neighbourhood_year", "neighbourhood_id", "year"),
|
||||
Index("ix_fact_amenities_type", "amenity_type"),
|
||||
)
|
||||
|
||||
|
||||
class FactRentals(Base):
|
||||
"""Fact table for CMHC rental market data.
|
||||
@@ -119,13 +120,16 @@ class FactRentals(Base):
|
||||
"""
|
||||
|
||||
__tablename__ = "fact_rentals"
|
||||
__table_args__ = {"schema": RAW_TORONTO_SCHEMA}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
date_key: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("dim_time.date_key"), nullable=False
|
||||
Integer, ForeignKey("public.dim_time.date_key"), nullable=False
|
||||
)
|
||||
zone_key: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("dim_cmhc_zone.zone_key"), nullable=False
|
||||
Integer,
|
||||
ForeignKey(f"{RAW_TORONTO_SCHEMA}.dim_cmhc_zone.zone_key"),
|
||||
nullable=False,
|
||||
)
|
||||
bedroom_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
universe: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
@@ -139,6 +143,6 @@ class FactRentals(Base):
|
||||
rent_change_pct: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True)
|
||||
reliability_code: Mapped[str | None] = mapped_column(String(2), nullable=True)
|
||||
|
||||
# Relationships
|
||||
time = relationship("DimTime", backref="rentals")
|
||||
zone = relationship("DimCMHCZone", backref="rentals")
|
||||
# Relationships - explicit foreign_keys needed for cross-schema joins
|
||||
time = relationship("DimTime", foreign_keys=[date_key], backref="rentals")
|
||||
zone = relationship("DimCMHCZone", foreign_keys=[zone_key], backref="rentals")
|
||||
|
||||
Reference in New Issue
Block a user