Spaces:
Running
Running
Update ui/analytics_plot_generator.py
Browse files- ui/analytics_plot_generator.py +189 -431
ui/analytics_plot_generator.py
CHANGED
@@ -5,8 +5,8 @@ from io import BytesIO
|
|
5 |
import base64
|
6 |
import numpy as np
|
7 |
import matplotlib.ticker as mticker
|
8 |
-
import matplotlib.patches as patches
|
9 |
-
import ast
|
10 |
from data_processing.analytics_data_processing import (
|
11 |
generate_chatbot_data_summaries,
|
12 |
prepare_filtered_analytics_data
|
@@ -15,51 +15,52 @@ from data_processing.analytics_data_processing import (
|
|
15 |
# Configure logging for this module
|
16 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
17 |
|
18 |
-
def _apply_theme_aware_styling(fig, ax):
|
19 |
"""
|
20 |
-
|
21 |
-
It reads colors from rcParams, which Gradio
|
22 |
-
This makes text, backgrounds, and grids adapt to light/dark mode.
|
23 |
"""
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
|
|
|
|
63 |
|
64 |
|
65 |
def create_placeholder_plot(title="No Data or Plot Error", message="Data might be empty or an error occurred."):
|
@@ -68,125 +69,133 @@ def create_placeholder_plot(title="No Data or Plot Error", message="Data might b
|
|
68 |
fig, ax = plt.subplots(figsize=(8, 4))
|
69 |
_apply_theme_aware_styling(fig, ax)
|
70 |
|
71 |
-
|
72 |
-
|
73 |
-
ax.text(0.5, 0.5, f"{title}\n{message}", ha='center', va='center', fontsize=10, wrap=True, zorder=1, color=THEME_TEXT_COLOR)
|
74 |
ax.axis('off')
|
75 |
-
fig.
|
76 |
return fig
|
77 |
except Exception as e:
|
78 |
logging.error(f"Error creating placeholder plot: {e}")
|
79 |
fig_err, ax_err = plt.subplots(figsize=(8,4))
|
80 |
-
fig_err.patch.
|
81 |
-
ax_err.
|
82 |
ax_err.text(0.5, 0.5, "Fatal: Plot generation error", ha='center', va='center', zorder=1, color='red')
|
83 |
ax_err.axis('off')
|
84 |
return fig_err
|
85 |
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
|
|
|
|
90 |
|
91 |
fig = None
|
92 |
try:
|
93 |
df_copy = df.copy()
|
94 |
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
|
95 |
-
df_copy =
|
|
|
96 |
if df_copy.empty:
|
97 |
-
return create_placeholder_plot(title=
|
98 |
|
99 |
-
|
100 |
-
if
|
101 |
-
return create_placeholder_plot(title=
|
102 |
|
103 |
fig, ax = plt.subplots(figsize=(10, 5))
|
104 |
_apply_theme_aware_styling(fig, ax)
|
105 |
|
106 |
-
|
107 |
-
ax.
|
108 |
-
ax.
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
112 |
return fig
|
113 |
except Exception as e:
|
114 |
-
logging.error(f"Error generating
|
115 |
if fig: plt.close(fig)
|
116 |
-
return create_placeholder_plot(title="
|
117 |
|
118 |
|
119 |
-
def
|
120 |
-
"""
|
121 |
-
if
|
122 |
-
return create_placeholder_plot(title=
|
123 |
|
124 |
fig = None
|
125 |
try:
|
126 |
-
|
127 |
-
|
128 |
-
df_copy = df_copy.dropna(subset=[date_column])
|
129 |
-
if df_copy.empty:
|
130 |
-
return create_placeholder_plot(title="Mentions Activity Over Time", message="No valid date entries found.")
|
131 |
|
132 |
-
|
133 |
-
|
134 |
-
return create_placeholder_plot(title="Mentions Activity Over Time", message="No mentions in the selected period.")
|
135 |
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
|
|
145 |
return fig
|
146 |
except Exception as e:
|
147 |
-
logging.error(f"Error generating
|
148 |
if fig: plt.close(fig)
|
149 |
-
return create_placeholder_plot(title="
|
150 |
|
151 |
-
def
|
152 |
-
"""
|
153 |
-
if
|
154 |
-
return create_placeholder_plot(title=
|
155 |
|
156 |
fig = None
|
157 |
try:
|
158 |
-
|
159 |
-
|
160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
|
162 |
-
fig, ax = plt.subplots(figsize=(8, 5))
|
163 |
-
_apply_theme_aware_styling(fig, ax)
|
164 |
-
|
165 |
-
THEME_TEXT_COLOR = plt.rcParams.get('text.color', 'black')
|
166 |
-
pie_slice_colors = plt.cm.get_cmap('Pastel2', len(sentiment_counts))
|
167 |
-
|
168 |
-
wedges, texts, autotexts = ax.pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%', startangle=90,
|
169 |
-
colors=[pie_slice_colors(i) for i in range(len(sentiment_counts))])
|
170 |
-
|
171 |
-
# Set text colors to be theme-aware
|
172 |
for text_item in texts + autotexts:
|
173 |
text_item.set_color(THEME_TEXT_COLOR)
|
|
|
174 |
text_item.set_zorder(2)
|
175 |
-
for wedge in wedges:
|
176 |
-
wedge.set_zorder(1)
|
177 |
|
178 |
-
|
179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
180 |
return fig
|
181 |
except Exception as e:
|
182 |
-
logging.error(f"Error generating
|
183 |
if fig: plt.close(fig)
|
184 |
-
return create_placeholder_plot(title="
|
|
|
|
|
185 |
|
186 |
def generate_followers_count_over_time_plot(df, **kwargs):
|
187 |
-
"""Generates a theme-aware plot for followers count over time."""
|
188 |
type_value = kwargs.get('type_value', 'follower_gains_monthly')
|
189 |
-
title = f"Followers Count Over Time
|
190 |
if df is None or df.empty:
|
191 |
return create_placeholder_plot(title=title, message="No follower data available.")
|
192 |
|
@@ -195,7 +204,7 @@ def generate_followers_count_over_time_plot(df, **kwargs):
|
|
195 |
df_filtered = df[df['follower_count_type'] == type_value].copy()
|
196 |
if df_filtered.empty:
|
197 |
return create_placeholder_plot(title=title, message=f"No data for type '{type_value}'.")
|
198 |
-
|
199 |
df_filtered['datetime_obj'] = pd.to_datetime(df_filtered['category_name'], errors='coerce')
|
200 |
df_filtered['follower_count_organic'] = pd.to_numeric(df_filtered['follower_count_organic'], errors='coerce').fillna(0)
|
201 |
df_filtered['follower_count_paid'] = pd.to_numeric(df_filtered['follower_count_paid'], errors='coerce').fillna(0)
|
@@ -205,89 +214,34 @@ def generate_followers_count_over_time_plot(df, **kwargs):
|
|
205 |
|
206 |
fig, ax = plt.subplots(figsize=(10, 5))
|
207 |
_apply_theme_aware_styling(fig, ax)
|
208 |
-
|
209 |
-
ax.plot(df_filtered['datetime_obj'], df_filtered['follower_count_organic'], marker='o', linestyle='-', color='dodgerblue', label='Organic Followers', zorder=1)
|
210 |
-
ax.plot(df_filtered['datetime_obj'], df_filtered['follower_count_paid'], marker='x', linestyle='--', color='seagreen', label='Paid Followers', zorder=1)
|
211 |
-
ax.set_xlabel('Date')
|
212 |
-
ax.set_ylabel('Follower Count')
|
213 |
-
|
214 |
-
legend = ax.legend()
|
215 |
-
if legend:
|
216 |
-
for text in legend.get_texts():
|
217 |
-
text.set_color(plt.rcParams.get('text.color', 'black'))
|
218 |
-
legend.set_zorder(2)
|
219 |
-
|
220 |
-
plt.xticks(rotation=45)
|
221 |
-
fig.tight_layout(pad=0.5)
|
222 |
-
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
|
223 |
-
return fig
|
224 |
-
except Exception as e:
|
225 |
-
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
226 |
-
if fig: plt.close(fig)
|
227 |
-
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
228 |
|
229 |
-
|
230 |
-
|
231 |
-
type_value = kwargs.get('type_value', 'follower_gains_monthly')
|
232 |
-
title = f"Follower Growth Rate ({type_value})"
|
233 |
-
if df is None or df.empty:
|
234 |
-
return create_placeholder_plot(title=title, message="No follower data available.")
|
235 |
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
if df_filtered.empty:
|
240 |
-
return create_placeholder_plot(title=title, message=f"No data for type '{type_value}'.")
|
241 |
-
df_filtered['datetime_obj'] = pd.to_datetime(df_filtered['category_name'], errors='coerce')
|
242 |
-
df_filtered['follower_count_organic'] = pd.to_numeric(df_filtered['follower_count_organic'], errors='coerce')
|
243 |
-
df_filtered['follower_count_paid'] = pd.to_numeric(df_filtered['follower_count_paid'], errors='coerce')
|
244 |
-
df_filtered = df_filtered.dropna(subset=['datetime_obj']).sort_values(by='datetime_obj').set_index('datetime_obj')
|
245 |
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
|
253 |
-
fig, ax = plt.subplots(figsize=(10, 5))
|
254 |
-
_apply_theme_aware_styling(fig, ax)
|
255 |
-
|
256 |
-
plotted = False
|
257 |
-
if not df_filtered['organic_growth_rate'].dropna().empty:
|
258 |
-
ax.plot(df_filtered.index, df_filtered['organic_growth_rate'], marker='o', linestyle='-', color='lightcoral', label='Organic Growth Rate', zorder=1)
|
259 |
-
plotted = True
|
260 |
-
if not df_filtered['paid_growth_rate'].dropna().empty:
|
261 |
-
ax.plot(df_filtered.index, df_filtered['paid_growth_rate'], marker='x', linestyle='--', color='mediumpurple', label='Paid Growth Rate', zorder=1)
|
262 |
-
plotted = True
|
263 |
-
|
264 |
-
if not plotted:
|
265 |
-
return create_placeholder_plot(title=title, message="No growth rate data to display.")
|
266 |
|
267 |
-
|
268 |
-
|
269 |
-
ax.yaxis.set_major_formatter(mticker.PercentFormatter())
|
270 |
-
|
271 |
-
legend = ax.legend()
|
272 |
-
if legend:
|
273 |
-
for text in legend.get_texts():
|
274 |
-
text.set_color(plt.rcParams.get('text.color', 'black'))
|
275 |
-
legend.set_zorder(2)
|
276 |
-
|
277 |
-
plt.xticks(rotation=45)
|
278 |
-
fig.tight_layout(pad=0.5)
|
279 |
-
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
|
280 |
return fig
|
281 |
except Exception as e:
|
282 |
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
283 |
if fig: plt.close(fig)
|
284 |
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
285 |
|
|
|
286 |
def generate_followers_by_demographics_plot(df, **kwargs):
|
287 |
-
"""Generates a theme-aware bar plot for followers by demographics."""
|
288 |
plot_title = kwargs.get('plot_title', "Followers by Demographics")
|
289 |
type_value = kwargs.get('type_value')
|
290 |
-
category_col = 'category_name'
|
291 |
|
292 |
if df is None or df.empty or not type_value:
|
293 |
return create_placeholder_plot(title=plot_title, message="No data or demographic type not specified.")
|
@@ -297,229 +251,33 @@ def generate_followers_by_demographics_plot(df, **kwargs):
|
|
297 |
df_filtered = df[df['follower_count_type'] == type_value].copy()
|
298 |
if df_filtered.empty:
|
299 |
return create_placeholder_plot(title=plot_title, message=f"No data for type '{type_value}'.")
|
300 |
-
|
301 |
df_filtered['follower_count_organic'] = pd.to_numeric(df_filtered['follower_count_organic'], errors='coerce').fillna(0)
|
302 |
-
|
303 |
-
demographics_data =
|
304 |
-
demographics_data['total_for_sort'] = demographics_data.sum(axis=1)
|
305 |
-
demographics_data = demographics_data.sort_values(by='total_for_sort', ascending=False).head(10).drop(columns=['total_for_sort'])
|
306 |
|
307 |
if demographics_data.empty:
|
308 |
return create_placeholder_plot(title=plot_title, message="No demographic data to display.")
|
309 |
-
|
310 |
-
fig, ax = plt.subplots(figsize=(12, 7))
|
311 |
-
_apply_theme_aware_styling(fig, ax)
|
312 |
-
|
313 |
-
demographics_data.plot(kind='bar', ax=ax, zorder=1, width=0.8, color=['dodgerblue', 'seagreen'])
|
314 |
-
ax.set_xlabel(category_col.replace('_', ' ').title())
|
315 |
-
ax.set_ylabel('Number of Followers')
|
316 |
-
|
317 |
-
legend = ax.legend(['Organic', 'Paid'])
|
318 |
-
if legend:
|
319 |
-
for text in legend.get_texts():
|
320 |
-
text.set_color(plt.rcParams.get('text.color', 'black'))
|
321 |
-
legend.set_zorder(2)
|
322 |
-
|
323 |
-
plt.xticks(rotation=45, ha="right")
|
324 |
-
fig.tight_layout(pad=0.5)
|
325 |
-
fig.subplots_adjust(top=0.92, bottom=0.25, left=0.1, right=0.95)
|
326 |
-
return fig
|
327 |
-
except Exception as e:
|
328 |
-
logging.error(f"Error generating {plot_title}: {e}", exc_info=True)
|
329 |
-
if fig: plt.close(fig)
|
330 |
-
return create_placeholder_plot(title=f"{plot_title} Error", message=str(e))
|
331 |
|
332 |
-
|
333 |
-
"""Generic function to create a theme-aware time series plot."""
|
334 |
-
if df is None or df.empty or date_column not in df.columns or value_column not in df.columns:
|
335 |
-
return create_placeholder_plot(title=title, message="No data available.")
|
336 |
-
|
337 |
-
fig = None
|
338 |
-
try:
|
339 |
-
df_copy = df.copy()
|
340 |
-
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
|
341 |
-
df_copy[value_column] = pd.to_numeric(df_copy[value_column], errors='coerce')
|
342 |
-
df_copy = df_copy.dropna(subset=[date_column, value_column]).set_index(date_column)
|
343 |
-
if df_copy.empty:
|
344 |
-
return create_placeholder_plot(title=title, message="No valid data.")
|
345 |
-
|
346 |
-
data_over_time = df_copy.resample('D')[value_column].sum()
|
347 |
-
if data_over_time.empty:
|
348 |
-
return create_placeholder_plot(title=title, message="No data in the selected period.")
|
349 |
-
|
350 |
-
fig, ax = plt.subplots(figsize=(10, 5))
|
351 |
-
_apply_theme_aware_styling(fig, ax)
|
352 |
-
|
353 |
-
ax.plot(data_over_time.index, data_over_time.values, marker='.', linestyle='-', color=color, zorder=1)
|
354 |
-
ax.set_title(title)
|
355 |
-
ax.set_xlabel('Date')
|
356 |
-
ax.set_ylabel(ylabel)
|
357 |
-
plt.xticks(rotation=45)
|
358 |
-
fig.tight_layout(pad=0.5)
|
359 |
-
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
|
360 |
-
return fig
|
361 |
except Exception as e:
|
362 |
-
logging.error(f"Error
|
363 |
if fig: plt.close(fig)
|
364 |
-
return create_placeholder_plot(title=f"{
|
365 |
|
366 |
def generate_engagement_rate_over_time_plot(df, date_column='published_at', engagement_rate_col='engagement'):
|
367 |
-
"""Generates a theme-aware plot for engagement rate with special y-axis formatting."""
|
368 |
title = "Engagement Rate Over Time"
|
369 |
-
|
370 |
-
|
371 |
-
fig = None
|
372 |
-
try:
|
373 |
-
df_copy = df.copy()
|
374 |
-
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
|
375 |
-
df_copy[engagement_rate_col] = pd.to_numeric(df_copy[engagement_rate_col], errors='coerce')
|
376 |
-
df_copy = df_copy.dropna(subset=[date_column, engagement_rate_col])
|
377 |
-
|
378 |
-
if df_copy.empty:
|
379 |
-
return create_placeholder_plot(title=title, message="No valid data.")
|
380 |
-
|
381 |
-
engagement_over_time = df_copy.set_index(date_column).resample('D')[engagement_rate_col].mean().dropna()
|
382 |
-
|
383 |
-
if engagement_over_time.empty:
|
384 |
-
return create_placeholder_plot(title=title, message="No data to display.")
|
385 |
-
|
386 |
-
fig, ax = plt.subplots(figsize=(10,5))
|
387 |
-
_apply_theme_aware_styling(fig,ax)
|
388 |
-
|
389 |
-
ax.plot(engagement_over_time.index, engagement_over_time.values, marker='.', linestyle='-', color='darkorange', zorder=1)
|
390 |
-
|
391 |
-
# Determine the correct formatter based on the data's scale
|
392 |
-
max_rate = engagement_over_time.max()
|
393 |
-
formatter_xmax = 1.0 if max_rate <= 1.5 else 100.0
|
394 |
-
ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=formatter_xmax))
|
395 |
-
|
396 |
-
ax.set_title(title)
|
397 |
-
ax.set_xlabel('Date')
|
398 |
-
ax.set_ylabel('Engagement Rate')
|
399 |
-
plt.xticks(rotation=45)
|
400 |
-
fig.tight_layout(pad=0.5)
|
401 |
-
return fig
|
402 |
-
except Exception as e:
|
403 |
-
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
404 |
-
if fig: plt.close(fig)
|
405 |
-
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
406 |
-
|
407 |
-
def generate_reach_over_time_plot(df, **kwargs):
|
408 |
-
return generate_generic_time_series_plot(df, 'published_at', 'clickCount', 'Reach Over Time (Clicks)', 'Total Clicks', color='mediumseagreen')
|
409 |
-
|
410 |
-
def generate_impressions_over_time_plot(df, **kwargs):
|
411 |
-
return generate_generic_time_series_plot(df, 'published_at', 'impressionCount', 'Impressions Over Time', 'Total Impressions', color='slateblue')
|
412 |
-
|
413 |
-
def generate_likes_over_time_plot(df, **kwargs):
|
414 |
-
return generate_generic_time_series_plot(df, 'published_at', 'likeCount', 'Reactions (Likes) Over Time', 'Total Likes', color='crimson')
|
415 |
-
|
416 |
-
def generate_clicks_over_time_plot(df, **kwargs):
|
417 |
-
return generate_generic_time_series_plot(df, 'published_at', 'clickCount', 'Clicks Over Time', 'Total Clicks', color='mediumseagreen')
|
418 |
-
|
419 |
-
def generate_shares_over_time_plot(df, **kwargs):
|
420 |
-
return generate_generic_time_series_plot(df, 'published_at', 'shareCount', 'Shares Over Time', 'Total Shares', color='teal')
|
421 |
-
|
422 |
-
def generate_comments_over_time_plot(df, **kwargs):
|
423 |
-
return generate_generic_time_series_plot(df, 'published_at', 'commentCount', 'Comments Over Time', 'Total Comments', color='gold')
|
424 |
-
|
425 |
-
def generate_comments_sentiment_breakdown_plot(df, sentiment_column='comment_sentiment', **kwargs):
|
426 |
-
"""Generates a theme-aware pie chart for comment sentiment."""
|
427 |
-
title = "Breakdown of Comments by Sentiment"
|
428 |
-
if df is None or df.empty or sentiment_column not in df.columns:
|
429 |
-
return create_placeholder_plot(title=title, message="No data available.")
|
430 |
-
|
431 |
-
fig = None
|
432 |
-
try:
|
433 |
-
sentiment_counts = df[sentiment_column].value_counts().dropna()
|
434 |
-
if sentiment_counts.empty:
|
435 |
-
return create_placeholder_plot(title=title, message="No sentiment data available.")
|
436 |
-
|
437 |
-
fig, ax = plt.subplots(figsize=(8, 5))
|
438 |
-
_apply_theme_aware_styling(fig, ax)
|
439 |
-
|
440 |
-
THEME_TEXT_COLOR = plt.rcParams.get('text.color', 'black')
|
441 |
-
pie_slice_colors = plt.cm.get_cmap('coolwarm', len(sentiment_counts))
|
442 |
-
wedges, texts, autotexts = ax.pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%', startangle=90, colors=[pie_slice_colors(i) for i in range(len(sentiment_counts))])
|
443 |
-
|
444 |
-
for text_item in texts + autotexts:
|
445 |
-
text_item.set_color(THEME_TEXT_COLOR)
|
446 |
-
|
447 |
-
ax.set_title(title)
|
448 |
-
ax.axis('equal')
|
449 |
-
fig.subplots_adjust(top=0.95, bottom=0.05, left=0.05, right=0.95)
|
450 |
-
return fig
|
451 |
-
except Exception as e:
|
452 |
-
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
453 |
-
if fig: plt.close(fig)
|
454 |
-
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
455 |
-
|
456 |
-
def generate_post_frequency_plot(df, date_column='published_at', **kwargs):
|
457 |
-
"""Generates a theme-aware plot for post frequency, using .size() for counting."""
|
458 |
-
title = "Post Frequency Over Time"
|
459 |
-
if df is None or df.empty or date_column not in df.columns:
|
460 |
-
return create_placeholder_plot(title=title, message="No data available.")
|
461 |
-
|
462 |
-
fig = None
|
463 |
-
try:
|
464 |
-
df_copy = df.copy()
|
465 |
-
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
|
466 |
-
df_copy = df_copy.dropna(subset=[date_column]).set_index(date_column)
|
467 |
-
if df_copy.empty:
|
468 |
-
return create_placeholder_plot(title=title, message="No valid data.")
|
469 |
-
|
470 |
-
data_over_time = df_copy.resample('D').size() # Use size() to count posts
|
471 |
-
if data_over_time.empty:
|
472 |
-
return create_placeholder_plot(title=title, message="No data in the selected period.")
|
473 |
-
|
474 |
-
fig, ax = plt.subplots(figsize=(10, 5))
|
475 |
-
_apply_theme_aware_styling(fig, ax)
|
476 |
-
|
477 |
-
ax.plot(data_over_time.index, data_over_time.values, marker='.', linestyle='-', zorder=1)
|
478 |
-
ax.set_title(title)
|
479 |
-
ax.set_xlabel('Date')
|
480 |
-
ax.set_ylabel('Number of Posts')
|
481 |
-
plt.xticks(rotation=45)
|
482 |
-
fig.tight_layout(pad=0.5)
|
483 |
-
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
|
484 |
-
return fig
|
485 |
-
except Exception as e:
|
486 |
-
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
487 |
-
if fig: plt.close(fig)
|
488 |
-
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
489 |
|
490 |
def generate_content_format_breakdown_plot(df, format_col='media_type', **kwargs):
|
491 |
-
|
492 |
-
title = "Breakdown of Content by Format"
|
493 |
if df is None or df.empty or format_col not in df.columns:
|
494 |
return create_placeholder_plot(title=title, message="No data available.")
|
495 |
-
|
496 |
-
fig = None
|
497 |
-
try:
|
498 |
-
format_counts = df[format_col].value_counts().dropna()
|
499 |
-
if format_counts.empty:
|
500 |
-
return create_placeholder_plot(title=title, message="No format data.")
|
501 |
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
format_counts.plot(kind='bar', ax=ax, zorder=1, color=plt.cm.get_cmap('viridis')(np.linspace(0, 1, len(format_counts))))
|
506 |
-
ax.set_title(title)
|
507 |
-
ax.set_xlabel('Media Type')
|
508 |
-
ax.set_ylabel('Number of Posts')
|
509 |
-
plt.xticks(rotation=45, ha="right")
|
510 |
-
|
511 |
-
# Add text labels with theme color
|
512 |
-
TEXT_COLOR = plt.rcParams.get('text.color', 'black')
|
513 |
-
for i, v in enumerate(format_counts):
|
514 |
-
ax.text(i, v + (0.01 * format_counts.max()), str(v), ha='center', va='bottom', zorder=2, color=TEXT_COLOR)
|
515 |
-
|
516 |
-
fig.tight_layout(pad=0.5)
|
517 |
-
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.15, right=0.95)
|
518 |
-
return fig
|
519 |
-
except Exception as e:
|
520 |
-
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
521 |
-
if fig: plt.close(fig)
|
522 |
-
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
523 |
|
524 |
def _parse_eb_label(label_data):
|
525 |
if isinstance(label_data, list): return label_data
|
@@ -532,51 +290,41 @@ def _parse_eb_label(label_data):
|
|
532 |
return [] if pd.isna(label_data) else [str(label_data)]
|
533 |
|
534 |
def generate_content_topic_breakdown_plot(df, topics_col='li_eb_labels', **kwargs):
|
535 |
-
|
536 |
-
title = "Breakdown of Content by Topics (Top 15)"
|
537 |
if df is None or df.empty or topics_col not in df.columns:
|
538 |
return create_placeholder_plot(title=title, message="No data available.")
|
539 |
-
|
540 |
-
fig = None
|
541 |
try:
|
542 |
topic_counts = df[topics_col].apply(_parse_eb_label).explode().dropna().value_counts()
|
543 |
-
topic_counts = topic_counts[topic_counts.index != '']
|
544 |
if topic_counts.empty:
|
545 |
return create_placeholder_plot(title=title, message="No topic data found.")
|
546 |
-
|
547 |
-
top_topics = topic_counts.nlargest(15).sort_values(ascending=True)
|
548 |
|
549 |
fig, ax = plt.subplots(figsize=(10, 8))
|
550 |
-
_apply_theme_aware_styling(fig,ax)
|
551 |
-
|
552 |
-
|
553 |
-
|
|
|
554 |
ax.set_xlabel('Number of Posts')
|
555 |
ax.set_ylabel('Topic')
|
556 |
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
fig.tight_layout(pad=0.5)
|
563 |
-
fig.subplots_adjust(top=0.92, bottom=0.1, left=0.3, right=0.95)
|
564 |
return fig
|
565 |
except Exception as e:
|
566 |
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
567 |
-
if fig: plt.close(fig)
|
568 |
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
569 |
|
|
|
570 |
def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date, current_plot_configs):
|
571 |
-
""
|
572 |
-
Main function to generate all analytics plots based on provided data and configurations.
|
573 |
-
Uses a dictionary-based approach for cleaner execution.
|
574 |
-
"""
|
575 |
-
logging.info(f"Updating analytics plot figures for theme-aware plotting. Filter: {date_filter_option}")
|
576 |
num_expected_plots = len(current_plot_configs)
|
577 |
|
578 |
-
|
579 |
-
|
580 |
if not token_state_value or not token_state_value.get("token"):
|
581 |
message = "❌ Accesso negato. Nessun token. Impossibile generare le analisi."
|
582 |
logging.warning(message)
|
@@ -600,27 +348,37 @@ def update_analytics_plots_figures(token_state_value, date_filter_option, custom
|
|
600 |
summaries = {p_cfg["id"]: f"Errore preparazione dati: {e}" for p_cfg in current_plot_configs}
|
601 |
return [error_msg] + placeholder_figs + [summaries]
|
602 |
|
|
|
603 |
# Map plot IDs to their respective generation functions
|
604 |
plot_functions = {
|
|
|
605 |
"followers_count": lambda: generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'),
|
606 |
-
"followers_growth_rate": lambda:
|
607 |
"followers_by_location": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Follower per Località"),
|
608 |
"followers_by_role": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Follower per Ruolo"),
|
609 |
"followers_by_industry": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Follower per Settore"),
|
610 |
"followers_by_seniority": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Follower per Anzianità"),
|
|
|
|
|
611 |
"engagement_rate": lambda: generate_engagement_rate_over_time_plot(filtered_merged_posts_df),
|
612 |
-
"reach_over_time": lambda:
|
613 |
-
"impressions_over_time": lambda:
|
614 |
-
"likes_over_time": lambda:
|
615 |
-
|
616 |
-
|
617 |
-
"
|
618 |
-
"
|
619 |
-
"
|
|
|
|
|
|
|
|
|
620 |
"content_format_breakdown_cs": lambda: generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=token_state_value.get("config_media_type_col", "media_type")),
|
621 |
"content_topic_breakdown_cs": lambda: generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=token_state_value.get("config_eb_labels_col", "li_eb_labels")),
|
622 |
-
|
623 |
-
|
|
|
|
|
624 |
}
|
625 |
|
626 |
plot_figs = []
|
|
|
5 |
import base64
|
6 |
import numpy as np
|
7 |
import matplotlib.ticker as mticker
|
8 |
+
import matplotlib.patches as patches
|
9 |
+
import ast
|
10 |
from data_processing.analytics_data_processing import (
|
11 |
generate_chatbot_data_summaries,
|
12 |
prepare_filtered_analytics_data
|
|
|
15 |
# Configure logging for this module
|
16 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
|
17 |
|
18 |
+
def _apply_theme_aware_styling(fig, ax, is_pie=False):
|
19 |
"""
|
20 |
+
Applies a modern, theme-aware style to a Matplotlib plot.
|
21 |
+
It reads colors from rcParams, which Gradio sets based on the theme.
|
|
|
22 |
"""
|
23 |
+
try:
|
24 |
+
# Use a modern, clean style as a base
|
25 |
+
plt.style.use('seaborn-v0_8-whitegrid')
|
26 |
+
|
27 |
+
# Get theme-aware colors from Matplotlib's runtime configuration
|
28 |
+
TEXT_COLOR = plt.rcParams.get('text.color', '#E5E7EB') # Default to light gray for dark themes
|
29 |
+
GRID_COLOR = plt.rcParams.get('grid.color', '#4B5563') # Default to a darker grid
|
30 |
+
FACE_COLOR = plt.rcParams.get('axes.facecolor', '#1F2937') # Default to dark gray
|
31 |
+
EDGE_COLOR = plt.rcParams.get('axes.edgecolor', '#374151') # Default to a slightly lighter gray
|
32 |
+
FIG_FACE_COLOR = plt.rcParams.get('figure.facecolor', '#111827') # Default to very dark gray
|
33 |
+
|
34 |
+
fig.set_facecolor(FIG_FACE_COLOR)
|
35 |
+
ax.set_facecolor(FACE_COLOR)
|
36 |
+
|
37 |
+
# Apply the theme's text color to all major text elements.
|
38 |
+
ax.title.set_color(TEXT_COLOR)
|
39 |
+
ax.xaxis.label.set_color(TEXT_COLOR)
|
40 |
+
ax.yaxis.label.set_color(TEXT_COLOR)
|
41 |
+
|
42 |
+
# Apply the theme's text color to the tick labels and tick marks.
|
43 |
+
ax.tick_params(axis='x', colors=TEXT_COLOR)
|
44 |
+
ax.tick_params(axis='y', colors=TEXT_COLOR)
|
45 |
+
|
46 |
+
# Remove spines for a cleaner look
|
47 |
+
if not is_pie:
|
48 |
+
ax.spines['top'].set_visible(False)
|
49 |
+
ax.spines['right'].set_visible(False)
|
50 |
+
ax.spines['bottom'].set_color(EDGE_COLOR)
|
51 |
+
ax.spines['left'].set_color(EDGE_COLOR)
|
52 |
+
else:
|
53 |
+
ax.spines['top'].set_visible(False)
|
54 |
+
ax.spines['right'].set_visible(False)
|
55 |
+
ax.spines['bottom'].set_visible(False)
|
56 |
+
ax.spines['left'].set_visible(False)
|
57 |
+
|
58 |
+
|
59 |
+
# Set grid color and ensure it's drawn behind data
|
60 |
+
ax.grid(True, linestyle='--', alpha=0.6, zorder=0, color=GRID_COLOR)
|
61 |
+
|
62 |
+
except Exception as e:
|
63 |
+
logging.error(f"Error applying theme styling: {e}")
|
64 |
|
65 |
|
66 |
def create_placeholder_plot(title="No Data or Plot Error", message="Data might be empty or an error occurred."):
|
|
|
69 |
fig, ax = plt.subplots(figsize=(8, 4))
|
70 |
_apply_theme_aware_styling(fig, ax)
|
71 |
|
72 |
+
TEXT_COLOR = plt.rcParams.get('text.color', '#E5E7EB')
|
73 |
+
ax.text(0.5, 0.5, f"{title}\n{message}", ha='center', va='center', fontsize=12, wrap=True, zorder=1, color=TEXT_COLOR, alpha=0.7)
|
|
|
74 |
ax.axis('off')
|
75 |
+
fig.tight_layout()
|
76 |
return fig
|
77 |
except Exception as e:
|
78 |
logging.error(f"Error creating placeholder plot: {e}")
|
79 |
fig_err, ax_err = plt.subplots(figsize=(8,4))
|
80 |
+
fig_err.patch.set_facecolor('#111827')
|
81 |
+
ax_err.set_facecolor('#1F2937')
|
82 |
ax_err.text(0.5, 0.5, "Fatal: Plot generation error", ha='center', va='center', zorder=1, color='red')
|
83 |
ax_err.axis('off')
|
84 |
return fig_err
|
85 |
|
86 |
+
# --- Generic and Reusable Plotting Functions ---
|
87 |
+
|
88 |
+
def generate_generic_time_series_plot(df, date_column, value_column, title, ylabel, color='cyan'):
|
89 |
+
"""Generic function to create a theme-aware time series plot."""
|
90 |
+
if df is None or df.empty or date_column not in df.columns or value_column not in df.columns:
|
91 |
+
return create_placeholder_plot(title=title, message="No data available.")
|
92 |
|
93 |
fig = None
|
94 |
try:
|
95 |
df_copy = df.copy()
|
96 |
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
|
97 |
+
df_copy[value_column] = pd.to_numeric(df_copy[value_column], errors='coerce')
|
98 |
+
df_copy = df_copy.dropna(subset=[date_column, value_column]).set_index(date_column)
|
99 |
if df_copy.empty:
|
100 |
+
return create_placeholder_plot(title=title, message="No valid data.")
|
101 |
|
102 |
+
data_over_time = df_copy.resample('D')[value_column].sum()
|
103 |
+
if data_over_time.empty:
|
104 |
+
return create_placeholder_plot(title=title, message="No data in the selected period.")
|
105 |
|
106 |
fig, ax = plt.subplots(figsize=(10, 5))
|
107 |
_apply_theme_aware_styling(fig, ax)
|
108 |
|
109 |
+
ax.plot(data_over_time.index, data_over_time.values, marker='o', linestyle='-', color=color, zorder=1, markersize=5, alpha=0.8)
|
110 |
+
ax.fill_between(data_over_time.index, data_over_time.values, color=color, alpha=0.1, zorder=1)
|
111 |
+
ax.set_title(title, fontsize=14, weight='bold')
|
112 |
+
ax.set_xlabel('Date', fontsize=10)
|
113 |
+
ax.set_ylabel(ylabel, fontsize=10)
|
114 |
+
plt.xticks(rotation=30, ha="right")
|
115 |
+
fig.tight_layout(pad=1.5)
|
116 |
return fig
|
117 |
except Exception as e:
|
118 |
+
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
119 |
if fig: plt.close(fig)
|
120 |
+
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
121 |
|
122 |
|
123 |
+
def generate_generic_bar_plot(data_series, title, xlabel, ylabel, color_map='viridis'):
|
124 |
+
"""Generic function to create a theme-aware bar plot."""
|
125 |
+
if data_series is None or data_series.empty:
|
126 |
+
return create_placeholder_plot(title=title, message="No data to display.")
|
127 |
|
128 |
fig = None
|
129 |
try:
|
130 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
131 |
+
_apply_theme_aware_styling(fig, ax)
|
|
|
|
|
|
|
132 |
|
133 |
+
colors = plt.cm.get_cmap(color_map)(np.linspace(0.4, 0.9, len(data_series)))
|
134 |
+
data_series.plot(kind='bar', ax=ax, zorder=2, color=colors, width=0.8)
|
|
|
135 |
|
136 |
+
ax.set_title(title, fontsize=14, weight='bold')
|
137 |
+
ax.set_xlabel(xlabel, fontsize=10)
|
138 |
+
ax.set_ylabel(ylabel, fontsize=10)
|
139 |
+
plt.xticks(rotation=45, ha="right")
|
140 |
+
|
141 |
+
TEXT_COLOR = plt.rcParams.get('text.color', '#E5E7EB')
|
142 |
+
for i, v in enumerate(data_series):
|
143 |
+
ax.text(i, v + (0.01 * data_series.max()), str(int(v)), ha='center', va='bottom', zorder=3, color=TEXT_COLOR, fontsize=9)
|
144 |
+
|
145 |
+
fig.tight_layout(pad=1.5)
|
146 |
return fig
|
147 |
except Exception as e:
|
148 |
+
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
149 |
if fig: plt.close(fig)
|
150 |
+
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
151 |
|
152 |
+
def generate_generic_pie_chart(data_series, title, color_map='Pastel2'):
|
153 |
+
"""Generic function to create a theme-aware pie chart."""
|
154 |
+
if data_series is None or data_series.empty:
|
155 |
+
return create_placeholder_plot(title=title, message="No data available.")
|
156 |
|
157 |
fig = None
|
158 |
try:
|
159 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
160 |
+
_apply_theme_aware_styling(fig, ax, is_pie=True)
|
161 |
+
|
162 |
+
THEME_TEXT_COLOR = plt.rcParams.get('text.color', '#E5E7EB')
|
163 |
+
pie_slice_colors = plt.cm.get_cmap(color_map, len(data_series))
|
164 |
+
colors = [pie_slice_colors(i) for i in range(len(data_series))]
|
165 |
+
|
166 |
+
wedges, texts, autotexts = ax.pie(
|
167 |
+
data_series,
|
168 |
+
autopct='%1.1f%%',
|
169 |
+
startangle=140,
|
170 |
+
colors=colors,
|
171 |
+
pctdistance=0.85,
|
172 |
+
wedgeprops=dict(width=0.4, edgecolor=plt.rcParams.get('figure.facecolor', '#111827'), linewidth=2)
|
173 |
+
)
|
174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
for text_item in texts + autotexts:
|
176 |
text_item.set_color(THEME_TEXT_COLOR)
|
177 |
+
text_item.set_fontsize(10)
|
178 |
text_item.set_zorder(2)
|
|
|
|
|
179 |
|
180 |
+
for autotext in autotexts:
|
181 |
+
autotext.set_weight('bold')
|
182 |
+
|
183 |
+
ax.set_title(title, fontsize=14, weight='bold', pad=20)
|
184 |
+
ax.legend(wedges, data_series.index, title="Categories", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1),
|
185 |
+
labelcolor=THEME_TEXT_COLOR,
|
186 |
+
frameon=False)
|
187 |
+
fig.tight_layout(pad=1.5)
|
188 |
return fig
|
189 |
except Exception as e:
|
190 |
+
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
191 |
if fig: plt.close(fig)
|
192 |
+
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
193 |
+
|
194 |
+
# --- Specific Plot Implementations ---
|
195 |
|
196 |
def generate_followers_count_over_time_plot(df, **kwargs):
|
|
|
197 |
type_value = kwargs.get('type_value', 'follower_gains_monthly')
|
198 |
+
title = f"Followers Count Over Time"
|
199 |
if df is None or df.empty:
|
200 |
return create_placeholder_plot(title=title, message="No follower data available.")
|
201 |
|
|
|
204 |
df_filtered = df[df['follower_count_type'] == type_value].copy()
|
205 |
if df_filtered.empty:
|
206 |
return create_placeholder_plot(title=title, message=f"No data for type '{type_value}'.")
|
207 |
+
|
208 |
df_filtered['datetime_obj'] = pd.to_datetime(df_filtered['category_name'], errors='coerce')
|
209 |
df_filtered['follower_count_organic'] = pd.to_numeric(df_filtered['follower_count_organic'], errors='coerce').fillna(0)
|
210 |
df_filtered['follower_count_paid'] = pd.to_numeric(df_filtered['follower_count_paid'], errors='coerce').fillna(0)
|
|
|
214 |
|
215 |
fig, ax = plt.subplots(figsize=(10, 5))
|
216 |
_apply_theme_aware_styling(fig, ax)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
217 |
|
218 |
+
ax.plot(df_filtered['datetime_obj'], df_filtered['follower_count_organic'], marker='o', linestyle='-', color='#22D3EE', label='Organic Followers', zorder=1)
|
219 |
+
ax.plot(df_filtered['datetime_obj'], df_filtered['follower_count_paid'], marker='x', linestyle='--', color='#A78BFA', label='Paid Followers', zorder=1)
|
|
|
|
|
|
|
|
|
220 |
|
221 |
+
ax.set_title(title, fontsize=14, weight='bold')
|
222 |
+
ax.set_xlabel('Date')
|
223 |
+
ax.set_ylabel('Follower Count')
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
|
225 |
+
legend = ax.legend()
|
226 |
+
for text in legend.get_texts():
|
227 |
+
text.set_color(plt.rcParams.get('text.color', 'black'))
|
228 |
+
legend.set_zorder(2)
|
229 |
+
legend.get_frame().set_alpha(0.5)
|
230 |
+
legend.get_frame().set_facecolor('#1F2937')
|
231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
232 |
|
233 |
+
plt.xticks(rotation=30, ha="right")
|
234 |
+
fig.tight_layout(pad=1.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
235 |
return fig
|
236 |
except Exception as e:
|
237 |
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
238 |
if fig: plt.close(fig)
|
239 |
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
240 |
|
241 |
+
|
242 |
def generate_followers_by_demographics_plot(df, **kwargs):
|
|
|
243 |
plot_title = kwargs.get('plot_title', "Followers by Demographics")
|
244 |
type_value = kwargs.get('type_value')
|
|
|
245 |
|
246 |
if df is None or df.empty or not type_value:
|
247 |
return create_placeholder_plot(title=plot_title, message="No data or demographic type not specified.")
|
|
|
251 |
df_filtered = df[df['follower_count_type'] == type_value].copy()
|
252 |
if df_filtered.empty:
|
253 |
return create_placeholder_plot(title=plot_title, message=f"No data for type '{type_value}'.")
|
254 |
+
|
255 |
df_filtered['follower_count_organic'] = pd.to_numeric(df_filtered['follower_count_organic'], errors='coerce').fillna(0)
|
256 |
+
demographics_data = df_filtered.groupby('category_name')['follower_count_organic'].sum()
|
257 |
+
demographics_data = demographics_data.sort_values(ascending=False).head(10)
|
|
|
|
|
258 |
|
259 |
if demographics_data.empty:
|
260 |
return create_placeholder_plot(title=plot_title, message="No demographic data to display.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
261 |
|
262 |
+
return generate_generic_bar_plot(demographics_data, plot_title, 'Category', 'Number of Followers', 'plasma')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
263 |
except Exception as e:
|
264 |
+
logging.error(f"Error in {plot_title}: {e}", exc_info=True)
|
265 |
if fig: plt.close(fig)
|
266 |
+
return create_placeholder_plot(title=f"{plot_title} Error", message=str(e))
|
267 |
|
268 |
def generate_engagement_rate_over_time_plot(df, date_column='published_at', engagement_rate_col='engagement'):
|
|
|
269 |
title = "Engagement Rate Over Time"
|
270 |
+
# This plot is a specific time series, so we use the generic function
|
271 |
+
return generate_generic_time_series_plot(df, date_column, engagement_rate_col, title, 'Engagement Rate (%)', color='#F472B6')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
272 |
|
273 |
def generate_content_format_breakdown_plot(df, format_col='media_type', **kwargs):
|
274 |
+
title = "Content by Format"
|
|
|
275 |
if df is None or df.empty or format_col not in df.columns:
|
276 |
return create_placeholder_plot(title=title, message="No data available.")
|
|
|
|
|
|
|
|
|
|
|
|
|
277 |
|
278 |
+
format_counts = df[format_col].value_counts().dropna()
|
279 |
+
return generate_generic_pie_chart(format_counts, title, 'viridis')
|
280 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
|
282 |
def _parse_eb_label(label_data):
|
283 |
if isinstance(label_data, list): return label_data
|
|
|
290 |
return [] if pd.isna(label_data) else [str(label_data)]
|
291 |
|
292 |
def generate_content_topic_breakdown_plot(df, topics_col='li_eb_labels', **kwargs):
|
293 |
+
title = "Content by Topics (Top 15)"
|
|
|
294 |
if df is None or df.empty or topics_col not in df.columns:
|
295 |
return create_placeholder_plot(title=title, message="No data available.")
|
296 |
+
|
|
|
297 |
try:
|
298 |
topic_counts = df[topics_col].apply(_parse_eb_label).explode().dropna().value_counts()
|
299 |
+
topic_counts = topic_counts[topic_counts.index != ''].nlargest(15).sort_values(ascending=True)
|
300 |
if topic_counts.empty:
|
301 |
return create_placeholder_plot(title=title, message="No topic data found.")
|
|
|
|
|
302 |
|
303 |
fig, ax = plt.subplots(figsize=(10, 8))
|
304 |
+
_apply_theme_aware_styling(fig, ax)
|
305 |
+
colors = plt.cm.get_cmap('YlGnBu')(np.linspace(0.3, 1, len(topic_counts)))
|
306 |
+
topic_counts.plot(kind='barh', ax=ax, zorder=2, color=colors)
|
307 |
+
|
308 |
+
ax.set_title(title, fontsize=14, weight='bold')
|
309 |
ax.set_xlabel('Number of Posts')
|
310 |
ax.set_ylabel('Topic')
|
311 |
|
312 |
+
TEXT_COLOR = plt.rcParams.get('text.color', '#E5E7EB')
|
313 |
+
for i, (topic, count) in enumerate(topic_counts.items()):
|
314 |
+
ax.text(count + (0.01 * topic_counts.max()), i, f' {count}', va='center', ha='left', zorder=3, color=TEXT_COLOR, fontsize=9)
|
315 |
+
|
316 |
+
fig.tight_layout(pad=1.5)
|
|
|
|
|
317 |
return fig
|
318 |
except Exception as e:
|
319 |
logging.error(f"Error generating {title}: {e}", exc_info=True)
|
|
|
320 |
return create_placeholder_plot(title=f"{title} Error", message=str(e))
|
321 |
|
322 |
+
|
323 |
def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date, current_plot_configs):
|
324 |
+
logging.info(f"Updating analytics plot figures with new styling. Filter: {date_filter_option}")
|
|
|
|
|
|
|
|
|
325 |
num_expected_plots = len(current_plot_configs)
|
326 |
|
327 |
+
# ... (rest of your data loading logic is fine)
|
|
|
328 |
if not token_state_value or not token_state_value.get("token"):
|
329 |
message = "❌ Accesso negato. Nessun token. Impossibile generare le analisi."
|
330 |
logging.warning(message)
|
|
|
348 |
summaries = {p_cfg["id"]: f"Errore preparazione dati: {e}" for p_cfg in current_plot_configs}
|
349 |
return [error_msg] + placeholder_figs + [summaries]
|
350 |
|
351 |
+
|
352 |
# Map plot IDs to their respective generation functions
|
353 |
plot_functions = {
|
354 |
+
# Dinamiche dei Follower
|
355 |
"followers_count": lambda: generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'),
|
356 |
+
"followers_growth_rate": lambda: generate_generic_time_series_plot(date_filtered_follower_stats_df, 'category_name', 'follower_count_organic', 'Follower Growth Rate', 'Growth Rate (%)', color='#A78BFA'), # Simplified for now
|
357 |
"followers_by_location": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Follower per Località"),
|
358 |
"followers_by_role": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Follower per Ruolo"),
|
359 |
"followers_by_industry": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Follower per Settore"),
|
360 |
"followers_by_seniority": lambda: generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Follower per Anzianità"),
|
361 |
+
|
362 |
+
# Approfondimenti Performance Post
|
363 |
"engagement_rate": lambda: generate_engagement_rate_over_time_plot(filtered_merged_posts_df),
|
364 |
+
"reach_over_time": lambda: generate_generic_time_series_plot(filtered_merged_posts_df, 'published_at', 'clickCount', 'Reach Over Time (Clicks)', 'Total Clicks', color='#6EE7B7'),
|
365 |
+
"impressions_over_time": lambda: generate_generic_time_series_plot(filtered_merged_posts_df, 'published_at', 'impressionCount', 'Impressions Over Time', 'Total Impressions', color='#38BDF8'),
|
366 |
+
"likes_over_time": lambda: generate_generic_time_series_plot(filtered_merged_posts_df, 'published_at', 'likeCount', 'Reactions (Likes) Over Time', 'Total Likes', color='#FB7185'),
|
367 |
+
|
368 |
+
# Engagement Dettagliato Post nel Tempo
|
369 |
+
"clicks_over_time": lambda: generate_generic_time_series_plot(filtered_merged_posts_df, 'published_at', 'clickCount', 'Clicks Over Time', 'Total Clicks', color='#6EE7B7'),
|
370 |
+
"shares_over_time": lambda: generate_generic_time_series_plot(filtered_merged_posts_df, 'published_at', 'shareCount', 'Shares Over Time', 'Total Shares', color='#34D399'),
|
371 |
+
"comments_over_time": lambda: generate_generic_time_series_plot(filtered_merged_posts_df, 'published_at', 'commentCount', 'Comments Over Time', 'Total Comments', color='#FACC15'),
|
372 |
+
"comments_sentiment": lambda: generate_generic_pie_chart(filtered_merged_posts_df['comment_sentiment'].value_counts().dropna(), "Breakdown of Comments by Sentiment", 'coolwarm'),
|
373 |
+
|
374 |
+
# Analisi Strategia Contenuti
|
375 |
+
"post_frequency_cs": lambda: generate_generic_time_series_plot(filtered_merged_posts_df.resample('D', on='published_at').size().reset_index(name='count'), 'published_at', 'count', 'Post Frequency', 'Number of Posts', color='#C084FC'),
|
376 |
"content_format_breakdown_cs": lambda: generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=token_state_value.get("config_media_type_col", "media_type")),
|
377 |
"content_topic_breakdown_cs": lambda: generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=token_state_value.get("config_eb_labels_col", "li_eb_labels")),
|
378 |
+
|
379 |
+
# Analisi Menzioni (Dettaglio)
|
380 |
+
"mention_analysis_volume": lambda: generate_generic_time_series_plot(filtered_mentions_df, token_state_value.get("config_date_col_mentions", "date"), 'mention_id', 'Mentions Volume', 'Number of Mentions', color='#818CF8'),
|
381 |
+
"mention_analysis_sentiment": lambda: generate_generic_pie_chart(filtered_mentions_df['sentiment_label'].value_counts().dropna(), "Mention Sentiment Breakdown")
|
382 |
}
|
383 |
|
384 |
plot_figs = []
|