"""Chart components for Gen3 Nudge Detection Dashboard.""" import plotly.graph_objects as go import pandas as pd # ============================================================================= # Color Constants # ============================================================================= CONFIDENCE_TIER_COLORS = { "HIGH": "#EF4444", # Red "MEDIUM": "#F59E0B", # Amber "LOW": "#10B981", # Green } PLATFORM_COLORS = { "CHATGPT": "#10A37F", "GOOGLE_AI": "#4285F4", "GOOGLE_OVERVIEW": "#34A853", "PERPLEXITY": "#6366F1", "GEMINI": "#8B5CF6", "BING": "#00A4EF", "CLAUDE": "#D97706", } POLARITY_COLORS = { "positive": "#10B981", "negative": "#EF4444", "neutral": "#6B7280", } # English to Korean emotion mapping EMOTION_KO = { "admiration": "감탄", "amusement": "재미", "anger": "분노", "annoyance": "짜증", "approval": "인정", "caring": "배려", "confusion": "혼란", "curiosity": "호기심", "desire": "욕구", "disappointment": "실망", "disapproval": "반대", "disgust": "혐오", "embarrassment": "당혹", "excitement": "흥분", "fear": "두려움", "gratitude": "감사", "grief": "슬픔", "joy": "기쁨", "love": "사랑", "nervousness": "불안", "optimism": "낙관", "pride": "자부심", "realization": "깨달음", "relief": "안도", "remorse": "후회", "sadness": "슬픔", "surprise": "놀라움", "neutral": "중립", "trust": "신뢰", "anticipation": "기대", "interest": "관심", "satisfaction": "만족", "frustration": "좌절", "hope": "희망", "worry": "걱정", } # CEJ Korean labels CEJ_LABELS = { "VERIFICATION": "검증 질문", "INFORMATION_DISCOVERY": "정보 탐색", "HOW_TO": "사용법", "WHERE_TO_BUY": "구매처", "RECOMMENDATION": "추천 요청", "SIDE_EFFECT": "부작용", "MARKET_TRENDS": "시장 동향", "RESULT_EFFECTIVENESS": "효과/결과", "COMPARISON": "비교", "INGREDIENT": "성분", "AWARENESS_COMPARISON": "인지/비교", "PURCHASE": "구매", "POST_PURCHASE": "구매 후", } # ============================================================================= # Gen3 Nudge Charts # ============================================================================= def create_confidence_tier_pie_chart(tier_stats: dict) -> go.Figure: """Create pie chart for confidence tier distribution (HIGH/MEDIUM/LOW).""" if not tier_stats: return go.Figure() labels = list(tier_stats.keys()) values = list(tier_stats.values()) colors = [CONFIDENCE_TIER_COLORS.get(k, "#6B7280") for k in labels] label_map = { "HIGH": "🔴 HIGH", "MEDIUM": "🟡 MEDIUM", "LOW": "🟢 LOW", } display_labels = [label_map.get(k, k) for k in labels] fig = go.Figure(data=[go.Pie( labels=display_labels, values=values, marker=dict(colors=colors), hole=0.4, textinfo="percent+value", textposition="outside", )]) fig.update_layout( title="", showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5), margin=dict(t=20, b=60, l=20, r=20), height=280, ) return fig def create_platform_bar_chart(platform_stats: dict) -> go.Figure: """Create horizontal bar chart for platform distribution.""" if not platform_stats: return go.Figure() sorted_items = sorted(platform_stats.items(), key=lambda x: x[1], reverse=True) platforms = [item[0] for item in sorted_items] counts = [item[1] for item in sorted_items] colors = [PLATFORM_COLORS.get(p, "#6B7280") for p in platforms] fig = go.Figure(data=[ go.Bar( y=platforms, x=counts, orientation="h", marker_color=colors, text=[f"{c}건" for c in counts], textposition="auto", ) ]) fig.update_layout( title="", xaxis_title="넛지 후보 수", yaxis=dict(autorange="reversed"), margin=dict(t=20, b=40, l=100, r=20), height=max(200, len(platforms) * 35), ) return fig def create_nudge_by_cej_bar_chart(cej_counts: dict) -> go.Figure: """Create horizontal bar chart for nudge distribution by CEJ stage.""" if not cej_counts: return go.Figure() sorted_items = sorted(cej_counts.items(), key=lambda x: x[1], reverse=True) cej_stages = [item[0] for item in sorted_items] counts = [item[1] for item in sorted_items] display_labels = [CEJ_LABELS.get(cej, cej) for cej in cej_stages] fig = go.Figure(data=[ go.Bar( y=display_labels, x=counts, orientation="h", marker_color="#6366F1", text=[f"{c}건" for c in counts], textposition="auto", ) ]) fig.update_layout( title="", xaxis_title="넛지 후보 수", yaxis=dict(autorange="reversed"), margin=dict(t=20, b=40, l=100, r=20), height=max(200, len(cej_stages) * 35), ) return fig def create_brand_sentiment_chart(brands: list[dict]) -> go.Figure: """Create horizontal stacked bar chart for brand sentiment comparison.""" if not brands: return go.Figure() # Sort by total mentions sorted_brands = sorted(brands, key=lambda x: x.get("total_mentions", 0), reverse=True)[:10] brand_names = [] for b in sorted_brands: name = b.get("brand_name", "Unknown") brand_type = b.get("brand_type", "") prefix = "🏠 " if brand_type == "IN_HOUSE" else "🏢 " brand_names.append(f"{prefix}{name}") positive_rates = [b.get("positive_rate", 0) for b in sorted_brands] negative_rates = [b.get("negative_rate", 0) for b in sorted_brands] neutral_rates = [100 - p - n for p, n in zip(positive_rates, negative_rates)] fig = go.Figure() fig.add_trace(go.Bar( y=brand_names, x=positive_rates, name="긍정", orientation="h", marker_color="#10B981", text=[f"{v:.1f}%" for v in positive_rates], textposition="inside", )) fig.add_trace(go.Bar( y=brand_names, x=neutral_rates, name="중립", orientation="h", marker_color="#E5E7EB", text=[f"{v:.1f}%" for v in neutral_rates], textposition="inside", )) fig.add_trace(go.Bar( y=brand_names, x=negative_rates, name="부정", orientation="h", marker_color="#EF4444", text=[f"{v:.1f}%" for v in negative_rates], textposition="inside", )) fig.update_layout( title="", barmode="stack", xaxis_title="비율 (%)", legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), margin=dict(t=50, b=50, l=150, r=20), height=max(300, len(sorted_brands) * 40), yaxis=dict(autorange="reversed"), ) return fig def create_domain_bar_chart(domain_counts: dict) -> go.Figure: """Create horizontal bar chart for citation domain distribution.""" if not domain_counts: return go.Figure() # Take top 15 domains sorted_items = list(domain_counts.items())[:15] domains = [item[0] for item in sorted_items] counts = [item[1] for item in sorted_items] # Truncate long domain names display_domains = [d[:40] + "..." if len(d) > 40 else d for d in domains] fig = go.Figure(data=[ go.Bar( y=display_domains, x=counts, orientation="h", marker_color="#3B82F6", text=[f"{c}회" for c in counts], textposition="auto", ) ]) fig.update_layout( title="", xaxis_title="인용 횟수", yaxis=dict(autorange="reversed"), margin=dict(t=20, b=40, l=200, r=20), height=max(300, len(sorted_items) * 30), ) return fig def create_bit_quadrant_chart(bit_stats: dict) -> go.Figure: """Create bar chart for BIT quadrant distribution.""" if not bit_stats: return go.Figure() BIT_LABELS = { "neutral": "중립", "product_satisfaction": "제품 만족", "unmet_expectations": "기대 미충족", "brand_trust": "브랜드 신뢰", } sorted_items = sorted(bit_stats.items(), key=lambda x: x[1], reverse=True) quadrants = [BIT_LABELS.get(item[0], item[0]) for item in sorted_items] counts = [item[1] for item in sorted_items] colors = ["#6366F1", "#8B5CF6", "#A855F7", "#D946EF"] fig = go.Figure(data=[ go.Bar( x=quadrants, y=counts, marker_color=colors[:len(quadrants)], text=[f"{c}건" for c in counts], textposition="outside", ) ]) fig.update_layout( title="", yaxis_title="넛지 후보 수", margin=dict(t=20, b=50, l=50, r=20), height=280, ) return fig def create_emotion_distribution_chart(emotion_stats: dict) -> go.Figure: """Create bar chart for emotion distribution in nudge candidates.""" if not emotion_stats: return go.Figure() sorted_items = sorted(emotion_stats.items(), key=lambda x: x[1], reverse=True)[:8] emotions = [EMOTION_KO.get(item[0], item[0]) for item in sorted_items] counts = [item[1] for item in sorted_items] colors = ["#6366F1", "#8B5CF6", "#A855F7", "#D946EF", "#EC4899", "#F43F5E", "#F97316", "#FBBF24"] fig = go.Figure(data=[ go.Bar( x=emotions, y=counts, marker_color=colors[:len(emotions)], text=[f"{c}건" for c in counts], textposition="outside", ) ]) fig.update_layout( title="", yaxis_title="넛지 후보 수", xaxis_tickangle=-30, margin=dict(t=20, b=80, l=50, r=20), height=280, ) return fig