Abdullah172 commited on
Commit
7370cf3
·
verified ·
1 Parent(s): 7a056a6

Update charts.py

Browse files
Files changed (1) hide show
  1. charts.py +198 -208
charts.py CHANGED
@@ -2,13 +2,12 @@
2
  charts.py — All Plotly chart builders. Pure functions, no Streamlit imports.
3
  """
4
 
5
- from typing import Dict, List, Tuple, Optional
6
  import plotly.graph_objects as go
7
- import plotly.express as px
8
  import pandas as pd
9
  import numpy as np
10
 
11
- # Shared theme
12
  DARK_BG = "#0d0f14"
13
  CARD_BG = "#13161e"
14
  BORDER = "#1e2330"
@@ -26,27 +25,91 @@ PLOTLY_LAYOUT = dict(
26
  plot_bgcolor="rgba(0,0,0,0)",
27
  font=dict(family="'DM Mono', monospace", color=TEXT_MAIN, size=12),
28
  margin=dict(l=20, r=20, t=40, b=20),
 
 
 
 
 
29
  )
30
 
31
 
32
- # Misinformation Gauge
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  def misinfo_gauge(score: float, label: str) -> go.Figure:
35
  """Gauge chart for misinformation confidence score (0–1)."""
36
  pct = score * 100
 
37
  if score < 0.35:
38
  bar_color = GREEN
 
39
  elif score < 0.65:
40
  bar_color = AMBER
 
41
  else:
42
  bar_color = RED
 
43
 
44
  fig = go.Figure(go.Indicator(
45
  mode="gauge+number+delta",
46
  value=pct,
47
- number={"suffix": "%", "font": {"size": 32, "color": bar_color, "family": "'DM Mono', monospace"}},
48
- delta={"reference": 50, "increasing": {"color": RED}, "decreasing": {"color": GREEN}},
49
- title={"text": label, "font": {"size": 13, "color": TEXT_DIM}},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  gauge={
51
  "axis": {
52
  "range": [0, 100],
@@ -54,13 +117,13 @@ def misinfo_gauge(score: float, label: str) -> go.Figure:
54
  "tickcolor": BORDER,
55
  "tickfont": {"color": TEXT_DIM, "size": 10},
56
  },
57
- "bar": {"color": bar_color, "thickness": 0.3},
58
  "bgcolor": CARD_BG,
59
  "borderwidth": 0,
60
  "steps": [
61
- {"range": [0, 35], "color": "#0d1f18"},
62
  {"range": [35, 65], "color": "#1f1a0d"},
63
- {"range": [65, 100],"color": "#1f0d0d"},
64
  ],
65
  "threshold": {
66
  "line": {"color": TEXT_MAIN, "width": 2},
@@ -69,45 +132,54 @@ def misinfo_gauge(score: float, label: str) -> go.Figure:
69
  },
70
  },
71
  ))
72
- fig.update_layout(**PLOTLY_LAYOUT, height=260)
73
- return fig
74
 
 
 
75
 
76
- # Sentiment Donut ─
77
 
78
  def sentiment_donut(summary: Dict) -> go.Figure:
79
  """Donut chart: Positive / Negative / Neutral breakdown."""
80
- labels = ["Positive", "Neutral", "Negative"]
81
- values = [summary["POSITIVE"], summary["NEUTRAL"], summary["NEGATIVE"]]
82
- colors = [GREEN, TEXT_DIM, RED]
83
 
84
  fig = go.Figure(go.Pie(
85
  labels=labels,
86
  values=values,
87
  hole=0.62,
 
88
  marker=dict(colors=colors, line=dict(color=DARK_BG, width=3)),
89
  textinfo="label+percent",
90
- textfont=dict(family="'DM Mono', monospace", size=11, color=TEXT_MAIN),
91
- hovertemplate="<b>%{label}</b><br>%{value} comments (%{percent})<extra></extra>",
 
 
 
 
 
 
92
  rotation=90,
93
  ))
94
 
95
- # Centre annotation
96
  avg = summary.get("avg_compound", 0)
97
  overall = "😊 Positive" if avg > 0.05 else ("😟 Negative" if avg < -0.05 else "😐 Mixed")
 
98
  fig.add_annotation(
99
  text=f"<b>{overall}</b><br><span style='font-size:11px;color:{TEXT_DIM}'>{summary['total']} comments</span>",
100
- x=0.5, y=0.5,
 
101
  showarrow=False,
102
  font=dict(size=13, color=TEXT_MAIN, family="'DM Mono', monospace"),
103
  align="center",
104
  )
105
- fig.update_layout(**PLOTLY_LAYOUT, height=300,
106
- legend=dict(orientation="h", y=-0.08, font=dict(size=11)))
107
- return fig
108
 
 
 
 
 
 
 
109
 
110
- # Keyword Bar Chart ─
111
 
112
  def keyword_bar(
113
  keywords: List[Tuple[str, float]],
@@ -118,7 +190,6 @@ def keyword_bar(
118
  return _empty_fig(title)
119
 
120
  words, weights = zip(*keywords[:15])
121
- # Normalize to 0-100
122
  max_w = max(weights) or 1
123
  norm = [w / max_w * 100 for w in weights]
124
 
@@ -129,25 +200,24 @@ def keyword_bar(
129
  marker=dict(
130
  color=norm,
131
  colorscale=[[0, f"{color}33"], [1, color]],
132
- line=dict(width=0),
133
  ),
134
  text=[f"{w:.0f}" for w in weights],
135
  textposition="inside",
136
  textfont=dict(size=10, color=DARK_BG),
137
- hovertemplate="<b>%{y}</b><br>Weight: %{text}<extra></extra>",
138
  ))
 
139
  fig.update_layout(
140
  **PLOTLY_LAYOUT,
141
  title=dict(text=title, font=dict(size=13, color=TEXT_DIM), x=0),
142
- height=380,
143
  yaxis=dict(autorange="reversed", tickfont=dict(size=11), gridcolor=BORDER),
144
  xaxis=dict(showticklabels=False, gridcolor=BORDER),
145
  bargap=0.35,
146
  )
147
- return fig
148
 
 
149
 
150
- # Stream Trust Bars ─
151
 
152
  def stream_trust_bars(stream_details: Dict) -> go.Figure:
153
  """Horizontal bar chart for per-stream misinfo scores."""
@@ -159,49 +229,37 @@ def stream_trust_bars(stream_details: Dict) -> go.Figure:
159
  x=values,
160
  y=[l.replace("_", " ").title() for l in labels],
161
  orientation="h",
162
- marker=dict(color=colors, line=dict(width=0)),
163
  text=[f"{v}%" for v in values],
164
  textposition="outside",
165
  textfont=dict(size=11, color=TEXT_MAIN),
166
- hovertemplate="<b>%{y}</b><br>Score: %{x}%<extra></extra>",
167
  ))
 
168
  fig.update_layout(
169
  **PLOTLY_LAYOUT,
170
  title=dict(text="Per-Stream Analysis", font=dict(size=13, color=TEXT_DIM), x=0),
171
- height=220,
172
  xaxis=dict(range=[0, 110], showticklabels=False, gridcolor=BORDER),
173
  yaxis=dict(tickfont=dict(size=11)),
174
  bargap=0.4,
175
  )
176
- return fig
177
 
 
178
 
179
- # Modality Misinformation Distribution ─
180
 
181
  def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure:
182
- """
183
- Grouped bar chart — Misinformation Score vs Not-Misinformation Score per modality.
184
-
185
- Bars are derived directly from the model's per-stream softmax probabilities
186
- (values in ``modality_analysis[modality]["misinfo_pct"]`` /
187
- ``modality_analysis[modality]["credible_pct"]``).
188
- Each pair of bars sums to exactly 100 % because they are complementary
189
- softmax outputs from the same binary classification head.
190
-
191
- Parameters
192
- ----------
193
- modality_analysis : dict
194
- Mapping {"text": {...}, "audio": {...}, "video": {...}} as returned by
195
- ``analyzer._compute_modality_analysis()`` — one sub-dict per stream.
196
- """
197
  MODALITIES = ["Text", "Audio", "Video"]
198
- KEYS = ["text", "audio", "video"]
199
 
200
- misinfo_pcts = [modality_analysis.get(k, {}).get("misinfo_pct", 50.0) for k in KEYS]
201
  credible_pcts = [modality_analysis.get(k, {}).get("credible_pct", 50.0) for k in KEYS]
202
- logit_tips = [
203
- (f"logit_m={modality_analysis.get(k, {}).get('misinfo_logit', 0.0):+.4f} | "
204
- f"logit_c={modality_analysis.get(k, {}).get('credible_logit', 0.0):+.4f}")
 
 
 
205
  for k in KEYS
206
  ]
207
 
@@ -211,18 +269,14 @@ def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure:
211
  name="Misinformation Score",
212
  x=MODALITIES,
213
  y=misinfo_pcts,
214
- marker=dict(
215
- color=[RED, RED, RED],
216
- opacity=0.88,
217
- line=dict(color=DARK_BG, width=1),
218
- ),
219
  text=[f"{v:.1f}%" for v in misinfo_pcts],
220
  textposition="outside",
221
  textfont=dict(size=11, color=RED),
222
  customdata=logit_tips,
223
  hovertemplate=(
224
  "<b>%{x} — Misinformation</b><br>"
225
- "Softmax: %{y:.2f}%<br>"
226
  "%{customdata}<extra></extra>"
227
  ),
228
  ))
@@ -231,97 +285,54 @@ def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure:
231
  name="Not Misinformation",
232
  x=MODALITIES,
233
  y=credible_pcts,
234
- marker=dict(
235
- color=[GREEN, GREEN, GREEN],
236
- opacity=0.88,
237
- line=dict(color=DARK_BG, width=1),
238
- ),
239
  text=[f"{v:.1f}%" for v in credible_pcts],
240
  textposition="outside",
241
  textfont=dict(size=11, color=GREEN),
242
  customdata=logit_tips,
243
  hovertemplate=(
244
  "<b>%{x} — Credible</b><br>"
245
- "Softmax: %{y:.2f}%<br>"
246
  "%{customdata}<extra></extra>"
247
  ),
248
  ))
249
 
250
  fig.update_layout(
251
  **PLOTLY_LAYOUT,
252
- title=dict(
253
- text="Modality Misinformation Distribution",
254
- font=dict(size=13, color=TEXT_DIM),
255
- x=0,
256
- ),
257
  barmode="group",
258
- height=280,
259
- xaxis=dict(
260
- title="Modality",
261
- tickfont=dict(size=12),
262
- gridcolor=BORDER,
263
- ),
264
- yaxis=dict(
265
- title="Softmax Score (%)",
266
- range=[0, 115],
267
- gridcolor=BORDER,
268
- ticksuffix="%",
269
- ),
270
- legend=dict(
271
- orientation="h",
272
- y=1.12,
273
- font=dict(size=11),
274
- bgcolor="rgba(0,0,0,0)",
275
- ),
276
  bargap=0.22,
277
  bargroupgap=0.06,
278
  )
279
- return fig
280
 
 
281
 
282
- # Trust Score by Modality ─
283
 
284
  def trust_score_by_modality(modality_analysis: Dict) -> go.Figure:
285
- """
286
- Vertical bar chart — model's reliability/trustworthiness coefficient per stream.
287
-
288
- Trust is computed as a linear combination of model confidence (1 – Shannon entropy)
289
- and content-richness, both derived from the actual inference pass, never fixed.
290
-
291
- Parameters
292
- ----------
293
- modality_analysis : dict
294
- Same structure as ``modality_misinfo_distribution``.
295
- """
296
  MODALITIES = ["Text", "Audio", "Video"]
297
- KEYS = ["text", "audio", "video"]
298
 
299
  trust_vals = [modality_analysis.get(k, {}).get("trust_score", 0.0) for k in KEYS]
300
- bar_colors = [
301
- (GREEN if v >= 60 else (AMBER if v >= 35 else RED))
302
- for v in trust_vals
303
- ]
304
 
305
  fig = go.Figure(go.Bar(
306
  x=MODALITIES,
307
  y=trust_vals,
308
- marker=dict(
309
- color=bar_colors,
310
- opacity=0.88,
311
- line=dict(color=DARK_BG, width=1),
312
- ),
313
  text=[f"{v:.1f}%" for v in trust_vals],
314
  textposition="outside",
315
  textfont=dict(size=11, color=TEXT_MAIN),
316
  hovertemplate=(
317
  "<b>%{x}</b><br>"
318
- "Trust Level: %{y:.2f}%<br>"
319
- "<i>Derived from (1 H_entropy) × content_richness</i>"
320
- "<extra></extra>"
321
  ),
322
  ))
323
 
324
- # Reference lines
325
  for level, label, color in [(80, "High Trust", GREEN), (50, "Threshold", AMBER)]:
326
  fig.add_hline(
327
  y=level,
@@ -333,84 +344,49 @@ def trust_score_by_modality(modality_analysis: Dict) -> go.Figure:
333
 
334
  fig.update_layout(
335
  **PLOTLY_LAYOUT,
336
- title=dict(
337
- text="Trust Score by Modality",
338
- font=dict(size=13, color=TEXT_DIM),
339
- x=0,
340
- ),
341
- height=280,
342
- xaxis=dict(
343
- title="Modality",
344
- tickfont=dict(size=12),
345
- gridcolor=BORDER,
346
- ),
347
- yaxis=dict(
348
- title="Trust Level (%)",
349
- range=[0, 115],
350
- gridcolor=BORDER,
351
- ticksuffix="%",
352
- ),
353
  bargap=0.38,
354
  )
355
- return fig
356
 
 
357
 
358
- # Uncertainty Analysis
359
 
360
  def uncertainty_analysis(modality_analysis: Dict) -> go.Figure:
361
- """
362
- Vertical bar chart — Shannon entropy of the model's softmax distribution per stream.
363
-
364
- High entropy ( → 100 %) means the model is maximally unsure (uniform distribution).
365
- Low entropy ( → 0 %) means the model is highly confident in its prediction.
366
- Values come directly from H = –Σ p·log₂(p) over the two softmax outputs.
367
-
368
- Parameters
369
- ----------
370
- modality_analysis : dict
371
- Same structure as ``modality_misinfo_distribution``.
372
- """
373
  MODALITIES = ["Text", "Audio", "Video"]
374
- KEYS = ["text", "audio", "video"]
375
 
376
  uncertainty_vals = [modality_analysis.get(k, {}).get("uncertainty", 100.0) for k in KEYS]
377
- misinfo_pcts = [modality_analysis.get(k, {}).get("misinfo_pct", 50.0) for k in KEYS]
378
 
379
- # Colour encodes confidence direction: red = uncertain, green = confident
380
- bar_colors = [
381
- (GREEN if v <= 35 else (AMBER if v <= 65 else RED))
382
- for v in uncertainty_vals
383
- ]
384
 
385
  fig = go.Figure(go.Bar(
386
  x=MODALITIES,
387
  y=uncertainty_vals,
388
- marker=dict(
389
- color=bar_colors,
390
- opacity=0.88,
391
- line=dict(color=DARK_BG, width=1),
392
- ),
393
  text=[f"{v:.1f}%" for v in uncertainty_vals],
394
  textposition="outside",
395
  textfont=dict(size=11, color=TEXT_MAIN),
396
  customdata=[[f"p_misinfo={m:.1f}%"] for m in misinfo_pcts],
397
  hovertemplate=(
398
  "<b>%{x}</b><br>"
399
- "Uncertainty (H): %{y:.2f}%<br>"
400
  "%{customdata[0]}<br>"
401
- "<i>H = –Σ p·log₂(p), normalised to %</i>"
402
- "<extra></extra>"
403
  ),
404
  ))
405
 
406
- # Max-entropy reference
407
  fig.add_hline(
408
  y=100,
409
  line=dict(color=RED, width=1, dash="dot"),
410
- annotation_text="Max Entropy (no signal)",
411
  annotation_position="right",
412
  annotation_font=dict(size=9, color=RED),
413
  )
 
414
  fig.add_hline(
415
  y=50,
416
  line=dict(color=AMBER, width=1, dash="dot"),
@@ -421,46 +397,33 @@ def uncertainty_analysis(modality_analysis: Dict) -> go.Figure:
421
 
422
  fig.update_layout(
423
  **PLOTLY_LAYOUT,
424
- title=dict(
425
- text="Uncertainty Analysis (Shannon Entropy)",
426
- font=dict(size=13, color=TEXT_DIM),
427
- x=0,
428
- ),
429
- height=280,
430
- xaxis=dict(
431
- title="Modality",
432
- tickfont=dict(size=12),
433
- gridcolor=BORDER,
434
- ),
435
- yaxis=dict(
436
- title="Uncertainty (%)",
437
- range=[0, 120],
438
- gridcolor=BORDER,
439
- ticksuffix="%",
440
- ),
441
  bargap=0.38,
442
  )
443
- return fig
444
 
 
445
 
446
- # Comment Sentiment Timeline
447
 
448
  def sentiment_timeline(comments_df: pd.DataFrame, sentiments: List[Dict]) -> go.Figure:
449
- """Scatter: comment likes vs. sentiment compound score."""
450
  if comments_df.empty:
451
  return _empty_fig("Comment Sentiment Distribution")
452
 
453
  df = comments_df.copy()
454
  df["compound"] = [s.get("compound", 0) for s in sentiments]
455
- df["label"] = [s.get("label", "NEUTRAL") for s in sentiments]
456
- df["color"] = df["label"].map({"POSITIVE": GREEN, "NEGATIVE": RED, "NEUTRAL": AMBER})
457
  df["text_short"] = df["text"].str[:80] + "…"
458
 
459
  fig = go.Figure()
 
460
  for lbl, clr in [("POSITIVE", GREEN), ("NEGATIVE", RED), ("NEUTRAL", AMBER)]:
461
  sub = df[df["label"] == lbl]
462
  if sub.empty:
463
  continue
 
464
  fig.add_trace(go.Scatter(
465
  x=sub.index,
466
  y=sub["compound"],
@@ -469,26 +432,37 @@ def sentiment_timeline(comments_df: pd.DataFrame, sentiments: List[Dict]) -> go.
469
  marker=dict(
470
  size=np.clip(np.log1p(sub["likes"].fillna(0)) * 4 + 4, 4, 20),
471
  color=clr,
472
- opacity=0.75,
473
- line=dict(width=0),
474
  ),
475
  text=sub["text_short"],
476
- hovertemplate="<b>%{text}</b><br>Sentiment: %{y:.2f}<br>Likes: %{marker.size}<extra></extra>",
 
 
 
 
 
 
 
 
 
 
 
 
477
  ))
478
 
479
  fig.add_hline(y=0, line=dict(color=BORDER, width=1, dash="dot"))
 
480
  fig.update_layout(
481
  **PLOTLY_LAYOUT,
482
  title=dict(text="Comment Sentiment (size = likes)", font=dict(size=13, color=TEXT_DIM), x=0),
483
- height=320,
484
  xaxis=dict(title="Comment index", gridcolor=BORDER, showgrid=False),
485
  yaxis=dict(title="Compound score", gridcolor=BORDER, range=[-1.1, 1.1]),
486
  legend=dict(orientation="h", y=1.12, font=dict(size=11)),
487
  )
488
- return fig
489
 
 
490
 
491
- # Positive vs Negative Keyword Comparison ─
492
 
493
  def keyword_comparison(
494
  pos_kw: List[Tuple[str, float]],
@@ -507,45 +481,61 @@ def keyword_comparison(
507
  if pos_kw:
508
  pw, pv = zip(*pos_kw)
509
  max_p = max(pv) or 1
 
510
  fig.add_trace(go.Bar(
511
  name="Positive",
512
  y=list(pw),
513
- x=[v/max_p*100 for v in pv],
514
  orientation="h",
515
- marker_color=GREEN,
516
- hovertemplate="<b>%{y}</b><br>Score: %{x:.1f}<extra></extra>",
517
  ))
518
 
519
  if neg_kw:
520
  nw, nv = zip(*neg_kw)
521
  max_n = max(nv) or 1
 
522
  fig.add_trace(go.Bar(
523
  name="Negative",
524
  y=list(nw),
525
- x=[-v/max_n*100 for v in nv],
526
  orientation="h",
527
- marker_color=RED,
528
- hovertemplate="<b>%{y}</b><br>Score: %{x:.1f}<extra></extra>",
529
  ))
530
 
531
  fig.update_layout(
532
  **PLOTLY_LAYOUT,
533
  title=dict(text="Sentiment-Weighted Keywords", font=dict(size=13, color=TEXT_DIM), x=0),
534
- height=360,
535
  barmode="overlay",
536
- xaxis=dict(title="← Negative | Positive →", gridcolor=BORDER, zeroline=True,
537
- zerolinecolor=BORDER, zerolinewidth=2),
 
 
 
 
 
538
  yaxis=dict(tickfont=dict(size=10)),
539
  legend=dict(orientation="h", y=1.1),
540
  )
541
- return fig
542
 
 
543
 
544
- # Helpers ─
545
 
546
  def _empty_fig(title: str) -> go.Figure:
547
  fig = go.Figure()
548
- fig.add_annotation(text="No data available", x=0.5, y=0.5, showarrow=False,
549
- font=dict(size=14, color=TEXT_DIM))
550
- fig.update_layout(**PLOTLY_LAYOUT, title=dict(text=title, x=0), height=250)
551
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
2
  charts.py — All Plotly chart builders. Pure functions, no Streamlit imports.
3
  """
