rocky250 commited on
Commit
c76064b
·
verified ·
1 Parent(s): e8a0f51

Update charts.py

Browse files
Files changed (1) hide show
  1. charts.py +208 -198
charts.py CHANGED
@@ -2,12 +2,13 @@
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,91 +26,27 @@ PLOTLY_LAYOUT = dict(
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,13 +54,13 @@ def misinfo_gauge(score: float, label: str) -> go.Figure:
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,54 +69,45 @@ def misinfo_gauge(score: float, label: str) -> go.Figure:
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,6 +118,7 @@ def keyword_bar(
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,24 +129,25 @@ def keyword_bar(
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,37 +159,49 @@ def stream_trust_bars(stream_details: Dict) -> go.Figure:
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,14 +211,18 @@ def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure:
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,54 +231,97 @@ def modality_misinfo_distribution(modality_analysis: Dict) -> go.Figure:
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,49 +333,84 @@ def trust_score_by_modality(modality_analysis: Dict) -> go.Figure:
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,33 +421,46 @@ def uncertainty_analysis(modality_analysis: Dict) -> go.Figure:
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,37 +469,26 @@ def sentiment_timeline(comments_df: pd.DataFrame, sentiments: List[Dict]) -> go.
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,61 +507,45 @@ def keyword_comparison(
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)
 
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
  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
  "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
  },
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
  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
  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
  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
  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
  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
 
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
 
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
  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
  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