MHMisinfo / charts.py
rocky250's picture
Update charts.py
d43adf3 verified
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