4
 
5
+ from typing import Dict, List, Tuple
6
  import plotly.graph_objects as go
 
7
  import pandas as pd
8
  import numpy as np
9
 
10
+ # Shared theme
11
  DARK_BG = "#0d0f14"
12
  CARD_BG = "#13161e"
13
  BORDER = "#1e2330"
 
25
  plot_bgcolor="rgba(0,0,0,0)",
26
  font=dict(family="'DM Mono', monospace", color=TEXT_MAIN, size=12),
27
  margin=dict(l=20, r=20, t=40, b=20),
28
+ hoverlabel=dict(
29
+ bgcolor=CARD_BG,
30
+ bordercolor=CYAN,
31
+ font=dict(color=TEXT_MAIN, family="'DM Mono', monospace", size=12),
32
+ ),
33
  )
34
 
35
 
36
+ def make_interactive(fig: go.Figure, height: int = 300) -> go.Figure:
37
+ """Apply shared interactive behaviour to every Plotly chart."""
38
+ fig.update_layout(
39
+ height=height,
40
+ hovermode="closest",
41
+ dragmode="zoom",
42
+ transition=dict(duration=450, easing="cubic-in-out"),
43
+ legend=dict(
44
+ itemclick="toggle",
45
+ itemdoubleclick="toggleothers",
46
+ bgcolor="rgba(0,0,0,0)",
47
+ font=dict(size=11, color=TEXT_MAIN),
48
+ ),
49
+ modebar=dict(
50
+ bgcolor="rgba(0,0,0,0)",
51
+ color=TEXT_DIM,
52
+ activecolor=CYAN,
53
+ ),
54
+ )
55
+
56
+ fig.update_xaxes(
57
+ showspikes=True,
58
+ spikecolor=CYAN,
59
+ spikethickness=1,
60
+ spikedash="dot",
61
+ showline=True,
62
+ linecolor=BORDER,
63
+ zerolinecolor=BORDER,
64
+ )
65
+
66
+ fig.update_yaxes(
67
+ showspikes=True,
68
+ spikecolor=CYAN,
69
+ spikethickness=1,
70
+ spikedash="dot",
71
+ showline=True,
72
+ linecolor=BORDER,
73
+ zerolinecolor=BORDER,
74
+ )
75
+
76
+ return fig
77
+
78
 
