- 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>
387 lines
11 KiB
Python
387 lines
11 KiB
Python
"""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
|