"""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"{title}
" f"Date: %{{x}}
" f"Level: {level.title()}
" f"Category: {category.title()}
" f"" ), 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