79
  def misinfo_gauge(score: float, label: str) -> go.Figure:
80
  """Gauge chart for misinformation confidence score (0–1)."""
81
  pct = score * 100
82
+
83
  if score < 0.35:
84
  bar_color = GREEN
85
+ risk_text = "Low Risk"
86
  elif score < 0.65:
87
  bar_color = AMBER
88
+ risk_text = "Medium Risk"
89
  else:
90
  bar_color = RED
91
+ risk_text = "High Risk"
92
 
93
  fig = go.Figure(go.Indicator(
94
  mode="gauge+number+delta",
95
  value=pct,
96
+ number={
97
+ "suffix": "%",
98
+ "font": {
99
+ "size": 34,
100
+ "color": bar_color,
101
+ "family": "'DM Mono', monospace",
102
+ },
103
+ },
104
+ delta={
105
+ "reference": 50,
106
+ "increasing": {"color": RED},
107
+ "decreasing": {"color": GREEN},
108
+ },
109
+ title={
110
+ "text": f"{label}<br><span style='font-size:11px;color:{TEXT_DIM}'>{risk_text}</span>",
111
+ "font": {"size": 13, "color": TEXT_DIM},
112
+ },
113
  gauge={
114
  "axis": {
115
  "range": [0, 100],
 
117
  "tickcolor": BORDER,
118
  "tickfont": {"color": TEXT_DIM, "size": 10},
119
  },
120
+ "bar": {"color": bar_color, "thickness": 0.32},
121
  "bgcolor": CARD_BG,
122
  "borderwidth": 0,
123
  "steps": [
124
+ {"range": [0, 35], "color": "#0d1f18"},
125
  {"range": [35, 65], "color": "#1f1a0d"},
126
+ {"range": [65, 100], "color": "#1f0d0d"},
127
  ],
128
  "threshold": {
129
  "line": {"color": TEXT_MAIN, "width": 2},
 
132
  },
133
  },
134
  ))
 
 
