from typing import Dict, List, Tuple, Optional import plotly.graph_objects as go import plotly.express as px import pandas as pd import numpy as np CREAM = "#FFFFE3" CARD_BG = "#FFFFFF" BORDER = "#BDDDFC" TEXT_MAIN = "#4A4A4A" TEXT_DIM = "#7b7b7b" INK_DARK = "#384959" PRIMARY = "#269ccc" STORMY_SKY = "#88BDF2" STORMY_SLATE = "#6A89A7" INK_GREY = "#CBCBCB" GREEN = "#2e9e6b" RED = "#c0392b" AMBER = "#d4841a" PURPLE = "#7C6DB5" BLUE = "#269ccc" PLOTLY_LAYOUT = dict( paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(189,221,252,0.13)", font=dict(family="'DM Mono', monospace", color=TEXT_MAIN, size=12), margin=dict(l=20, r=20, t=40, b=20), ) def misinfo_gauge(score: float, label: str) -> go.Figure: pct = score * 100 if score < 0.35: bar_color = GREEN elif score < 0.65: bar_color = AMBER else: bar_color = RED fig = go.Figure(go.Indicator( mode="gauge+number+delta", value=pct, number={"suffix": "%", "font": {"size": 32, "color": bar_color, "family": "'DM Mono', monospace"}}, delta={"reference": 50, "increasing": {"color": RED}, "decreasing": {"color": GREEN}}, title={"text": label, "font": {"size": 13, "color": TEXT_DIM}}, gauge={ "axis": { "range": [0, 100], "tickwidth": 1, "tickcolor": BORDER, "tickfont": {"color": TEXT_DIM, "size": 10}, }, "bar": {"color": bar_color, "thickness": 0.3}, "bgcolor": CARD_BG, "borderwidth": 0, "steps": [ {"range": [0, 35], "color": "#e8f7ef"}, {"range": [35, 65], "color": "#fdf3e3"}, {"range": [65, 100], "color": "#fdecea"}, ], "threshold": { "line": {"color": INK_DARK, "width": 2}, "thickness": 0.75, "value": pct, }, }, )) fig.update_layout(**PLOTLY_LAYOUT, height=260) return fig def sentiment_donut(summary: Dict) -> go.Figure: labels = ["Positively Engagement", "Neutral", "Negatively Engagement"] values = [summary["POSITIVE"], summary["NEUTRAL"], summary["NEGATIVE"]] colors = [STORMY_SKY, INK_GREY, STORMY_SLATE] fig = go.Figure(go.Pie( labels=labels, values=values, hole=0.62, marker=dict(colors=colors, line=dict(color=CARD_BG, width=3)), textinfo="label+percent", textfont=dict(family="'DM Mono', monospace", size=11, color=TEXT_MAIN), hovertemplate="%{label}
%{value} comments (%{percent})", rotation=90, )) avg = summary.get("avg_compound", 0) overall = "๐Ÿ˜Š Positive" if avg > 0.05 else ("๐Ÿ˜Ÿ Negative" if avg < -0.05 else "๐Ÿ˜ Mixed") fig.add_annotation( text=f"{overall}
{summary['total']} comments", x=0.5, y=0.5, showarrow=False, font=dict(size=13, color=TEXT_MAIN, family="'DM Mono', monospace"), align="center", ) fig.update_layout( **PLOTLY_LAYOUT, height=300, legend=dict(orientation="h", y=-0.08, font=dict(size=11, color=TEXT_MAIN)), ) return fig def keyword_bar( keywords: List[Tuple[str, float]], title: str = "Top Keywords", color: str = PRIMARY, ) -> go.Figure: if not keywords: return _empty_fig(title) words, weights = zip(*keywords[:15]) max_w = max(weights) or 1 norm = [w / max_w * 100 for w in weights] fig = go.Figure(go.Bar( x=norm, y=words, orientation="h", marker=dict( color=norm, colorscale=[[0, f"{color}44"], [1, color]], line=dict(width=0), ), text=[f"{w:.0f}" for w in weights], textposition="inside", textfont=dict(size=10, color=CARD_BG), hovertemplate="%{y}
Weight: %{text}", )) fig.update_layout( **PLOTLY_LAYOUT, title=dict(text=title, font=dict(size=13, color=INK_DARK), x=0), height=380, yaxis=dict( autorange="reversed", tickfont=dict(size=11, color=TEXT_MAIN), gridcolor=BORDER, ), xaxis=dict(showticklabels=False, gridcolor=BORDER), bargap=0.35, ) return fig def stream_trust_bars(stream_details: Dict) -> go.Figure: labels = list(stream_details.keys()) values = [round(v * 100, 1) for v in stream_details.values()] colors = [RED if v > 50 else (AMBER if v > 30 else GREEN) for v in values] fig = go.Figure(go.Bar( x=values, y=[l.replace("_", " ").title() for l in labels], orientation="h", marker=dict(color=colors, line=dict(color=CARD_BG, width=1)), text=[f"{v}%" for v in values], textposition="outside", textfont=dict(size=11, color=TEXT_MAIN), hovertemplate="%{y}
Score: %{x}%", )) fig.update_layout( **PLOTLY_LAYOUT, title=dict(text="Per-Stream Analysis", font=dict(size=13, color=INK_DARK), x=0), height=220, xaxis=dict(range=[0, 120], showticklabels=False, gridcolor=BORDER), yaxis=dict(tickfont=dict(size=11, color=TEXT_MAIN)), bargap=0.4, ) return fig def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure: MODALITIES = ["Text", "Audio", "Video"] KEYS = ["text", "audio", "video"] misinfo_pcts = [modality_analysis.get(k, {}).get("misinfo_pct", 50.0) for k in KEYS] credible_pcts = [modality_analysis.get(k, {}).get("credible_pct", 50.0) for k in KEYS] logit_tips = [ (f"logit_m={modality_analysis.get(k, {}).get('misinfo_logit', 0.0):+.4f} | " f"logit_c={modality_analysis.get(k, {}).get('credible_logit', 0.0):+.4f}") for k in KEYS ] fig = go.Figure() fig.add_trace(go.Bar( name="Misinformation Score", x=MODALITIES, y=misinfo_pcts, marker=dict( color=[RED, RED, RED], opacity=0.90, line=dict(color=CARD_BG, width=2), ), text=[f"{v:.1f}%" for v in misinfo_pcts], textposition="outside", textfont=dict(size=11, color=RED, family="'DM Mono', monospace"), customdata=logit_tips, hovertemplate=( "%{x} โ€” Misinformation
" "Softmax: %{y:.2f}%
" "%{customdata}" ), )) fig.add_trace(go.Bar( name="Not Misinformation", x=MODALITIES, y=credible_pcts, marker=dict( color=[GREEN, GREEN, GREEN], opacity=0.90, line=dict(color=CARD_BG, width=2), ), text=[f"{v:.1f}%" for v in credible_pcts], textposition="outside", textfont=dict(size=11, color=GREEN, family="'DM Mono', monospace"), customdata=logit_tips, hovertemplate=( "%{x} โ€” Credible
" "Softmax: %{y:.2f}%
" "%{customdata}" ), )) fig.update_layout( **PLOTLY_LAYOUT, title=dict( text="Modality Misinformation Distribution", font=dict(size=13, color=INK_DARK), x=0, ), barmode="group", height=300, xaxis=dict( title=dict(text="Modality", font=dict(color=TEXT_MAIN)), tickfont=dict(size=12, color=TEXT_MAIN), gridcolor=BORDER, ), yaxis=dict( title=dict(text="Softmax Score (%)", font=dict(color=TEXT_MAIN)), range=[0, 125], gridcolor=BORDER, ticksuffix="%", tickfont=dict(color=TEXT_MAIN), ), legend=dict( orientation="h", y=1.14, font=dict(size=11, color=TEXT_MAIN), bgcolor="rgba(0,0,0,0)", ), bargap=0.22, bargroupgap=0.06, ) return fig def trust_score_by_modality(modality_analysis: Dict) -> go.Figure: MODALITIES = ["Text", "Audio", "Video"] KEYS = ["text", "audio", "video"] trust_vals = [modality_analysis.get(k, {}).get("trust_score", 0.0) for k in KEYS] bar_colors = [ (GREEN if v >= 60 else (AMBER if v >= 35 else RED)) for v in trust_vals ] fig = go.Figure(go.Bar( x=MODALITIES, y=trust_vals, marker=dict( color=bar_colors, opacity=0.90, line=dict(color=CARD_BG, width=2), ), text=[f"{v:.1f}%" for v in trust_vals], textposition="outside", textfont=dict(size=11, color=TEXT_MAIN, family="'DM Mono', monospace"), hovertemplate=( "%{x}
" "Trust Level: %{y:.2f}%
" "Derived from (1 โ€“ H_entropy) ร— content_richness" "" ), )) for level, label, color in [(80, "High Trust", GREEN), (50, "Threshold", AMBER)]: fig.add_hline( y=level, line=dict(color=color, width=1.5, dash="dot"), annotation_text=label, annotation_position="right", annotation_font=dict(size=9, color=color), ) fig.update_layout( **PLOTLY_LAYOUT, title=dict( text="Trust Score by Modality", font=dict(size=13, color=INK_DARK), x=0, ), height=300, xaxis=dict( title=dict(text="Modality", font=dict(color=TEXT_MAIN)), tickfont=dict(size=12, color=TEXT_MAIN), gridcolor=BORDER, ), yaxis=dict( title=dict(text="Trust Level (%)", font=dict(color=TEXT_MAIN)), range=[0, 120], gridcolor=BORDER, ticksuffix="%", tickfont=dict(color=TEXT_MAIN), ), bargap=0.38, ) return fig def uncertainty_analysis(modality_analysis: Dict) -> go.Figure: MODALITIES = ["Text", "Audio", "Video"] KEYS = ["text", "audio", "video"] uncertainty_vals = [modality_analysis.get(k, {}).get("uncertainty", 100.0) for k in KEYS] misinfo_pcts = [modality_analysis.get(k, {}).get("misinfo_pct", 50.0) for k in KEYS] bar_colors = [ (GREEN if v <= 35 else (AMBER if v <= 65 else RED)) for v in uncertainty_vals ] fig = go.Figure(go.Bar( x=MODALITIES, y=uncertainty_vals, marker=dict( color=bar_colors, opacity=0.90, line=dict(color=CARD_BG, width=2), ), text=[f"{v:.1f}%" for v in uncertainty_vals], textposition="outside", textfont=dict(size=11, color=TEXT_MAIN, family="'DM Mono', monospace"), customdata=[[f"p_misinfo={m:.1f}%"] for m in misinfo_pcts], hovertemplate=( "%{x}
" "Uncertainty (H): %{y:.2f}%
" "%{customdata[0]}
" "H = โ€“ฮฃ pยทlogโ‚‚(p), normalised to %" "" ), )) fig.add_hline( y=100, line=dict(color=RED, width=1.5, dash="dot"), annotation_text="Max Entropy (no signal)", annotation_position="right", annotation_font=dict(size=9, color=RED), ) fig.add_hline( y=50, line=dict(color=AMBER, width=1.5, dash="dot"), annotation_text="Mid Uncertainty", annotation_position="right", annotation_font=dict(size=9, color=AMBER), ) fig.update_layout( **PLOTLY_LAYOUT, title=dict( text="Uncertainty Analysis (Shannon Entropy)", font=dict(size=13, color=INK_DARK), x=0, ), height=300, xaxis=dict( title=dict(text="Modality", font=dict(color=TEXT_MAIN)), tickfont=dict(size=12, color=TEXT_MAIN), gridcolor=BORDER, ), yaxis=dict( title=dict(text="Uncertainty (%)", font=dict(color=TEXT_MAIN)), range=[0, 130], gridcolor=BORDER, ticksuffix="%", tickfont=dict(color=TEXT_MAIN), ), bargap=0.38, ) return fig def sentiment_timeline(comments_df: pd.DataFrame, sentiments: List[Dict]) -> go.Figure: if comments_df.empty: return _empty_fig("Comment Sentiment Distribution") df = comments_df.copy() df["compound"] = [s.get("compound", 0) for s in sentiments] df["label"] = [s.get("label", "NEUTRAL") for s in sentiments] df["color"] = df["label"].map({"POSITIVE": STORMY_SKY, "NEGATIVE": STORMY_SLATE, "NEUTRAL": INK_GREY}) df["text_short"] = df["text"].str[:80] + "โ€ฆ" fig = go.Figure() for lbl, clr, disp in [ ("POSITIVE", STORMY_SKY, "Positively Engagement"), ("NEGATIVE", STORMY_SLATE, "Negatively Engagement"), ("NEUTRAL", INK_GREY, "Neutral"), ]: sub = df[df["label"] == lbl] if sub.empty: continue fig.add_trace(go.Scatter( x=sub.index, y=sub["compound"], mode="markers", name=disp, marker=dict( size=np.clip(np.log1p(sub["likes"].fillna(0)) * 4 + 4, 4, 20), color=clr, opacity=0.80, line=dict(width=0), ), text=sub["text_short"], hovertemplate="%{text}
Sentiment: %{y:.2f}
Likes: %{marker.size}", )) fig.add_hline(y=0, line=dict(color=BORDER, width=1, dash="dot")) fig.update_layout( **PLOTLY_LAYOUT, title=dict(text="Comment Sentiment (size = likes)", font=dict(size=13, color=INK_DARK), x=0), height=320, xaxis=dict(title="Comment index", gridcolor=BORDER, showgrid=False, tickfont=dict(color=TEXT_MAIN)), yaxis=dict(title="Compound score", gridcolor=BORDER, range=[-1.1, 1.1], tickfont=dict(color=TEXT_MAIN)), legend=dict(orientation="h", y=1.14, font=dict(size=11, color=TEXT_MAIN)), ) return fig def keyword_comparison( pos_kw: List[Tuple[str, float]], neg_kw: List[Tuple[str, float]], ) -> go.Figure: if not pos_kw and not neg_kw: return _empty_fig("Sentiment Keywords") top = 10 pos_kw = pos_kw[:top] neg_kw = neg_kw[:top] fig = go.Figure() if pos_kw: pw, pv = zip(*pos_kw) max_p = max(pv) or 1 fig.add_trace(go.Bar( name="Positively Engagement", y=list(pw), x=[v / max_p * 100 for v in pv], orientation="h", marker_color=STORMY_SKY, hovertemplate="%{y}
Score: %{x:.1f}", )) if neg_kw: nw, nv = zip(*neg_kw) max_n = max(nv) or 1 fig.add_trace(go.Bar( name="Negatively Engagement", y=list(nw), x=[-v / max_n * 100 for v in nv], orientation="h", marker_color=STORMY_SLATE, hovertemplate="%{y}
Score: %{x:.1f}", )) fig.update_layout( **PLOTLY_LAYOUT, title=dict(text="Sentiment-Weighted Keywords", font=dict(size=13, color=INK_DARK), x=0), height=360, barmode="overlay", xaxis=dict( title="โ† Negatively Engagement | Positively Engagement โ†’", gridcolor=BORDER, zeroline=True, zerolinecolor=INK_DARK, zerolinewidth=2, tickfont=dict(color=TEXT_MAIN), ), yaxis=dict(tickfont=dict(size=10, color=TEXT_MAIN)), legend=dict(orientation="h", y=1.1, font=dict(size=11, color=TEXT_MAIN)), ) return fig def _empty_fig(title: str) -> go.Figure: fig = go.Figure() fig.add_annotation( text="No data available", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color=TEXT_DIM), ) fig.update_layout(**PLOTLY_LAYOUT, title=dict(text=title, x=0, font=dict(color=INK_DARK)), height=250) return fig