"""Plotly figures for the deck-builder Space. Three breakdown charts displayed alongside a generated deck: - cost curve (bar): quantities by cost bucket vs. the target curve - type breakdown (bar): Character/Event/Stage counts - color breakdown (bar): cards per color Color hex map mirrors the upstream `optcg_cards.visualize._first_color_hex` so palettes stay consistent across all OPTCG-related Spaces. """ from __future__ import annotations from typing import TYPE_CHECKING from optcg_cards.visualize import _first_color_hex if TYPE_CHECKING: from spaceutil.deck import Deck def _empty_fig(message: str, height: int = 240): import plotly.graph_objects as go fig = go.Figure() fig.add_annotation( text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(color="gray"), ) fig.update_layout( xaxis=dict(visible=False), yaxis=dict(visible=False), plot_bgcolor="white", margin=dict(l=40, r=40, t=40, b=40), height=height, ) return fig def build_cost_curve_figure(deck: Deck | None, height: int = 280): """Bar chart comparing the deck's actual cost curve vs. the style target.""" import plotly.graph_objects as go if deck is None or deck.total_quantity == 0: return _empty_fig("Build a deck to see its cost curve.", height=height) actual = deck.cost_distribution target = deck.target_curve or {} buckets = sorted(set(actual) | set(target)) labels = [("8+" if b == 8 else str(b)) for b in buckets] actual_y = [actual.get(b, 0) for b in buckets] target_y = [target.get(b, 0) for b in buckets] fig = go.Figure(data=[ go.Bar(name="Actual", x=labels, y=actual_y, marker_color="#dc3545"), go.Bar(name="Target", x=labels, y=target_y, marker_color="#1f77b4", opacity=0.5), ]) fig.update_layout( title="Cost curve: actual vs. target", xaxis=dict(title="Cost"), yaxis=dict(title="Cards"), barmode="group", plot_bgcolor="white", margin=dict(l=40, r=40, t=60, b=40), height=height, legend=dict(orientation="h", y=1.0, yanchor="bottom"), ) return fig def build_type_breakdown_figure(deck: Deck | None, height: int = 240): import plotly.graph_objects as go if deck is None or deck.total_quantity == 0: return _empty_fig("Build a deck to see its type mix.", height=height) dist = deck.type_distribution types = sorted(dist.keys()) counts = [dist[t] for t in types] fig = go.Figure(data=[go.Bar(x=types, y=counts, marker_color="#6c757d")]) fig.update_layout( title="Card type mix", xaxis=dict(title=""), yaxis=dict(title="Cards"), plot_bgcolor="white", margin=dict(l=40, r=40, t=60, b=40), height=height, ) return fig def build_color_breakdown_figure(deck: Deck | None, height: int = 240): import plotly.graph_objects as go if deck is None or deck.total_quantity == 0: return _empty_fig("Build a deck to see its color mix.", height=height) dist = deck.color_distribution colors = sorted(dist.keys()) counts = [dist[c] for c in colors] bar_colors = [_first_color_hex([c]) for c in colors] fig = go.Figure(data=[go.Bar(x=colors, y=counts, marker_color=bar_colors)]) fig.update_layout( title="Color mix (per copy)", xaxis=dict(title=""), yaxis=dict(title="Cards (counted per color of multicolor cards)"), plot_bgcolor="white", margin=dict(l=40, r=40, t=60, b=40), height=height, ) return fig