t22000t's picture
Add Synergy Inspector tab: leader-anchored recs with color legality + family bonus
78c571c
"""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"<b>Rank {rank + 1}</b><br>{_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