rocky250 commited on
Commit
8db9a33
Β·
verified Β·
1 Parent(s): f52d7fe

Create charts.py

Browse files
Files changed (1) hide show
  1. charts.py +284 -0
charts.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
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"
15
+ TEXT_MAIN = "#e8eaf0"
16
+ TEXT_DIM = "#5a6070"
17
+ CYAN = "#00d4ff"
18
+ GREEN = "#00e5a0"
19
+ RED = "#ff4757"
20
+ AMBER = "#ffb347"
21
+ PURPLE = "#b388ff"
22
+ BLUE = "#4a8eff"
23
+
24
+ PLOTLY_LAYOUT = dict(
25
+ paper_bgcolor="rgba(0,0,0,0)",
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],
53
+ "tickwidth": 1,
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},
67
+ "thickness": 0.75,
68
+ "value": pct,
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]],
114
+ title: str = "Top Keywords",
115
+ color: str = CYAN,
116
+ ) -> go.Figure:
117
+ if not keywords:
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
+
125
+ fig = go.Figure(go.Bar(
126
+ x=norm,
127
+ y=words,
128
+ orientation="h",
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."""
154
+ labels = list(stream_details.keys())
155
+ values = [round(v * 100, 1) for v in stream_details.values()]
156
+ colors = [RED if v > 50 else (AMBER if v > 30 else GREEN) for v in values]
157
+
158
+ fig = go.Figure(go.Bar(
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
+ # ── Comment Sentiment Timeline ────────────────────────────────────────────────
180
+
181
+ def sentiment_timeline(comments_df: pd.DataFrame, sentiments: List[Dict]) -> go.Figure:
182
+ """Scatter: comment likes vs. sentiment compound score."""
183
+ if comments_df.empty:
184
+ return _empty_fig("Comment Sentiment Distribution")
185
+
186
+ df = comments_df.copy()
187
+ df["compound"] = [s.get("compound", 0) for s in sentiments]
188
+ df["label"] = [s.get("label", "NEUTRAL") for s in sentiments]
189
+ df["color"] = df["label"].map({"POSITIVE": GREEN, "NEGATIVE": RED, "NEUTRAL": AMBER})
190
+ df["text_short"] = df["text"].str[:80] + "…"
191
+
192
+ fig = go.Figure()
193
+ for lbl, clr in [("POSITIVE", GREEN), ("NEGATIVE", RED), ("NEUTRAL", AMBER)]:
194
+ sub = df[df["label"] == lbl]
195
+ if sub.empty:
196
+ continue
197
+ fig.add_trace(go.Scatter(
198
+ x=sub.index,
199
+ y=sub["compound"],
200
+ mode="markers",
201
+ name=lbl,
202
+ marker=dict(
203
+ size=np.clip(np.log1p(sub["likes"].fillna(0)) * 4 + 4, 4, 20),
204
+ color=clr,
205
+ opacity=0.75,
206
+ line=dict(width=0),
207
+ ),
208
+ text=sub["text_short"],
209
+ hovertemplate="<b>%{text}</b><br>Sentiment: %{y:.2f}<br>Likes: %{marker.size}<extra></extra>",
210
+ ))
211
+
212
+ fig.add_hline(y=0, line=dict(color=BORDER, width=1, dash="dot"))
213
+ fig.update_layout(
214
+ **PLOTLY_LAYOUT,
215
+ title=dict(text="Comment Sentiment (size = likes)", font=dict(size=13, color=TEXT_DIM), x=0),
216
+ height=320,
217
+ xaxis=dict(title="Comment index", gridcolor=BORDER, showgrid=False),
218
+ yaxis=dict(title="Compound score", gridcolor=BORDER, range=[-1.1, 1.1]),
219
+ legend=dict(orientation="h", y=1.12, font=dict(size=11)),
220
+ )
221
+ return fig
222
+
223
+
224
+ # ── Positive vs Negative Keyword Comparison ───────────────────────────────────
225
+
226
+ def keyword_comparison(
227
+ pos_kw: List[Tuple[str, float]],
228
+ neg_kw: List[Tuple[str, float]],
229
+ ) -> go.Figure:
230
+ """Diverging bar chart: positive keywords right, negative left."""
231
+ if not pos_kw and not neg_kw:
232
+ return _empty_fig("Sentiment Keywords")
233
+
234
+ top = 10
235
+ pos_kw = pos_kw[:top]
236
+ neg_kw = neg_kw[:top]
237
+
238
+ fig = go.Figure()
239
+
240
+ if pos_kw:
241
+ pw, pv = zip(*pos_kw)
242
+ max_p = max(pv) or 1
243
+ fig.add_trace(go.Bar(
244
+ name="Positive",
245
+ y=list(pw),
246
+ x=[v/max_p*100 for v in pv],
247
+ orientation="h",
248
+ marker_color=GREEN,
249
+ hovertemplate="<b>%{y}</b><br>Score: %{x:.1f}<extra></extra>",
250
+ ))
251
+
252
+ if neg_kw:
253
+ nw, nv = zip(*neg_kw)
254
+ max_n = max(nv) or 1
255
+ fig.add_trace(go.Bar(
256
+ name="Negative",
257
+ y=list(nw),
258
+ x=[-v/max_n*100 for v in nv],
259
+ orientation="h",
260
+ marker_color=RED,
261
+ hovertemplate="<b>%{y}</b><br>Score: %{x:.1f}<extra></extra>",
262
+ ))
263
+
264
+ fig.update_layout(
265
+ **PLOTLY_LAYOUT,
266
+ title=dict(text="Sentiment-Weighted Keywords", font=dict(size=13, color=TEXT_DIM), x=0),
267
+ height=360,
268
+ barmode="overlay",
269
+ xaxis=dict(title="← Negative | Positive β†’", gridcolor=BORDER, zeroline=True,
270
+ zerolinecolor=BORDER, zerolinewidth=2),
271
+ yaxis=dict(tickfont=dict(size=10)),
272
+ legend=dict(orientation="h", y=1.1),
273
+ )
274
+ return fig
275
+
276
+
277
+ # ── Helpers ───────────────────────────────────────────────────────────────────
278
+
279
+ def _empty_fig(title: str) -> go.Figure:
280
+ fig = go.Figure()
281
+ fig.add_annotation(text="No data available", x=0.5, y=0.5, showarrow=False,
282
+ font=dict(size=14, color=TEXT_DIM))
283
+ fig.update_layout(**PLOTLY_LAYOUT, title=dict(text=title, x=0), height=250)
284
+ return fig