135
 
136
+ fig.update_layout(**PLOTLY_LAYOUT)
137
+ return make_interactive(fig, height=260)
138
 
 
139
 
140
  def sentiment_donut(summary: Dict) -> go.Figure:
141
  """Donut chart: Positive / Negative / Neutral breakdown."""
142
+ labels = ["Positive", "Neutral", "Negative"]
143
+ values = [summary["POSITIVE"], summary["NEUTRAL"], summary["NEGATIVE"]]
144
+ colors = [GREEN, TEXT_DIM, RED]
145
 
146
  fig = go.Figure(go.Pie(
147
  labels=labels,
148
  values=values,
149
  hole=0.62,
150
+ pull=[0.04, 0.02, 0.04],
151
  marker=dict(colors=colors, line=dict(color=DARK_BG, width=3)),
152
  textinfo="label+percent",
153
+ hoverinfo="label+value+percent",
154
+ insidetextorientation="radial",
155
+ textfont=dict(
156
+ family="'DM Mono', monospace",
157
+ size=11,
158
+ color=TEXT_MAIN,
159
+ ),
160
+ hovertemplate="<b>%{label}</b><br>%{value} comments<br>%{percent}<extra></extra>",
161
  rotation=90,
162
  ))
163
 
 
164
  avg = summary.get("avg_compound", 0)
