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:
106
portfolio_app/figures/summary_cards.py
Normal file
106
portfolio_app/figures/summary_cards.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user