"""Plotly figures used by the Gradio Space. `build_umap_figure` adapts `optcg_cards.visualize.render_html` (visualize.py:109-128) - returns a Figure rather than writing HTML, and overlays a second trace for search hits. `build_cost_curve_figure` plots a synergy recommendation set's cost-distribution as a bar chart for the Synergy Inspector tab. """ from __future__ import annotations from typing import TYPE_CHECKING, Any from optcg_cards.visualize import _first_color_hex, _hover_label if TYPE_CHECKING: from spaceutil.synergy import SynergyHit def build_umap_figure( cards: list[dict[str, Any]], highlight_indices: list[int] | None = None, title: str = "OPTCG Card Embedding Map", ): import plotly.graph_objects as go if not cards: raise ValueError("No cards to render") if "umap_x" not in cards[0] or "umap_y" not in cards[0]: raise ValueError("Cards must have umap_x/umap_y; cannot plot") xs = [c["umap_x"] for c in cards] ys = [c["umap_y"] for c in cards] colors = [_first_color_hex(c.get("colors", [])) for c in cards] hover = [_hover_label(c) for c in cards] base_customdata = [[i] for i in range(len(cards))] fig = go.Figure() fig.add_trace( go.Scatter( x=xs, y=ys, mode="markers", marker=dict(size=5, color=colors, opacity=0.4), text=hover, hoverinfo="text", customdata=base_customdata, name="all cards", ) ) if highlight_indices: hi_x = [cards[i]["umap_x"] for i in highlight_indices] hi_y = [cards[i]["umap_y"] for i in highlight_indices] hi_colors = [_first_color_hex(cards[i].get("colors", [])) for i in highlight_indices] hi_hover = [ f"Rank {rank + 1}
{_hover_label(cards[i])}" for rank, i in enumerate(highlight_indices) ] hi_customdata = [[idx, rank + 1] for rank, idx in enumerate(highlight_indices)] fig.add_trace( go.Scatter( x=hi_x, y=hi_y, mode="markers", marker=dict( size=14, color=hi_colors, opacity=1.0, line=dict(width=2, color="black"), ), text=hi_hover, hoverinfo="text", customdata=hi_customdata, name="search hits", ) ) fig.update_layout( title=title, xaxis=dict(title="UMAP-1", showgrid=False, zeroline=False), yaxis=dict(title="UMAP-2", showgrid=False, zeroline=False), plot_bgcolor="white", showlegend=False, margin=dict(l=40, r=40, t=60, b=40), ) return fig def build_cost_curve_figure( hits: list[SynergyHit], title: str = "Cost curve of recommendations", ): """Bar chart of synergy recommendations grouped by cost. Costs above 10 are clamped into a single "10+" bucket so a stray high-cost finisher doesn't stretch the X axis.""" import plotly.graph_objects as go from spaceutil.synergy import cost_curve counts = cost_curve(hits, max_cost=10) if not counts: fig = go.Figure() fig.add_annotation( text="No cost data yet - pick a leader to see recommendations.", 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=60, b=40), height=260, ) return fig costs = sorted(counts.keys()) labels = [("10+" if c == 10 else str(c)) for c in costs] values = [counts[c] for c in costs] fig = go.Figure(data=[go.Bar(x=labels, y=values, marker_color="#dc3545")]) fig.update_layout( title=title, xaxis=dict(title="Cost"), yaxis=dict(title="Count of recommendations"), plot_bgcolor="white", margin=dict(l=40, r=40, t=60, b=40), height=260, ) return fig