165
  overall = "😊 Positive" if avg > 0.05 else ("😟 Negative" if avg < -0.05 else "😐 Mixed")
166
+
167
  fig.add_annotation(
168
  text=f"<b>{overall}</b><br><span style='font-size:11px;color:{TEXT_DIM}'>{summary['total']} comments</span>",
169
+ x=0.5,
170
+ y=0.5,
171
  showarrow=False,
172
  font=dict(size=13, color=TEXT_MAIN, family="'DM Mono', monospace"),
173
  align="center",
174
  )
 
 
 
175
 
176
+ fig.update_layout(
177
+ **PLOTLY_LAYOUT,
178
+ legend=dict(orientation="h", y=-0.08, font=dict(size=11)),
179
+ )
180
+
181
+ return make_interactive(fig, height=300)
182
 
 
183
 
184
  def keyword_bar(
185
  keywords: List[Tuple[str, float]],
 
190
  return _empty_fig(title)
191
 
192
  words, weights = zip(*keywords[:15])
 
193
  max_w = max(weights) or 1
194
  norm = [w / max_w * 100 for w in weights]
195
 
 
200
  marker=dict(
201
  color=norm,
202
  colorscale=[[0, f"{color}33"], [1, color]],
203
+ line=dict(color=DARK_BG, width=1),
204
  ),
205
  text=[f"{w:.0f}" for w in weights],
206
  textposition="inside",
207
  textfont=dict(size=10, color=DARK_BG),
208
+ hovertemplate="<b>%{y}</b><br>Keyword weight: %{text}<br>Normalised: %{x:.1f}%<extra></extra>",
209
  ))
