"""UMAP 토픽 맵 시각화 (Plotly scatter).
snapshot.coordinates: [{cluster_id, x, y, size, label}]
clusters: [{id, cluster_label, attention_score, citation_density, opportunity_score, fanout_count, ...}]
"""
import streamlit as st
import plotly.graph_objects as go
def render_topic_map(clusters: list[dict], snapshot: dict | None, frame: str = "all"):
"""Render UMAP 2D scatter from snapshot coordinates + cluster metadata."""
if not snapshot or not snapshot.get("coordinates"):
st.info("UMAP 좌표 데이터가 없습니다. 클러스터링 실행 후 생성됩니다.")
return
# Frame-specific guide
if frame == "demand":
st.markdown("""
각 점은 하나의 **Demand 토픽** (ChatGPT sub-query 그룹)입니다.
- **점 크기**: 해당 토픽의 fanout 수 (클수록 소비자가 자주 묻는 토픽)
- **점 색상**: 기회 점수 (빨강 = 기회 큼, 노랑 = 보통)
- **가까이 있는 점**: 유사한 검색 의도의 토픽
""")
elif frame == "supply":
st.markdown("""
각 점은 하나의 **Supply 토픽** (Gemini citation quote 그룹)입니다.
- **점 크기**: 해당 토픽의 인용 수 (클수록 AI가 자주 인용하는 토픽)
- **점 색상**: 기회 점수 (빨강 = 기회 큼, 노랑 = 보통)
- **가까이 있는 점**: 유사한 인용 주제의 토픽
""")
else:
st.markdown("""
각 점은 하나의 **토픽**(AI 추가 질문 그룹)입니다.
- **점 크기**: 해당 토픽의 AI 추가 질문 수 (클수록 AI가 자주 묻는 토픽)
- **점 색상**: 기회 점수 (빨강 = 기회 큼, 노랑 = 보통)
- **가까이 있는 점**: 유사한 주제의 토픽
""")
count_label = "Citations" if frame == "supply" else "Fanouts"
coords = snapshot["coordinates"]
# Build cluster lookup by id
cluster_map = {c["id"]: c for c in clusters}
# Merge coordinate data with cluster metadata
xs, ys, sizes, colors, hover_texts = [], [], [], [], []
for pt in coords:
cid = pt.get("cluster_id")
meta = cluster_map.get(cid, {})
xs.append(pt["x"])
ys.append(pt["y"])
fanout_count = pt.get("size", meta.get("fanout_count", 10))
# Normalize size for display (min 5, max 40)
norm_size = max(5, min(40, fanout_count / 5))
sizes.append(norm_size)
opp = float(meta.get("opportunity_score", 0) or 0)
colors.append(opp)
label = meta.get("cluster_label") or f"Cluster {pt.get('label', '?')}"
attn = float(meta.get("attention_score", 0) or 0)
density = float(meta.get("citation_density", 0) or 0)
hover_texts.append(
f"{label}
"
f"Attention: {attn:.4f}
"
f"Density: {density:.4f}
"
f"Opportunity: {opp:.4f}
"
f"{count_label}: {fanout_count}"
)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=xs,
y=ys,
mode="markers",
marker=dict(
size=sizes,
color=colors,
colorscale="YlOrRd",
colorbar=dict(title="Opportunity"),
opacity=0.7,
line=dict(width=0.5, color="#333"),
),
text=hover_texts,
hoverinfo="text",
))
fig.update_layout(
title="AI 토픽 맵 (UMAP 2D Projection)",
xaxis=dict(title="UMAP-1", showgrid=False, zeroline=False),
yaxis=dict(title="UMAP-2", showgrid=False, zeroline=False),
height=600,
template="plotly_white",
hoverlabel=dict(bgcolor="white", font_size=12),
)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
# Algorithm params info
params = snapshot.get("algorithm_params")
if params:
with st.expander("분석 설정 (기술 상세)"):
st.json(params)