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:
238
portfolio_app/figures/toronto/bar_charts.py
Normal file
238
portfolio_app/figures/toronto/bar_charts.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""Bar chart figure factories for dashboard visualizations."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
|
||||
def create_ranking_bar(
|
||||
data: list[dict[str, Any]],
|
||||
name_column: str,
|
||||
value_column: str,
|
||||
title: str | None = None,
|
||||
top_n: int = 10,
|
||||
bottom_n: int = 10,
|
||||
color_top: str = "#4CAF50",
|
||||
color_bottom: str = "#F44336",
|
||||
value_format: str = ",.0f",
|
||||
) -> go.Figure:
|
||||
"""Create horizontal bar chart showing top and bottom rankings.
|
||||
|
||||
Args:
|
||||
data: List of data records.
|
||||
name_column: Column name for labels.
|
||||
value_column: Column name for values.
|
||||
title: Optional chart title.
|
||||
top_n: Number of top items to show.
|
||||
bottom_n: Number of bottom items to show.
|
||||
color_top: Color for top performers.
|
||||
color_bottom: Color for bottom performers.
|
||||
value_format: Number format string for values.
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
if not data:
|
||||
return _create_empty_figure(title or "Rankings")
|
||||
|
||||
df = pd.DataFrame(data).sort_values(value_column, ascending=False)
|
||||
|
||||
# Get top and bottom
|
||||
top_df = df.head(top_n).copy()
|
||||
bottom_df = df.tail(bottom_n).copy()
|
||||
|
||||
top_df["group"] = "Top"
|
||||
bottom_df["group"] = "Bottom"
|
||||
|
||||
# Combine with gap in the middle
|
||||
combined = pd.concat([top_df, bottom_df])
|
||||
combined["color"] = combined["group"].map(
|
||||
{"Top": color_top, "Bottom": color_bottom}
|
||||
)
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
# Add top bars
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
y=top_df[name_column],
|
||||
x=top_df[value_column],
|
||||
orientation="h",
|
||||
marker_color=color_top,
|
||||
name="Top",
|
||||
text=top_df[value_column].apply(lambda x: f"{x:{value_format}}"),
|
||||
textposition="auto",
|
||||
hovertemplate=f"%{{y}}<br>{value_column}: %{{x:{value_format}}}<extra></extra>",
|
||||
)
|
||||
)
|
||||
|
||||
# Add bottom bars
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
y=bottom_df[name_column],
|
||||
x=bottom_df[value_column],
|
||||
orientation="h",
|
||||
marker_color=color_bottom,
|
||||
name="Bottom",
|
||||
text=bottom_df[value_column].apply(lambda x: f"{x:{value_format}}"),
|
||||
textposition="auto",
|
||||
hovertemplate=f"%{{y}}<br>{value_column}: %{{x:{value_format}}}<extra></extra>",
|
||||
)
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
barmode="group",
|
||||
showlegend=True,
|
||||
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02},
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
|
||||
yaxis={"autorange": "reversed", "title": None},
|
||||
margin={"l": 10, "r": 10, "t": 40, "b": 10},
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_stacked_bar(
|
||||
data: list[dict[str, Any]],
|
||||
x_column: str,
|
||||
value_column: str,
|
||||
category_column: str,
|
||||
title: str | None = None,
|
||||
color_map: dict[str, str] | None = None,
|
||||
show_percentages: bool = False,
|
||||
) -> go.Figure:
|
||||
"""Create stacked bar chart for breakdown visualizations.
|
||||
|
||||
Args:
|
||||
data: List of data records.
|
||||
x_column: Column name for x-axis categories.
|
||||
value_column: Column name for values.
|
||||
category_column: Column name for stacking categories.
|
||||
title: Optional chart title.
|
||||
color_map: Mapping of category to color.
|
||||
show_percentages: Whether to normalize to 100%.
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
if not data:
|
||||
return _create_empty_figure(title or "Breakdown")
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Default color scheme
|
||||
if color_map is None:
|
||||
categories = df[category_column].unique()
|
||||
colors = px.colors.qualitative.Set2[: len(categories)]
|
||||
color_map = dict(zip(categories, colors, strict=False))
|
||||
|
||||
fig = px.bar(
|
||||
df,
|
||||
x=x_column,
|
||||
y=value_column,
|
||||
color=category_column,
|
||||
color_discrete_map=color_map,
|
||||
barmode="stack",
|
||||
text=value_column if not show_percentages else None,
|
||||
)
|
||||
|
||||
if show_percentages:
|
||||
fig.update_traces(texttemplate="%{y:.1f}%", textposition="inside")
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
|
||||
yaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
|
||||
legend={"orientation": "h", "yanchor": "bottom", "y": 1.02},
|
||||
margin={"l": 10, "r": 10, "t": 60, "b": 10},
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def create_horizontal_bar(
|
||||
data: list[dict[str, Any]],
|
||||
name_column: str,
|
||||
value_column: str,
|
||||
title: str | None = None,
|
||||
color: str = "#2196F3",
|
||||
value_format: str = ",.0f",
|
||||
sort: bool = True,
|
||||
) -> go.Figure:
|
||||
"""Create simple horizontal bar chart.
|
||||
|
||||
Args:
|
||||
data: List of data records.
|
||||
name_column: Column name for labels.
|
||||
value_column: Column name for values.
|
||||
title: Optional chart title.
|
||||
color: Bar color.
|
||||
value_format: Number format string.
|
||||
sort: Whether to sort by value descending.
|
||||
|
||||
Returns:
|
||||
Plotly Figure object.
|
||||
"""
|
||||
if not data:
|
||||
return _create_empty_figure(title or "Bar Chart")
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
if sort:
|
||||
df = df.sort_values(value_column, ascending=True)
|
||||
|
||||
fig = go.Figure(
|
||||
go.Bar(
|
||||
y=df[name_column],
|
||||
x=df[value_column],
|
||||
orientation="h",
|
||||
marker_color=color,
|
||||
text=df[value_column].apply(lambda x: f"{x:{value_format}}"),
|
||||
textposition="outside",
|
||||
hovertemplate=f"%{{y}}<br>Value: %{{x:{value_format}}}<extra></extra>",
|
||||
)
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"gridcolor": "rgba(128,128,128,0.2)", "title": None},
|
||||
yaxis={"title": None},
|
||||
margin={"l": 10, "r": 10, "t": 40, "b": 10},
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def _create_empty_figure(title: str) -> go.Figure:
|
||||
"""Create an empty figure with a message."""
|
||||
fig = go.Figure()
|
||||
fig.add_annotation(
|
||||
text="No data available",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"size": 14, "color": "#888888"},
|
||||
)
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"visible": False},
|
||||
yaxis={"visible": False},
|
||||
)
|
||||
return fig
|
||||
Reference in New Issue
Block a user