210
+
211
  fig.update_layout(
212
  **PLOTLY_LAYOUT,
213
  title=dict(text=title, font=dict(size=13, color=TEXT_DIM), x=0),
 
214
  yaxis=dict(autorange="reversed", tickfont=dict(size=11), gridcolor=BORDER),
215
  xaxis=dict(showticklabels=False, gridcolor=BORDER),
216
  bargap=0.35,
217
  )
 
218
 
219
+ return make_interactive(fig, height=380)
220
 
 
221
 
222
  def stream_trust_bars(stream_details: Dict) -> go.Figure:
223
  """Horizontal bar chart for per-stream misinfo scores."""
 
229
  x=values,
230
  y=[l.replace("_", " ").title() for l in labels],
231
  orientation="h",
232
+ marker=dict(color=colors, line=dict(color=DARK_BG, width=1)),
233
  text=[f"{v}%" for v in values],
234
  textposition="outside",
235
  textfont=dict(size=11, color=TEXT_MAIN),
236
+ hovertemplate="<b>%{y}</b><br>Stream score: %{x:.1f}%<extra></extra>",
237
  ))
238
+
239
  fig.update_layout(
240
  **PLOTLY_LAYOUT,
241
  title=dict(text="Per-Stream Analysis", font=dict(size=13, color=TEXT_DIM), x=0),
 
242
  xaxis=dict(range=[0, 110], showticklabels=False, gridcolor=BORDER),
243
  yaxis=dict(tickfont=dict(size=11)),
244
  bargap=0.4,
245
  )
 
246
 
247
+ return make_interactive(fig, height=220)
248
 
 
249
 
250
  def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure:
