Spaces:
Sleeping
Sleeping
| 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="<b>%{label}</b><br>%{value} comments (%{percent})<extra></extra>", | |
| 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"<b>{overall}</b><br><span style='font-size:11px;color:{TEXT_DIM}'>{summary['total']} comments</span>", | |
| 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="<b>%{y}</b><br>Weight: %{text}<extra></extra>", | |
| )) | |
| 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="<b>%{y}</b><br>Score: %{x}%<extra></extra>", | |
| )) | |
| 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=( | |
| "<b>%{x} — Misinformation</b><br>" | |
| "Softmax: %{y:.2f}%<br>" | |
| "%{customdata}<extra></extra>" | |
| ), | |
| )) | |
| 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=( | |
| "<b>%{x} — Credible</b><br>" | |
| "Softmax: %{y:.2f}%<br>" | |
| "%{customdata}<extra></extra>" | |
| ), | |
| )) | |
| 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=( | |
| "<b>%{x}</b><br>" | |
| "Trust Level: %{y:.2f}%<br>" | |
| "<i>Derived from (1 – H_entropy) × content_richness</i>" | |
| "<extra></extra>" | |
| ), | |
| )) | |
| 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=( | |
| "<b>%{x}</b><br>" | |
| "Uncertainty (H): %{y:.2f}%<br>" | |
| "%{customdata[0]}<br>" | |
| "<i>H = –Σ p·log₂(p), normalised to %</i>" | |
| "<extra></extra>" | |
| ), | |
| )) | |
| 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="<b>%{text}</b><br>Sentiment: %{y:.2f}<br>Likes: %{marker.size}<extra></extra>", | |
| )) | |
| 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="<b>%{y}</b><br>Score: %{x:.1f}<extra></extra>", | |
| )) | |
| 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="<b>%{y}</b><br>Score: %{x:.1f}<extra></extra>", | |
| )) | |
| 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 |