refactor: multi-dashboard structural migration
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:
2026-02-01 19:08:20 -05:00
parent a5d6866d63
commit 62d1a52eed
73 changed files with 1114 additions and 623 deletions

View File

@@ -0,0 +1,386 @@
"""Time series figure factories for Toronto housing data."""
from typing import Any
import plotly.express as px
import plotly.graph_objects as go
def create_price_time_series(
data: list[dict[str, Any]],
date_column: str = "full_date",
price_column: str = "avg_price",
group_column: str | None = None,
title: str = "Average Price Over Time",
show_yoy: bool = True,
) -> go.Figure:
"""Create a time series chart for price data.
Args:
data: List of records with date and price columns.
date_column: Column name for dates.
price_column: Column name for price values.
group_column: Optional column for grouping (e.g., district_code).
title: Chart title.
show_yoy: Whether to show year-over-year change annotations.
Returns:
Plotly Figure object.
"""
import pandas as pd
if not data:
fig = go.Figure()
fig.add_annotation(
text="No data available",
xref="paper",
yref="paper",
x=0.5,
y=0.5,
showarrow=False,
font={"color": "#888888"},
)
fig.update_layout(
title=title,
height=350,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
)
return fig
df = pd.DataFrame(data)
df[date_column] = pd.to_datetime(df[date_column])
if group_column and group_column in df.columns:
fig = px.line(
df,
x=date_column,
y=price_column,
color=group_column,
title=title,
)
else:
fig = px.line(
df,
x=date_column,
y=price_column,
title=title,
)
fig.update_layout(
height=350,
margin={"l": 40, "r": 20, "t": 50, "b": 40},
xaxis_title="Date",
yaxis_title=price_column.replace("_", " ").title(),
yaxis_tickprefix="$",
yaxis_tickformat=",",
hovermode="x unified",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
)
return fig
def create_volume_time_series(
data: list[dict[str, Any]],
date_column: str = "full_date",
volume_column: str = "sales_count",
group_column: str | None = None,
title: str = "Sales Volume Over Time",
chart_type: str = "bar",
) -> go.Figure:
"""Create a time series chart for volume/count data.
Args:
data: List of records with date and volume columns.
date_column: Column name for dates.
volume_column: Column name for volume values.
group_column: Optional column for grouping.
title: Chart title.
chart_type: 'bar' or 'line'.
Returns:
Plotly Figure object.
"""
import pandas as pd
if not data:
fig = go.Figure()
fig.add_annotation(
text="No data available",
xref="paper",
yref="paper",
x=0.5,
y=0.5,
showarrow=False,
font={"color": "#888888"},
)
fig.update_layout(
title=title,
height=350,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
)
return fig
df = pd.DataFrame(data)
df[date_column] = pd.to_datetime(df[date_column])
if chart_type == "bar":
if group_column and group_column in df.columns:
fig = px.bar(
df,
x=date_column,
y=volume_column,
color=group_column,
title=title,
)
else:
fig = px.bar(
df,
x=date_column,
y=volume_column,
title=title,
)
else:
if group_column and group_column in df.columns:
fig = px.line(
df,
x=date_column,
y=volume_column,
color=group_column,
title=title,
)
else:
fig = px.line(
df,
x=date_column,
y=volume_column,
title=title,
)
fig.update_layout(
height=350,
margin={"l": 40, "r": 20, "t": 50, "b": 40},
xaxis_title="Date",
yaxis_title=volume_column.replace("_", " ").title(),
yaxis_tickformat=",",
hovermode="x unified",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
)
return fig
def create_market_comparison_chart(
data: list[dict[str, Any]],
date_column: str = "full_date",
metrics: list[str] | None = None,
title: str = "Market Indicators",
) -> go.Figure:
"""Create a multi-metric comparison chart.
Args:
data: List of records with date and metric columns.
date_column: Column name for dates.
metrics: List of metric columns to display.
title: Chart title.
Returns:
Plotly Figure object with secondary y-axis.
"""
import pandas as pd
from plotly.subplots import make_subplots
if not data:
fig = go.Figure()
fig.add_annotation(
text="No data available",
xref="paper",
yref="paper",
x=0.5,
y=0.5,
showarrow=False,
font={"color": "#888888"},
)
fig.update_layout(
title=title,
height=400,
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
)
return fig
if metrics is None:
metrics = ["avg_price", "sales_count"]
df = pd.DataFrame(data)
df[date_column] = pd.to_datetime(df[date_column])
fig = make_subplots(specs=[[{"secondary_y": True}]])
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]
for i, metric in enumerate(metrics[:4]):
if metric not in df.columns:
continue
secondary = i > 0
fig.add_trace(
go.Scatter(
x=df[date_column],
y=df[metric],
name=metric.replace("_", " ").title(),
line={"color": colors[i % len(colors)]},
),
secondary_y=secondary,
)
fig.update_layout(
title=title,
height=400,
margin={"l": 40, "r": 40, "t": 50, "b": 40},
hovermode="x unified",
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
font_color="#c9c9c9",
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
legend={
"orientation": "h",
"yanchor": "bottom",
"y": 1.02,
"xanchor": "right",
"x": 1,
"font": {"color": "#c9c9c9"},
},
)
return fig
def add_policy_markers(
fig: go.Figure,
policy_events: list[dict[str, Any]],
date_column: str = "event_date",
y_position: float | None = None,
) -> go.Figure:
"""Add policy event markers to an existing time series figure.
Args:
fig: Existing Plotly figure to add markers to.
policy_events: List of policy event dicts with date and metadata.
date_column: Column name for event dates.
y_position: Y position for markers. If None, uses top of chart.
Returns:
Updated Plotly Figure object with policy markers.
"""
if not policy_events:
return fig
# Color mapping for policy categories
category_colors = {
"monetary": "#1f77b4", # Blue
"tax": "#2ca02c", # Green
"regulatory": "#ff7f0e", # Orange
"supply": "#9467bd", # Purple
"economic": "#d62728", # Red
}
# Symbol mapping for expected direction
direction_symbols = {
"bullish": "triangle-up",
"bearish": "triangle-down",
"neutral": "circle",
}
for event in policy_events:
event_date = event.get(date_column)
category = event.get("category", "economic")
direction = event.get("expected_direction", "neutral")
title = event.get("title", "Policy Event")
level = event.get("level", "federal")
color = category_colors.get(category, "#666666")
symbol = direction_symbols.get(direction, "circle")
# Add vertical line for the event
fig.add_vline(
x=event_date,
line_dash="dot",
line_color=color,
opacity=0.5,
annotation_text="",
)
# Add marker with hover info
fig.add_trace(
go.Scatter(
x=[event_date],
y=[y_position] if y_position else [None], # type: ignore[list-item]
mode="markers",
marker={
"symbol": symbol,
"size": 12,
"color": color,
"line": {"width": 1, "color": "white"},
},
name=title,
hovertemplate=(
f"<b>{title}</b><br>"
f"Date: %{{x}}<br>"
f"Level: {level.title()}<br>"
f"Category: {category.title()}<br>"
f"<extra></extra>"
),
showlegend=False,
)
)
return fig
def create_time_series_with_events(
data: list[dict[str, Any]],
policy_events: list[dict[str, Any]],
date_column: str = "full_date",
value_column: str = "avg_price",
title: str = "Price Trend with Policy Events",
) -> go.Figure:
"""Create a time series chart with policy event markers.
Args:
data: Time series data.
policy_events: Policy events to overlay.
date_column: Column name for dates.
value_column: Column name for values.
title: Chart title.
Returns:
Plotly Figure with time series and policy markers.
"""
# Create base time series
fig = create_price_time_series(
data=data,
date_column=date_column,
price_column=value_column,
title=title,
)
# Add policy markers at the top of the chart
if policy_events:
fig = add_policy_markers(fig, policy_events)
return fig