251
+ """Grouped bar chart — Misinformation Score vs Not-Misinformation Score per modality."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  MODALITIES = ["Text", "Audio", "Video"]
253
+ KEYS = ["text", "audio", "video"]
254
 
255
+ misinfo_pcts = [modality_analysis.get(k, {}).get("misinfo_pct", 50.0) for k in KEYS]
256
  credible_pcts = [modality_analysis.get(k, {}).get("credible_pct", 50.0) for k in KEYS]
257
+
258
+ logit_tips = [
259
+ (
260
+ f"logit_m={modality_analysis.get(k, {}).get('misinfo_logit', 0.0):+.4f} | "
261
+ f"logit_c={modality_analysis.get(k, {}).get('credible_logit', 0.0):+.4f}"
262
+ )
263
  for k in KEYS
264
  ]
265
 
 
269
  name="Misinformation Score",
270
  x=MODALITIES,
271
  y=misinfo_pcts,
272
+ marker=dict(color=[RED, RED, RED], opacity=0.88, line=dict(color=DARK_BG, width=1)),
 
 
 
 
273
  text=[f"{v:.1f}%" for v in misinfo_pcts],
274
  textposition="outside",
275
  textfont=dict(size=11, color=RED),
276
  customdata=logit_tips,
277
  hovertemplate=(
278
  "<b>%{x} — Misinformation</b><br>"
279
+ "Softmax score: %{y:.2f}%<br>"
280
  "%{customdata}<extra></extra>"
281
  ),
282
  ))
 
285
  name="Not Misinformation",
286
  x=MODALITIES,
287
  y=credible_pcts,
288
+ marker=dict(color=[GREEN, GREEN, GREEN], opacity=0.88, line=dict(color=DARK_BG, width=1)),
 
 
 
 
289
  text=[f"{v:.1f}%" for v in credible_pcts],
290
  textposition="outside",
291
  textfont=dict(size=11, color=GREEN),
292
  customdata=logit_tips,
293
  hovertemplate=(
294
  "<b>%{x} — Credible</b><br>"
295
+ "Softmax score: %{y:.2f}%<br>"
296
  "%{customdata}<extra></extra>"
297
  ),
298
  ))
299
 
300
  fig.update_layout(
301
  **PLOTLY_LAYOUT,
302
+ title=dict(text="Modality Misinformation Distribution", font=dict(size=13, color=TEXT_DIM), x=0),
 
 
 
 
303
  barmode="group",
304
+ xaxis=dict(title="Modality", tickfont=dict(size=12), gridcolor=BORDER),
305
+ yaxis=dict(title="Softmax Score (%)", range=[0, 115], gridcolor=BORDER, ticksuffix="%"),
306
+ legend=dict(orientation="h", y=1.12, font=dict(size=11), bgcolor="rgba(0,0,0,0)"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  bargap=0.22,
308
  bargroupgap=0.06,
309
  )
 
310
 
311
+ return make_interactive(fig, height=280)
312
 
 
313
 
314
  def trust_score_by_modality(modality_analysis: Dict) -> go.Figure:
315
+ """Vertical bar chart — model reliability/trustworthiness coefficient per stream."""
 
 
 
 
 
 
 
 
 
 
316
  MODALITIES = ["Text", "Audio", "Video"]
317
+ KEYS = ["text", "audio", "video"]
318
 
319
  trust_vals = [modality_analysis.get(k, {}).get("trust_score", 0.0) for k in KEYS]
320
+ bar_colors = [GREEN if v >= 60 else (AMBER if v >= 35 else RED) for v in trust_vals]
 
 
 
321
 
322
  fig = go.Figure(go.Bar(
323
  x=MODALITIES,
324
  y=trust_vals,
325
+ marker=dict(color=bar_colors, opacity=0.88, line=dict(color=DARK_BG, width=1)),
 
 
 
 
326
  text=[f"{v:.1f}%" for v in trust_vals],
327
  textposition="outside",
328
  textfont=dict(size=11, color=TEXT_MAIN),
329
  hovertemplate=(
330
  "<b>%{x}</b><br>"
331
+ "Trust level: %{y:.2f}%<br>"
332
+ "<i>Derived from confidence and content richness</i><extra></extra>"
 
333
  ),
334
  ))
335
 
 
336
  for level, label, color in [(80, "High Trust", GREEN), (50, "Threshold", AMBER)]:
337
  fig.add_hline(
338
  y=level,
 
344
 
345
  fig.update_layout(
346
  **PLOTLY_LAYOUT,
347
+ title=dict(text="Trust Score by Modality", font=dict(size=13, color=TEXT_DIM), x=0),
348
+ xaxis=dict(title="Modality", tickfont=dict(size=12), gridcolor=BORDER),
349
+ yaxis=dict(title="Trust Level (%)", range=[0, 115], gridcolor=BORDER, ticksuffix="%"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  bargap=0.38,
351
  )
 
352
 
353
+ return make_interactive(fig, height=280)
354
 
 
355
 
356
  def uncertainty_analysis(modality_analysis: Dict) -> go.Figure:
357
+ """Vertical bar chart — Shannon entropy uncertainty per stream."""
 
 
 
 
 
 
 
 
 
 
 
358
  MODALITIES = ["Text", "Audio", "Video"]
359
+ KEYS = ["text", "audio", "video"]
360
 
361
  uncertainty_vals = [modality_analysis.get(k, {}).get("uncertainty", 100.0) for k in KEYS]
362
+ misinfo_pcts = [modality_analysis.get(k, {}).get("misinfo_pct", 50.0) for k in KEYS]
363
 
364
+ bar_colors = [GREEN if v <= 35 else (AMBER if v <= 65 else RED) for v in uncertainty_vals]
 
 
 
 
365
 
366
  fig = go.Figure(go.Bar(
367
  x=MODALITIES,
368
  y=uncertainty_vals,
369
+ marker=dict(color=bar_colors, opacity=0.88, line=dict(color=DARK_BG, width=1)),
 
 
 
 
370
  text=[f"{v:.1f}%" for v in uncertainty_vals],
371
  textposition="outside",
372
  textfont=dict(size=11, color=TEXT_MAIN),
373
  customdata=[[f"p_misinfo={m:.1f}%"] for m in misinfo_pcts],
374
  hovertemplate=(
375
  "<b>%{x}</b><br>"
376
+ "Uncertainty: %{y:.2f}%<br>"
377
  "%{customdata[0]}<br>"
378
+ "<i>H = –Σ p·log₂(p), normalised to %</i><extra></extra>"
 
379
  ),
380
  ))
381
 
 
382
  fig.add_hline(
383
  y=100,
384
  line=dict(color=RED, width=1, dash="dot"),
385
+ annotation_text="Max Entropy",
386
  annotation_position="right",
387
  annotation_font=dict(size=9, color=RED),
388
  )
389
+
390
  fig.add_hline(
391
  y=50,
392
  line=dict(color=AMBER, width=1, dash="dot"),
 
397
 
398
  fig.update_layout(
399
  **PLOTLY_LAYOUT,
400
+ title=dict(text="Uncertainty Analysis (Shannon Entropy)", font=dict(size=13, color=TEXT_DIM), x=0),
401
+ xaxis=dict(title="Modality", tickfont=dict(size=12), gridcolor=BORDER),
402
+ yaxis=dict(title="Uncertainty (%)", range=[0, 120], gridcolor=BORDER, ticksuffix="%"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  bargap=0.38,
404
  )
 
405
 
406
+ return make_interactive(fig, height=280)
407
 
 
408
 
409
  def sentiment_timeline(comments_df: pd.DataFrame, sentiments: List[Dict]) -> go.Figure:
410
+ """Scatter: comment index vs sentiment compound score."""
411
  if comments_df.empty:
412
  return _empty_fig("Comment Sentiment Distribution")
413
 
414
  df = comments_df.copy()
415
  df["compound"] = [s.get("compound", 0) for s in sentiments]
416
+ df["label"] = [s.get("label", "NEUTRAL") for s in sentiments]
417
+ df["color"] = df["label"].map({"POSITIVE": GREEN, "NEGATIVE": RED, "NEUTRAL": AMBER})
418
  df["text_short"] = df["text"].str[:80] + "…"
419
 
420
  fig = go.Figure()
421
+
422
  for lbl, clr in [("POSITIVE", GREEN), ("NEGATIVE", RED), ("NEUTRAL", AMBER)]:
423
  sub = df[df["label"] == lbl]
424
  if sub.empty:
425
  continue
426
+
427
  fig.add_trace(go.Scatter(
428
  x=sub.index,
429
  y=sub["compound"],
 
432
  marker=dict(
433
  size=np.clip(np.log1p(sub["likes"].fillna(0)) * 4 + 4, 4, 20),
434
  color=clr,
435
+ opacity=0.78,
436
+ line=dict(width=1, color=DARK_BG),
437
  ),
438
  text=sub["text_short"],
439
+ customdata=np.stack(
440
+ [
441
+ sub["likes"].fillna(0).astype(str),
442
+ sub["label"].astype(str),
443
+ ],
444
+ axis=-1,
445
+ ),
446
+ hovertemplate=(
447
+ "<b>%{text}</b><br>"
448
+ "Sentiment: %{customdata[1]}<br>"
449
+ "Compound score: %{y:.2f}<br>"
450
+ "Likes: %{customdata[0]}<extra></extra>"
451
+ ),
452
  ))
453
 
454
  fig.add_hline(y=0, line=dict(color=BORDER, width=1, dash="dot"))
455
+
456
  fig.update_layout(
457
  **PLOTLY_LAYOUT,
458
  title=dict(text="Comment Sentiment (size = likes)", font=dict(size=13, color=TEXT_DIM), x=0),
 
459
  xaxis=dict(title="Comment index", gridcolor=BORDER, showgrid=False),
460
  yaxis=dict(title="Compound score", gridcolor=BORDER, range=[-1.1, 1.1]),
461
  legend=dict(orientation="h", y=1.12, font=dict(size=11)),
462
  )
 
463
 
464
+ return make_interactive(fig, height=320)
465
 
 
466
 
467
  def keyword_comparison(
468
  pos_kw: List[Tuple[str, float]],
 
481
  if pos_kw:
482
  pw, pv = zip(*pos_kw)
483
  max_p = max(pv) or 1
484
+
485
  fig.add_trace(go.Bar(
486
  name="Positive",
487
  y=list(pw),
488
+ x=[v / max_p * 100 for v in pv],
489
  orientation="h",
490
+ marker=dict(color=GREEN, line=dict(color=DARK_BG, width=1)),
491
+ hovertemplate="<b>%{y}</b><br>Positive score: %{x:.1f}<extra></extra>",
492
  ))
493
 
494
  if neg_kw:
495
  nw, nv = zip(*neg_kw)
496
  max_n = max(nv) or 1
497
+
498
  fig.add_trace(go.Bar(
499
  name="Negative",
500
  y=list(nw),
501
+ x=[-v / max_n * 100 for v in nv],
502
  orientation="h",
503
+ marker=dict(color=RED, line=dict(color=DARK_BG, width=1)),
504
+ hovertemplate="<b>%{y}</b><br>Negative score: %{x:.1f}<extra></extra>",
505
  ))
506
 
507
  fig.update_layout(
508
  **PLOTLY_LAYOUT,
509
  title=dict(text="Sentiment-Weighted Keywords", font=dict(size=13, color=TEXT_DIM), x=0),
 
510
  barmode="overlay",
511
+ xaxis=dict(
512
+ title="← Negative | Positive →",
513
+ gridcolor=BORDER,
514
+ zeroline=True,
515
+ zerolinecolor=BORDER,
516
+ zerolinewidth=2,
517
+ ),
518
  yaxis=dict(tickfont=dict(size=10)),
519
  legend=dict(orientation="h", y=1.1),
520
  )
 
521
 
522
+ return make_interactive(fig, height=360)
523
 
 
524
 
525
  def _empty_fig(title: str) -> go.Figure:
526
  fig = go.Figure()
527
+
528
+ fig.add_annotation(
529
+ text="No data available",
530
+ x=0.5,
531
+ y=0.5,
532
+ showarrow=False,
533
+ font=dict(size=14, color=TEXT_DIM),
534
+ )
535
+
536
+ fig.update_layout(
537
+ **PLOTLY_LAYOUT,
538
+ title=dict(text=title, x=0),
539
+ )
540
+
541
+ return make_interactive(fig, height=250)