feat: add Sprint 5 visualization components and Toronto dashboard
- Add figure factories: choropleth, time_series, summary_cards - Add shared components: map_controls, time_slider, metric_card - Create Toronto dashboard page with KPI cards, choropleth maps, and time series - Add dashboard callbacks for interactivity - Placeholder data for demonstration until QGIS boundaries are complete Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
152
portfolio_app/figures/choropleth.py
Normal file
152
portfolio_app/figures/choropleth.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Choropleth map figure factory for Toronto housing data."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
|
||||
def create_choropleth_figure(
|
||||
geojson: dict[str, Any] | None,
|
||||
data: list[dict[str, Any]],
|
||||
location_key: str,
|
||||
color_column: str,
|
||||
hover_data: list[str] | None = None,
|
||||
color_scale: str = "Blues",
|
||||
title: str | None = None,
|
||||
map_style: str = "carto-positron",
|
||||
center: dict[str, float] | None = None,
|
||||
zoom: float = 9.5,
|
||||
) -> go.Figure:
|
||||
"""Create a choropleth map figure.
|
||||
|
||||
Args:
|
||||
geojson: GeoJSON FeatureCollection for boundaries.
|
||||
data: List of data records with location keys and values.
|
||||
location_key: Column name for location identifier.
|
||||
color_column: Column name for color values.
|
||||
hover_data: Additional columns to show on hover.
|
||||
color_scale: Plotly color scale name.
|
||||
title: Optional chart title.
|
||||
map_style: Mapbox style (carto-positron, open-street-map, etc.).
|
||||
center: Map center coordinates {"lat": float, "lon": float}.
|
||||
zoom: Initial zoom level.
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
# Default center to Toronto
|
||||
if center is None:
|
||||
center = {"lat": 43.7, "lon": -79.4}
|
||||
|
||||
# If no geojson provided, create a placeholder map
|
||||
if geojson is None or not data:
|
||||
fig = go.Figure(go.Scattermapbox())
|
||||
fig.update_layout(
|
||||
mapbox={
|
||||
"style": map_style,
|
||||
"center": center,
|
||||
"zoom": zoom,
|
||||
},
|
||||
margin={"l": 0, "r": 0, "t": 40, "b": 0},
|
||||
title=title or "Toronto Housing Map",
|
||||
height=500,
|
||||
)
|
||||
fig.add_annotation(
|
||||
text="No geometry data available. Complete QGIS digitization to enable map.",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"size": 14, "color": "gray"},
|
||||
)
|
||||
return fig
|
||||
|
||||
# Create choropleth with data
|
||||
import pandas as pd
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
fig = px.choropleth_mapbox(
|
||||
df,
|
||||
geojson=geojson,
|
||||
locations=location_key,
|
||||
featureidkey=f"properties.{location_key}",
|
||||
color=color_column,
|
||||
color_continuous_scale=color_scale,
|
||||
hover_data=hover_data,
|
||||
mapbox_style=map_style,
|
||||
center=center,
|
||||
zoom=zoom,
|
||||
opacity=0.7,
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
margin={"l": 0, "r": 0, "t": 40, "b": 0},
|
||||
title=title,
|
||||
height=500,
|
||||
coloraxis_colorbar={
|
||||
"title": color_column.replace("_", " ").title(),
|
||||
"thickness": 15,
|
||||
"len": 0.7,
|
||||
},
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_district_map(
|
||||
districts_geojson: dict[str, Any] | None,
|
||||
purchase_data: list[dict[str, Any]],
|
||||
metric: str = "avg_price",
|
||||
) -> go.Figure:
|
||||
"""Create choropleth map for TRREB districts.
|
||||
|
||||
Args:
|
||||
districts_geojson: GeoJSON for TRREB district boundaries.
|
||||
purchase_data: Purchase statistics by district.
|
||||
metric: Metric to display (avg_price, sales_count, etc.).
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
hover_columns = ["district_name", "sales_count", "avg_price", "median_price"]
|
||||
|
||||
return create_choropleth_figure(
|
||||
geojson=districts_geojson,
|
||||
data=purchase_data,
|
||||
location_key="district_code",
|
||||
color_column=metric,
|
||||
hover_data=[c for c in hover_columns if c != metric],
|
||||
color_scale="Blues" if "price" in metric else "Greens",
|
||||
title="Toronto Purchase Market by District",
|
||||
)
|
||||
|
||||
|
||||
def create_zone_map(
|
||||
zones_geojson: dict[str, Any] | None,
|
||||
rental_data: list[dict[str, Any]],
|
||||
metric: str = "avg_rent",
|
||||
) -> go.Figure:
|
||||
"""Create choropleth map for CMHC zones.
|
||||
|
||||
Args:
|
||||
zones_geojson: GeoJSON for CMHC zone boundaries.
|
||||
rental_data: Rental statistics by zone.
|
||||
metric: Metric to display (avg_rent, vacancy_rate, etc.).
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
hover_columns = ["zone_name", "avg_rent", "vacancy_rate", "rental_universe"]
|
||||
|
||||
return create_choropleth_figure(
|
||||
geojson=zones_geojson,
|
||||
data=rental_data,
|
||||
location_key="zone_code",
|
||||
color_column=metric,
|
||||
hover_data=[c for c in hover_columns if c != metric],
|
||||
color_scale="Oranges" if "rent" in metric else "Purples",
|
||||
title="Toronto Rental Market by Zone",
|
||||
)
|
||||
Reference in New Issue
Block a user