GuglielmoTor commited on
Commit
c8187fc
·
verified ·
1 Parent(s): eaa683c

Update ui/analytics_plot_generator.py

Browse files
Files changed (1) hide show
  1. 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 # Added for rounded corners
9
- import ast # For safely evaluating string representations of lists
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
- Helper to apply theme-aware styling to a Matplotlib plot.
21
- It reads colors from rcParams, which Gradio should set based on the current theme.
22
- This makes text, backgrounds, and grids adapt to light/dark mode.
23
  """
24
- # Get theme-aware colors from Matplotlib's runtime configuration
25
- THEME_TEXT_COLOR = plt.rcParams.get('text.color', 'black')
26
- THEME_GRID_COLOR = plt.rcParams.get('grid.color', 'lightgray')
27
- THEME_AXES_FACE_COLOR = plt.rcParams.get('axes.facecolor', 'whitesmoke')
28
- THEME_AXES_EDGE_COLOR = plt.rcParams.get('axes.edgecolor', 'lightgray')
29
-
30
- # Make the original figure and axes backgrounds transparent to draw our own.
31
- fig.patch.set_alpha(0.0)
32
- ax.patch.set_alpha(0.0)
33
-
34
- # Turn off original spines to draw a new rounded background shape.
35
- ax.spines['top'].set_visible(False)
36
- ax.spines['right'].set_visible(False)
37
- ax.spines['bottom'].set_visible(False)
38
- ax.spines['left'].set_visible(False)
39
-
40
- # Add a new rounded background for the axes area using theme colors.
41
- rounded_rect_bg = patches.FancyBboxPatch(
42
- (0, 0), 1, 1,
43
- boxstyle="round,pad=0,rounding_size=0.015",
44
- transform=ax.transAxes,
45
- facecolor=THEME_AXES_FACE_COLOR,
46
- edgecolor=THEME_AXES_EDGE_COLOR,
47
- linewidth=0.5,
48
- zorder=-1
49
- )
50
- ax.add_patch(rounded_rect_bg)
51
-
52
- # Apply the theme's text color to all major text elements.
53
- ax.xaxis.label.set_color(THEME_TEXT_COLOR)
54
- ax.yaxis.label.set_color(THEME_TEXT_COLOR)
55
- ax.title.set_color(THEME_TEXT_COLOR)
56
-
57
- # Apply the theme's text color to the tick labels and tick marks.
58
- ax.tick_params(axis='x', colors=THEME_TEXT_COLOR)
59
- ax.tick_params(axis='y', colors=THEME_TEXT_COLOR)
60
-
61
- # Set grid color and ensure it's drawn behind data
62
- ax.grid(True, linestyle='--', alpha=0.6, zorder=0, color=THEME_GRID_COLOR)
 
 
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
- # Use the theme's text color for the message
72
- THEME_TEXT_COLOR = plt.rcParams.get('text.color', 'black')
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.subplots_adjust(top=0.90, bottom=0.10, left=0.10, right=0.90)
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_alpha(0.0)
81
- ax_err.patch.set_alpha(0.0)
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
- def generate_posts_activity_plot(df, date_column='published_at'):
87
- """Generates a theme-aware plot for posts activity over time."""
88
- if df is None or df.empty or date_column not in df.columns:
89
- return create_placeholder_plot(title="Posts Activity Over Time", message="No data available.")
 
 
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 = df_copy.dropna(subset=[date_column])
 
96
  if df_copy.empty:
97
- return create_placeholder_plot(title="Posts Activity Over Time", message="No valid date entries found.")
98
 
99
- posts_over_time = df_copy.set_index(date_column).resample('D').size()
100
- if posts_over_time.empty:
101
- return create_placeholder_plot(title="Posts Activity Over Time", message="No posts in the selected period.")
102
 
103
  fig, ax = plt.subplots(figsize=(10, 5))
104
  _apply_theme_aware_styling(fig, ax)
105
 
106
- posts_over_time.plot(kind='line', ax=ax, marker='o', linestyle='-', zorder=1)
107
- ax.set_xlabel('Date')
108
- ax.set_ylabel('Number of Posts')
109
- plt.xticks(rotation=45)
110
- fig.tight_layout(pad=0.5)
111
- fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
 
112
  return fig
113
  except Exception as e:
114
- logging.error(f"Error generating posts activity plot: {e}", exc_info=True)
115
  if fig: plt.close(fig)
116
- return create_placeholder_plot(title="Posts Activity Error", message=str(e))
117
 
118
 
119
- def generate_mentions_activity_plot(df, date_column='date'):
120
- """Generates a theme-aware plot for mentions activity over time."""
121
- if df is None or df.empty or date_column not in df.columns:
122
- return create_placeholder_plot(title="Mentions Activity Over Time", message="No data available.")
123
 
124
  fig = None
125
  try:
126
- df_copy = df.copy()
127
- df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
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
- mentions_over_time = df_copy.set_index(date_column).resample('D').size()
133
- if mentions_over_time.empty:
134
- return create_placeholder_plot(title="Mentions Activity Over Time", message="No mentions in the selected period.")
135
 
136
- fig, ax = plt.subplots(figsize=(10, 5))
137
- _apply_theme_aware_styling(fig, ax)
138
-
139
- mentions_over_time.plot(kind='line', ax=ax, marker='o', linestyle='-', color='purple', zorder=1)
140
- ax.set_xlabel('Date')
141
- ax.set_ylabel('Number of Mentions')
142
- plt.xticks(rotation=45)
143
- fig.tight_layout(pad=0.5)
144
- fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
 
145
  return fig
146
  except Exception as e:
147
- logging.error(f"Error generating mentions activity plot: {e}", exc_info=True)
148
  if fig: plt.close(fig)
149
- return create_placeholder_plot(title="Mentions Activity Error", message=str(e))
150
 
151
- def generate_mention_sentiment_plot(df, sentiment_column='sentiment_label'):
152
- """Generates a theme-aware pie chart for mention sentiment distribution."""
153
- if df is None or df.empty or sentiment_column not in df.columns:
154
- return create_placeholder_plot(title="Mention Sentiment Distribution", message="No data available.")
155
 
156
  fig = None
157
  try:
158
- sentiment_counts = df[sentiment_column].value_counts()
159
- if sentiment_counts.empty:
160
- return create_placeholder_plot(title="Mention Sentiment Distribution", message="No sentiment data available.")
 
 
 
 
 
 
 
 
 
 
 
 
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
- ax.axis('equal')
179
- fig.subplots_adjust(top=0.95, bottom=0.05, left=0.05, right=0.95)
 
 
 
 
 
 
180
  return fig
181
  except Exception as e:
182
- logging.error(f"Error generating mention sentiment plot: {e}", exc_info=True)
183
  if fig: plt.close(fig)
184
- return create_placeholder_plot(title="Mention Sentiment Error", message=str(e))
 
 
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 ({type_value})"
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
- def generate_followers_growth_rate_plot(df, **kwargs):
230
- """Generates a theme-aware plot for follower growth rate."""
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
- fig = None
237
- try:
238
- df_filtered = df[df['follower_count_type'] == type_value].copy()
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
- if len(df_filtered) < 2:
247
- return create_placeholder_plot(title=title, message="Not enough data points to calculate growth rate.")
248
-
249
- df_filtered['organic_growth_rate'] = df_filtered['follower_count_organic'].pct_change() * 100
250
- df_filtered['paid_growth_rate'] = df_filtered['follower_count_paid'].pct_change() * 100
251
- df_filtered.replace([np.inf, -np.inf], np.nan, inplace=True)
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
- ax.set_xlabel('Date')
268
- ax.set_ylabel('Growth Rate (%)')
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
- df_filtered['follower_count_paid'] = pd.to_numeric(df_filtered['follower_count_paid'], errors='coerce').fillna(0)
303
- demographics_data = df_filtered.groupby(category_col)[['follower_count_organic', 'follower_count_paid']].sum()
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
- def generate_generic_time_series_plot(df, date_column, value_column, title, ylabel, color='blue'):
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 generating {title}: {e}", exc_info=True)
363
  if fig: plt.close(fig)
364
- return create_placeholder_plot(title=f"{title} Error", message=str(e))
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
- if df is None or df.empty or date_column not in df.columns or engagement_rate_col not in df.columns:
370
- return create_placeholder_plot(title=title, message="No data available.")
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
- """Generates a theme-aware bar chart for content format breakdown."""
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
- fig, ax = plt.subplots(figsize=(8,6))
503
- _apply_theme_aware_styling(fig,ax)
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
- """Generates a theme-aware horizontal bar chart for content topics."""
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
- top_topics.plot(kind='barh', ax=ax, zorder=1, color=plt.cm.get_cmap('YlGnBu')(np.linspace(0.3, 1, len(top_topics))))
553
- ax.set_title(title)
 
554
  ax.set_xlabel('Number of Posts')
555
  ax.set_ylabel('Topic')
556
 
557
- # Add text labels with theme color
558
- TEXT_COLOR = plt.rcParams.get('text.color', 'black')
559
- for i, (topic, count) in enumerate(top_topics.items()):
560
- ax.text(count + (0.01 * top_topics.max()), i, f' {count}', va='center', ha='left', zorder=2, color=TEXT_COLOR)
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
- plot_data_summaries_for_chatbot = {}
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: generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'),
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: generate_reach_over_time_plot(filtered_merged_posts_df),
613
- "impressions_over_time": lambda: generate_impressions_over_time_plot(filtered_merged_posts_df),
614
- "likes_over_time": lambda: generate_likes_over_time_plot(filtered_merged_posts_df),
615
- "clicks_over_time": lambda: generate_clicks_over_time_plot(filtered_merged_posts_df),
616
- "shares_over_time": lambda: generate_shares_over_time_plot(filtered_merged_posts_df),
617
- "comments_over_time": lambda: generate_comments_over_time_plot(filtered_merged_posts_df),
618
- "comments_sentiment": lambda: generate_comments_sentiment_breakdown_plot(filtered_merged_posts_df),
619
- "post_frequency_cs": lambda: generate_post_frequency_plot(filtered_merged_posts_df),
 
 
 
 
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
- "mention_analysis_volume": lambda: generate_mentions_activity_plot(filtered_mentions_df, date_column=token_state_value.get("config_date_col_mentions", "date")),
623
- "mention_analysis_sentiment": lambda: generate_mention_sentiment_plot(filtered_mentions_df)
 
 
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 = []