Files
personal-portfolio/portfolio_app/figures/summary_cards.py
lmiranda 077e426d34 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>
2026-01-11 16:20:01 -05:00

107 lines
3.1 KiB
Python

"""Summary card figure factories for KPI display."""
from typing import Any
import plotly.graph_objects as go
def create_metric_card_figure(
value: float | int | str,
title: str,
delta: float | None = None,
delta_suffix: str = "%",
prefix: str = "",
suffix: str = "",
format_spec: str = ",.0f",
positive_is_good: bool = True,
) -> go.Figure:
"""Create a KPI indicator figure.
Args:
value: The main metric value.
title: Card title.
delta: Optional change value (for delta indicator).
delta_suffix: Suffix for delta value (e.g., '%').
prefix: Prefix for main value (e.g., '$').
suffix: Suffix for main value.
format_spec: Python format specification for the value.
positive_is_good: Whether positive delta is good (green) or bad (red).
Returns:
Plotly Figure object.
"""
# Determine numeric value for indicator
if isinstance(value, int | float):
number_value: float | None = float(value)
else:
number_value = None
fig = go.Figure()
# Add indicator trace
indicator_config: dict[str, Any] = {
"mode": "number",
"value": number_value if number_value is not None else 0,
"title": {"text": title, "font": {"size": 14}},
"number": {
"font": {"size": 32},
"prefix": prefix,
"suffix": suffix,
"valueformat": format_spec,
},
}
# Add delta if provided
if delta is not None:
indicator_config["mode"] = "number+delta"
indicator_config["delta"] = {
"reference": number_value - delta if number_value else 0,
"relative": False,
"valueformat": ".1f",
"suffix": delta_suffix,
"increasing": {"color": "green" if positive_is_good else "red"},
"decreasing": {"color": "red" if positive_is_good else "green"},
}
fig.add_trace(go.Indicator(**indicator_config))
fig.update_layout(
height=120,
margin={"l": 20, "r": 20, "t": 40, "b": 20},
paper_bgcolor="rgba(0,0,0,0)",
font={"family": "Inter, sans-serif"},
)
return fig
def create_summary_metrics(
metrics: dict[str, dict[str, Any]],
) -> list[go.Figure]:
"""Create multiple metric card figures.
Args:
metrics: Dictionary of metric configurations.
Key: metric name
Value: dict with 'value', 'title', 'delta' (optional), etc.
Returns:
List of Plotly Figure objects.
"""
figures = []
for metric_config in metrics.values():
fig = create_metric_card_figure(
value=metric_config.get("value", 0),
title=metric_config.get("title", ""),
delta=metric_config.get("delta"),
delta_suffix=metric_config.get("delta_suffix", "%"),
prefix=metric_config.get("prefix", ""),
suffix=metric_config.get("suffix", ""),
format_spec=metric_config.get("format_spec", ",.0f"),
positive_is_good=metric_config.get("positive_is_good", True),
)
figures.append(fig)
return figures