GuglielmoTor commited on
Commit
266ae82
Β·
verified Β·
1 Parent(s): c6716b6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +237 -251
app.py CHANGED
@@ -4,10 +4,11 @@ import os
4
  import logging
5
  import matplotlib
6
  matplotlib.use('Agg') # Set backend for Matplotlib to avoid GUI conflicts with Gradio
7
- import matplotlib.pyplot as plt
 
8
 
9
  # --- Module Imports ---
10
- from gradio_utils import get_url_user_token
11
 
12
  # Functions from newly created/refactored modules
13
  from config import (
@@ -19,10 +20,11 @@ from sync_logic import sync_all_linkedin_data_orchestrator
19
  from ui_generators import (
20
  display_main_dashboard,
21
  run_mentions_tab_display,
22
- run_follower_stats_tab_display
 
23
  )
24
  # Corrected import for analytics_data_processing
25
- from analytics_data_processing import prepare_filtered_analytics_data
26
  from analytics_plot_generator import (
27
  generate_posts_activity_plot, generate_engagement_type_plot,
28
  generate_mentions_activity_plot, generate_mention_sentiment_plot,
@@ -33,13 +35,11 @@ from analytics_plot_generator import (
33
  generate_reach_over_time_plot,
34
  generate_impressions_over_time_plot,
35
  create_placeholder_plot, # For initializing plots
36
- # --- Import existing new plot functions ---
37
  generate_likes_over_time_plot,
38
- generate_clicks_over_time_plot, # Note: can be same as reach
39
  generate_shares_over_time_plot,
40
  generate_comments_over_time_plot,
41
  generate_comments_sentiment_breakdown_plot,
42
- # --- Import NEW plot functions for Content Strategy ---
43
  generate_post_frequency_plot,
44
  generate_content_format_breakdown_plot,
45
  generate_content_topic_breakdown_plot
@@ -48,19 +48,14 @@ from analytics_plot_generator import (
48
  # Configure logging
49
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
50
 
51
- # --- Analytics Tab: Plot Update Function ---
52
- def update_analytics_plots(token_state_value, date_filter_option, custom_start_date, custom_end_date):
53
  """
54
- Prepares analytics data using external processing function and then generates plots.
 
55
  """
56
- logging.info(f"Updating analytics plots. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
57
-
58
- # --- Increased number of expected plots ---
59
- # Original 13 + 5 engagement = 18
60
- # New Content Strategy (3: freq, format, topics)
61
- # New Mention Analysis (2: volume, sentiment - these reuse existing plot objects but are new UI slots)
62
- # Total = 18 + 3 + 2 = 23
63
- num_expected_plots = 23
64
 
65
  if not token_state_value or not token_state_value.get("token"):
66
  message = "❌ Access denied. No token. Cannot generate analytics."
@@ -69,27 +64,14 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
69
  return [message] + placeholder_figs
70
 
71
  try:
72
- # prepare_filtered_analytics_data might need to be updated if new DFs are required for new plots
73
- # (e.g. if 'media_type' or 'eb_labels' are not in 'bubble_posts_df' and need special handling)
74
- # For now, we assume 'filtered_merged_posts_df' contains these columns.
75
- (filtered_merged_posts_df,
76
- filtered_mentions_df,
77
- date_filtered_follower_stats_df,
78
- raw_follower_stats_df,
79
  start_dt_for_msg, end_dt_for_msg) = \
80
  prepare_filtered_analytics_data(
81
  token_state_value, date_filter_option, custom_start_date, custom_end_date
82
  )
83
-
84
- # Ensure 'media_type' and 'eb_labels' exist in filtered_merged_posts_df for new plots,
85
- # or handle their absence gracefully in the plot functions themselves (which they do).
86
- # Example: Add dummy columns if they might be missing, for robust testing:
87
- # if 'media_type' not in filtered_merged_posts_df.columns:
88
- # filtered_merged_posts_df['media_type'] = 'Unknown'
89
- # if 'eb_labels' not in filtered_merged_posts_df.columns:
90
- # filtered_merged_posts_df['eb_labels'] = None
91
-
92
-
93
  except Exception as e:
94
  error_msg = f"❌ Error preparing analytics data: {e}"
95
  logging.error(error_msg, exc_info=True)
@@ -98,114 +80,68 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
98
 
99
  date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
100
  date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
101
- # config_date_col_followers_source = token_state_value.get("config_date_col_followers", "date")
 
102
 
103
  logging.info(f"Data for plotting - Filtered Merged Posts: {len(filtered_merged_posts_df)} rows, Filtered Mentions: {len(filtered_mentions_df)} rows.")
104
  logging.info(f"Date-Filtered Follower Stats: {len(date_filtered_follower_stats_df)} rows, Raw Follower Stats: {len(raw_follower_stats_df)} rows.")
105
 
106
  try:
107
- # Existing plots (13)
108
- plot_posts_activity = generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts)
109
- plot_engagement_type = generate_engagement_type_plot(filtered_merged_posts_df)
110
 
111
- # These two will be used for the new "Mention Analysis" section as well
112
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
113
- fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
114
-
115
- plot_followers_count = generate_followers_count_over_time_plot(
116
- date_filtered_follower_stats_df,
117
- type_filter_column='follower_count_type',
118
- type_value='follower_gains_monthly'
119
- )
120
- plot_followers_growth_rate = generate_followers_growth_rate_plot(
121
- date_filtered_follower_stats_df,
122
- type_filter_column='follower_count_type',
123
- type_value='follower_gains_monthly'
124
- )
125
-
126
- plot_followers_by_location = generate_followers_by_demographics_plot(raw_follower_stats_df, category_col='category_name', type_filter_column='follower_count_type', type_value='follower_geo', plot_title="Followers by Location")
127
- plot_followers_by_role = generate_followers_by_demographics_plot(raw_follower_stats_df, category_col='category_name', type_filter_column='follower_count_type', type_value='follower_function', plot_title="Followers by Role")
128
- plot_followers_by_industry = generate_followers_by_demographics_plot(raw_follower_stats_df, category_col='category_name', type_filter_column='follower_count_type', type_value='follower_industry', plot_title="Followers by Industry")
129
- plot_followers_by_seniority = generate_followers_by_demographics_plot(raw_follower_stats_df, category_col='category_name', type_filter_column='follower_count_type', type_value='follower_seniority', plot_title="Followers by Seniority")
130
-
131
- plot_engagement_rate = generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, engagement_rate_col='engagement')
132
- plot_reach_over_time = generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, reach_col='clickCount')
133
- plot_impressions_over_time = generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, impressions_col='impressionCount')
134
-
135
- # Additional Engagement plots (5)
136
- plot_likes_over_time = generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, likes_col='likeCount')
137
- plot_clicks_over_time = generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, clicks_col='clickCount')
138
- plot_shares_over_time = generate_shares_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, shares_col='shareCount')
139
- plot_comments_over_time = generate_comments_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, comments_col='commentCount')
140
 
141
- # Assuming 'comment_sentiment' column might exist, or 'sentiment' as fallback (handled in plot function)
142
- plot_comments_sentiment_breakdown = generate_comments_sentiment_breakdown_plot(
143
- filtered_merged_posts_df,
144
- sentiment_column='comment_sentiment'
145
- )
146
-
147
- # --- Generate NEW plots for Content Strategy (3) ---
148
- # Assuming 'media_type' and 'eb_labels' are in filtered_merged_posts_df
149
- # The plot functions themselves have fallbacks/placeholders if columns are missing.
150
- media_type_col_name = token_state_value.get("config_media_type_col", "media_type") # Example if configurable
151
- eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels") # Example if configurable
152
-
153
- plot_post_frequency = generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts)
154
- plot_content_format_breakdown = generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name)
155
- plot_content_topic_breakdown = generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name)
156
-
 
 
 
 
 
 
 
 
157
 
158
  message = f"πŸ“Š Analytics updated for period: {date_filter_option}"
159
  if date_filter_option == "Custom Range":
160
  s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
161
- e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
162
  message += f" (From: {s_display} To: {e_display})"
163
 
164
- all_generated_plots = [
165
- plot_posts_activity, plot_engagement_type,
166
- fig_mentions_activity_shared, fig_mention_sentiment_shared, # Original mention plots
167
- plot_followers_count, plot_followers_growth_rate,
168
- plot_followers_by_location, plot_followers_by_role, plot_followers_by_industry, plot_followers_by_seniority,
169
- plot_engagement_rate, plot_reach_over_time, plot_impressions_over_time,
170
- # Add new engagement plot objects to the list
171
- plot_likes_over_time, plot_clicks_over_time,
172
- plot_shares_over_time, plot_comments_over_time,
173
- plot_comments_sentiment_breakdown,
174
- # --- Add NEW Content Strategy plot objects ---
175
- plot_post_frequency, plot_content_format_breakdown, plot_content_topic_breakdown,
176
- # --- Add plots for the NEW "Mention Analysis" section (reusing figures) ---
177
- fig_mentions_activity_shared, # Reused figure for new UI slot
178
- fig_mention_sentiment_shared # Reused figure for new UI slot
179
- ]
180
- num_plots_generated = sum(1 for p in all_generated_plots if p is not None and not isinstance(p, str))
181
- logging.info(f"Successfully generated {num_plots_generated} plot figures for {num_expected_plots} UI slots.")
182
-
183
- # Ensure the number of returned plots matches num_expected_plots, padding with placeholders if necessary
184
- final_plots_list = []
185
- for i, p in enumerate(all_generated_plots): # Iterate up to the expected number of plots
186
- if i < num_expected_plots: # Ensure we don't exceed the expected number of outputs
187
- if p is not None and not isinstance(p, str): # isinstance check for safety
188
- final_plots_list.append(p)
189
- else:
190
- logging.warning(f"A plot generation failed or returned unexpected type for slot {i}, using placeholder. Plot: {p}")
191
- final_plots_list.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot."))
192
  else:
193
- logging.warning(f"Generated more plot figures ({len(all_generated_plots)}) than expected UI slots ({num_expected_plots}). Truncating.")
194
- break
195
-
196
-
197
- # If fewer plots were generated than expected (e.g. due to early exit or major error in a plot function)
198
- while len(final_plots_list) < num_expected_plots:
199
- logging.warning(f"Padding missing plot with placeholder. Expected {num_expected_plots}, got {len(final_plots_list)} so far.")
200
- final_plots_list.append(create_placeholder_plot(title="Missing Plot", message="Plot could not be generated."))
201
- if len(final_plots_list) > num_expected_plots + 5: # Safety break
202
- logging.error("Too many placeholders added, breaking loop.")
203
- break
204
-
205
- return [message] + final_plots_list[:num_expected_plots] # Ensure correct number of outputs
206
 
207
  except Exception as e:
208
- error_msg = f"❌ Error generating analytics plots: {e}"
209
  logging.error(error_msg, exc_info=True)
210
  placeholder_figs = [create_placeholder_plot(title="Plot Generation Error", message=str(e)) for _ in range(num_expected_plots)]
211
  return [error_msg] + placeholder_figs
@@ -213,24 +149,21 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
213
 
214
  # --- Gradio UI Blocks ---
215
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
216
- title="LinkedIn Organization Dashboard") as app:
217
 
218
  token_state = gr.State(value={
219
  "token": None, "client_id": None, "org_urn": None,
220
- "bubble_posts_df": pd.DataFrame(),
221
- "bubble_post_stats_df": pd.DataFrame(),
222
  "bubble_mentions_df": pd.DataFrame(),
223
  "bubble_follower_stats_df": pd.DataFrame(),
224
- # Consider adding "bubble_comments_sentiment_df": pd.DataFrame() if you plan to fetch this data
225
- # Add keys for new data if needed by prepare_filtered_analytics_data, e.g.
226
- # "bubble_posts_with_content_details_df": pd.DataFrame(),
227
- "fetch_count_for_api": 0,
228
  "url_user_token_temp_storage": None,
229
- "config_date_col_posts": "published_at",
230
- "config_date_col_mentions": "date",
231
  "config_date_col_followers": "date",
232
- "config_media_type_col": "media_type", # For new plot
233
- "config_eb_labels_col": "eb_labels" # For new plot
234
  })
235
 
236
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
@@ -243,7 +176,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
243
  def initial_load_sequence(url_token, org_urn_val, current_state):
244
  logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
245
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
246
- dashboard_content = display_main_dashboard(new_state)
247
  return status_msg, new_state, btn_update, dashboard_content
248
 
249
  with gr.Tabs() as tabs:
@@ -260,38 +193,19 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
260
  show_progress="full"
261
  )
262
 
263
- sync_click_event = sync_data_btn.click(
264
- fn=sync_all_linkedin_data_orchestrator,
265
- inputs=[token_state],
266
- outputs=[sync_status_html_output, token_state],
267
- show_progress="full"
268
- ).then(
269
- fn=process_and_store_bubble_token,
270
- inputs=[url_user_token_display, org_urn_display, token_state],
271
- outputs=[status_box, token_state, sync_data_btn],
272
- show_progress=False
273
- ).then(
274
- fn=display_main_dashboard,
275
- inputs=[token_state],
276
- outputs=[dashboard_display_html],
277
- show_progress=False
278
- )
279
-
280
-
281
  with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
282
  gr.Markdown("## πŸ“ˆ LinkedIn Performance Analytics")
283
- gr.Markdown("Select a date range to filter Posts and Mentions analytics. Follower demographic plots show overall latest data. Follower time-series plots respect the selected date range if applicable to their data source (e.g. monthly gains).")
284
 
285
  analytics_status_md = gr.Markdown("Analytics status will appear here...")
286
 
287
  with gr.Row():
288
  date_filter_selector = gr.Radio(
289
  ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
290
- label="Select Date Range (for Posts, Mentions, and some Follower time-series)",
291
- value="Last 30 Days"
292
  )
293
- custom_start_date_picker = gr.DateTime(label="Start Date (Custom)", visible=False, include_time=False, type="datetime") # Changed to datetime
294
- custom_end_date_picker = gr.DateTime(label="End Date (Custom)", visible=False, include_time=False, type="datetime") # Changed to datetime
295
 
296
  apply_filter_btn = gr.Button("πŸ” Apply Filter & Refresh Analytics", variant="primary")
297
 
@@ -305,107 +219,152 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
305
  outputs=[custom_start_date_picker, custom_end_date_picker]
306
  )
307
 
308
- gr.Markdown("### Posts & Engagement Overview (Filtered by Date)")
309
- with gr.Row():
310
- posts_activity_plot = gr.Plot(label="Posts Activity Over Time")
311
- engagement_type_plot = gr.Plot(label="Post Engagement Types")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- # Original Mentions Overview - these plots will also be used for the "Mention Analysis" section below
314
- gr.Markdown("### Mentions Overview (Filtered by Date)")
315
- with gr.Row():
316
- mentions_activity_plot = gr.Plot(label="Mentions Activity Over Time") # Will be updated by fig_mentions_activity_shared
317
- mention_sentiment_plot = gr.Plot(label="Mention Sentiment Distribution") # Will be updated by fig_mention_sentiment_shared
318
-
319
- gr.Markdown("### Follower Dynamics")
320
- with gr.Row():
321
- followers_count_plot = gr.Plot(label="Followers Count Over Time (e.g., Monthly Gains)")
322
- followers_growth_rate_plot = gr.Plot(label="Followers Growth Rate (e.g., Monthly Gains)")
323
 
324
- gr.Markdown("### Follower Demographics (Overall Latest Data)")
325
- with gr.Row():
326
- followers_by_location_plot = gr.Plot(label="Followers by Location")
327
- followers_by_role_plot = gr.Plot(label="Followers by Role (Function)")
328
- with gr.Row():
329
- followers_by_industry_plot = gr.Plot(label="Followers by Industry")
330
- followers_by_seniority_plot = gr.Plot(label="Followers by Seniority")
331
-
332
- gr.Markdown("### Post Performance Insights (Filtered by Date)")
333
- with gr.Row():
334
- engagement_rate_plot = gr.Plot(label="Engagement Rate Over Time")
335
- reach_over_time_plot = gr.Plot(label="Reach Over Time (Clicks)")
336
- with gr.Row():
337
- impressions_over_time_plot = gr.Plot(label="Impressions Over Time")
338
- likes_over_time_plot = gr.Plot(label="Reactions (Likes) Over Time")
339
-
340
- gr.Markdown("### Detailed Post Engagement Over Time (Filtered by Date)")
341
- with gr.Row():
342
- clicks_over_time_plot = gr.Plot(label="Clicks Over Time") # Can be same as reach
343
- shares_over_time_plot = gr.Plot(label="Shares Over Time")
344
- with gr.Row():
345
- comments_over_time_plot = gr.Plot(label="Comments Over Time")
346
- comments_sentiment_plot = gr.Plot(label="Breakdown of Comments by Sentiment")
347
-
348
- # --- NEW: Content Strategy Analysis ---
349
- gr.Markdown("### πŸ“Š Content Strategy Analysis (Filtered by Date)")
350
- with gr.Row():
351
- post_frequency_cs_plot = gr.Plot(label="Post Frequency") # New plot component
352
- content_format_breakdown_cs_plot = gr.Plot(label="Breakdown of Content by Format") # New
353
- with gr.Row():
354
- content_topic_breakdown_cs_plot = gr.Plot(label="Breakdown of Content by Topics") # New (might need more width)
355
- # You can add another plot here or make the topic plot wider if needed, e.g. by itself in a row.
356
- # For now, placing it here. If it's too cramped:
357
- # content_topic_breakdown_cs_plot = gr.Plot(label="Breakdown of Content by Topics", elem_id="topic_plot_wide") # and use CSS for width if needed
358
-
359
- # --- NEW: Mention Analysis (reusing plots from above) ---
360
- gr.Markdown("### πŸ’¬ Mention Analysis (Filtered by Date)")
361
- with gr.Row():
362
- mention_analysis_volume_plot = gr.Plot(label="Mentions Volume Over Time") # New UI slot, uses fig_mentions_activity_shared
363
- mention_analysis_sentiment_plot = gr.Plot(label="Breakdown of Mentions by Sentiment") # New UI slot, uses fig_mention_sentiment_shared
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
-
366
- analytics_plot_outputs = [
367
- analytics_status_md,
368
- posts_activity_plot, engagement_type_plot,
369
- mentions_activity_plot, mention_sentiment_plot, # Original mention plots
370
- followers_count_plot, followers_growth_rate_plot,
371
- followers_by_location_plot, followers_by_role_plot,
372
- followers_by_industry_plot, followers_by_seniority_plot,
373
- engagement_rate_plot, reach_over_time_plot, impressions_over_time_plot,
374
- # Add new engagement plot components to the output list
375
- likes_over_time_plot, clicks_over_time_plot,
376
- shares_over_time_plot, comments_over_time_plot,
377
- comments_sentiment_plot,
378
- # --- Add NEW Content Strategy plot components ---
379
- post_frequency_cs_plot, content_format_breakdown_cs_plot, content_topic_breakdown_cs_plot,
380
- # --- Add NEW Mention Analysis plot components (these will receive the reused figures) ---
381
- mention_analysis_volume_plot, mention_analysis_sentiment_plot
382
- ]
383
- # Expected length: 1 (status) + 13 (original plots) + 5 (new engagement) + 3 (content strategy) + 2 (mention analysis) = 24
384
- # The update_analytics_plots function returns message + 23 plots. So len(analytics_plot_outputs) should be 24.
385
- # Current count: 1 + 2 + 2 + 2 + 4 + 3 + 5 + 3 + 2 = 24. Correct.
386
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  apply_filter_btn.click(
388
- fn=update_analytics_plots,
389
- inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
390
- outputs=analytics_plot_outputs,
391
- show_progress="full"
392
- )
393
-
394
- # Also update analytics after sync
395
- sync_click_event.then(
396
- fn=update_analytics_plots,
397
  inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
398
- outputs=analytics_plot_outputs,
399
  show_progress="full"
400
  )
401
 
402
  with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
403
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
404
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
405
- mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
406
  refresh_mentions_display_btn.click(
407
  fn=run_mentions_tab_display, inputs=[token_state],
408
- outputs=[mentions_html, mentions_sentiment_dist_plot],
409
  show_progress="full"
410
  )
411
 
@@ -413,17 +372,43 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
413
  refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
414
  follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
415
  with gr.Row():
416
- fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
417
  with gr.Row():
418
  fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
419
  fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")
420
 
421
  refresh_follower_stats_btn.click(
422
  fn=run_follower_stats_tab_display, inputs=[token_state],
423
- outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
424
  show_progress="full"
425
  )
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
 
428
  if __name__ == "__main__":
429
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
@@ -439,3 +424,4 @@ if __name__ == "__main__":
439
  logging.error("Matplotlib is not installed. Plots will not be generated.")
440
 
441
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
 
 
4
  import logging
5
  import matplotlib
6
  matplotlib.use('Agg') # Set backend for Matplotlib to avoid GUI conflicts with Gradio
7
+ import matplotlib.pyplot as plt
8
+ # from functools import partial # No longer needed if gr.State(value=plot_id) is used
9
 
10
  # --- Module Imports ---
11
+ from gradio_utils import get_url_user_token
12
 
13
  # Functions from newly created/refactored modules
14
  from config import (
 
20
  from ui_generators import (
21
  display_main_dashboard,
22
  run_mentions_tab_display,
23
+ run_follower_stats_tab_display,
24
+ build_analytics_tab_ui_components # Import the new UI builder function
25
  )
26
  # Corrected import for analytics_data_processing
27
+ from analytics_data_processing import prepare_filtered_analytics_data
28
  from analytics_plot_generator import (
29
  generate_posts_activity_plot, generate_engagement_type_plot,
30
  generate_mentions_activity_plot, generate_mention_sentiment_plot,
 
35
  generate_reach_over_time_plot,
36
  generate_impressions_over_time_plot,
37
  create_placeholder_plot, # For initializing plots
 
38
  generate_likes_over_time_plot,
39
+ generate_clicks_over_time_plot,
40
  generate_shares_over_time_plot,
41
  generate_comments_over_time_plot,
42
  generate_comments_sentiment_breakdown_plot,
 
43
  generate_post_frequency_plot,
44
  generate_content_format_breakdown_plot,
45
  generate_content_topic_breakdown_plot
 
48
  # Configure logging
49
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
50
 
51
+ # --- Analytics Tab: Plot Update Function (Original, generates figures) ---
52
+ def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date):
53
  """
54
+ Prepares analytics data using external processing function and then generates plot figures.
55
+ This function is primarily responsible for returning the Matplotlib figure objects.
56
  """
57
+ logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
58
+ num_expected_plots = 23 # This should match the number of plots defined in plot_configs
 
 
 
 
 
 
59
 
60
  if not token_state_value or not token_state_value.get("token"):
61
  message = "❌ Access denied. No token. Cannot generate analytics."
 
64
  return [message] + placeholder_figs
65
 
66
  try:
67
+ (filtered_merged_posts_df,
68
+ filtered_mentions_df,
69
+ date_filtered_follower_stats_df,
70
+ raw_follower_stats_df,
 
 
 
71
  start_dt_for_msg, end_dt_for_msg) = \
72
  prepare_filtered_analytics_data(
73
  token_state_value, date_filter_option, custom_start_date, custom_end_date
74
  )
 
 
 
 
 
 
 
 
 
 
75
  except Exception as e:
76
  error_msg = f"❌ Error preparing analytics data: {e}"
77
  logging.error(error_msg, exc_info=True)
 
80
 
81
  date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
82
  date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
83
+ media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
84
+ eb_labels_col_name = token_state_value.get("config_eb_labels_col", "eb_labels")
85
 
86
  logging.info(f"Data for plotting - Filtered Merged Posts: {len(filtered_merged_posts_df)} rows, Filtered Mentions: {len(filtered_mentions_df)} rows.")
87
  logging.info(f"Date-Filtered Follower Stats: {len(date_filtered_follower_stats_df)} rows, Raw Follower Stats: {len(raw_follower_stats_df)} rows.")
88
 
89
  try:
90
+ plot_figs = []
91
+ plot_figs.append(generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts))
92
+ plot_figs.append(generate_engagement_type_plot(filtered_merged_posts_df))
93
 
 
94
  fig_mentions_activity_shared = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
95
+ fig_mention_sentiment_shared = generate_mention_sentiment_plot(filtered_mentions_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ plot_figs.append(fig_mentions_activity_shared) # Original mention plot slot 1
98
+ plot_figs.append(fig_mention_sentiment_shared) # Original mention plot slot 2
99
+
100
+ plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
101
+ plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
102
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Followers by Location"))
103
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Followers by Role"))
104
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Followers by Industry"))
105
+ plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Followers by Seniority"))
106
+ plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
107
+ plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
108
+ plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
109
+ plot_figs.append(generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
110
+ plot_figs.append(generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
111
+ plot_figs.append(generate_shares_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
112
+ plot_figs.append(generate_comments_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
113
+ plot_figs.append(generate_comments_sentiment_breakdown_plot(filtered_merged_posts_df, sentiment_column='comment_sentiment'))
114
+ plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
115
+ plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
116
+ plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
117
+
118
+ # For the "Mention Analysis" section, we reuse the figures generated earlier
119
+ plot_figs.append(fig_mentions_activity_shared) # New UI slot for mention volume, reuses figure
120
+ plot_figs.append(fig_mention_sentiment_shared) # New UI slot for mention sentiment, reuses figure
121
 
122
  message = f"πŸ“Š Analytics updated for period: {date_filter_option}"
123
  if date_filter_option == "Custom Range":
124
  s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
125
+ e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Any"
126
  message += f" (From: {s_display} To: {e_display})"
127
 
128
+ final_plot_figs = []
129
+ for i, p_fig in enumerate(plot_figs):
130
+ if p_fig is not None and not isinstance(p_fig, str):
131
+ final_plot_figs.append(p_fig)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  else:
133
+ logging.warning(f"Plot figure generation failed or returned unexpected type for slot {i}, using placeholder. Figure: {p_fig}")
134
+ final_plot_figs.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot figure."))
135
+
136
+ while len(final_plot_figs) < num_expected_plots:
137
+ logging.warning(f"Padding missing plot figure with placeholder. Expected {num_expected_plots}, got {len(final_plot_figs)}.")
138
+ final_plot_figs.append(create_placeholder_plot(title="Missing Plot", message="Plot figure could not be generated."))
139
+
140
+ logging.info(f"Successfully generated {len(final_plot_figs)} plot figures for {num_expected_plots} UI slots.")
141
+ return [message] + final_plot_figs[:num_expected_plots]
 
 
 
 
142
 
143
  except Exception as e:
144
+ error_msg = f"❌ Error generating analytics plot figures: {e}"
145
  logging.error(error_msg, exc_info=True)
146
  placeholder_figs = [create_placeholder_plot(title="Plot Generation Error", message=str(e)) for _ in range(num_expected_plots)]
147
  return [error_msg] + placeholder_figs
 
149
 
150
  # --- Gradio UI Blocks ---
151
  with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
152
+ title="LinkedIn Organization Dashboard") as app:
153
 
154
  token_state = gr.State(value={
155
  "token": None, "client_id": None, "org_urn": None,
156
+ "bubble_posts_df": pd.DataFrame(),
157
+ "bubble_post_stats_df": pd.DataFrame(),
158
  "bubble_mentions_df": pd.DataFrame(),
159
  "bubble_follower_stats_df": pd.DataFrame(),
160
+ "fetch_count_for_api": 0,
 
 
 
161
  "url_user_token_temp_storage": None,
162
+ "config_date_col_posts": "published_at",
163
+ "config_date_col_mentions": "date",
164
  "config_date_col_followers": "date",
165
+ "config_media_type_col": "media_type",
166
+ "config_eb_labels_col": "eb_labels"
167
  })
168
 
169
  gr.Markdown("# πŸš€ LinkedIn Organization Dashboard")
 
176
  def initial_load_sequence(url_token, org_urn_val, current_state):
177
  logging.info(f"Initial load sequence triggered. Org URN: {org_urn_val}, URL Token: {'Present' if url_token else 'Absent'}")
178
  status_msg, new_state, btn_update = process_and_store_bubble_token(url_token, org_urn_val, current_state)
179
+ dashboard_content = display_main_dashboard(new_state)
180
  return status_msg, new_state, btn_update, dashboard_content
181
 
182
  with gr.Tabs() as tabs:
 
193
  show_progress="full"
194
  )
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  with gr.TabItem("2️⃣ Analytics", id="tab_analytics"):
197
  gr.Markdown("## πŸ“ˆ LinkedIn Performance Analytics")
198
+ gr.Markdown("Select a date range to filter analytics. Click πŸ’£ for insights.")
199
 
200
  analytics_status_md = gr.Markdown("Analytics status will appear here...")
201
 
202
  with gr.Row():
203
  date_filter_selector = gr.Radio(
204
  ["All Time", "Last 7 Days", "Last 30 Days", "Custom Range"],
205
+ label="Select Date Range", value="Last 30 Days"
 
206
  )
207
+ custom_start_date_picker = gr.DateTime(label="Start Date", visible=False, include_time=False, type="datetime")
208
+ custom_end_date_picker = gr.DateTime(label="End Date", visible=False, include_time=False, type="datetime")
209
 
210
  apply_filter_btn = gr.Button("πŸ” Apply Filter & Refresh Analytics", variant="primary")
211
 
 
219
  outputs=[custom_start_date_picker, custom_end_date_picker]
220
  )
221
 
222
+ # --- Define plot configurations ---
223
+ # (Order must match the order of figures returned by update_analytics_plots_figures)
224
+ plot_configs = [
225
+ {"label": "Posts Activity Over Time", "id": "posts_activity", "section": "Posts & Engagement Overview"},
226
+ {"label": "Post Engagement Types", "id": "engagement_type", "section": "Posts & Engagement Overview"},
227
+ {"label": "Mentions Activity Over Time", "id": "mentions_activity", "section": "Mentions Overview"},
228
+ {"label": "Mention Sentiment Distribution", "id": "mention_sentiment", "section": "Mentions Overview"},
229
+ {"label": "Followers Count Over Time", "id": "followers_count", "section": "Follower Dynamics"},
230
+ {"label": "Followers Growth Rate", "id": "followers_growth_rate", "section": "Follower Dynamics"},
231
+ {"label": "Followers by Location", "id": "followers_by_location", "section": "Follower Demographics"},
232
+ {"label": "Followers by Role (Function)", "id": "followers_by_role", "section": "Follower Demographics"},
233
+ {"label": "Followers by Industry", "id": "followers_by_industry", "section": "Follower Demographics"},
234
+ {"label": "Followers by Seniority", "id": "followers_by_seniority", "section": "Follower Demographics"},
235
+ {"label": "Engagement Rate Over Time", "id": "engagement_rate", "section": "Post Performance Insights"},
236
+ {"label": "Reach Over Time (Clicks)", "id": "reach_over_time", "section": "Post Performance Insights"},
237
+ {"label": "Impressions Over Time", "id": "impressions_over_time", "section": "Post Performance Insights"},
238
+ {"label": "Reactions (Likes) Over Time", "id": "likes_over_time", "section": "Post Performance Insights"},
239
+ {"label": "Clicks Over Time", "id": "clicks_over_time", "section": "Detailed Post Engagement Over Time"},
240
+ {"label": "Shares Over Time", "id": "shares_over_time", "section": "Detailed Post Engagement Over Time"},
241
+ {"label": "Comments Over Time", "id": "comments_over_time", "section": "Detailed Post Engagement Over Time"},
242
+ {"label": "Breakdown of Comments by Sentiment", "id": "comments_sentiment", "section": "Detailed Post Engagement Over Time"},
243
+ {"label": "Post Frequency", "id": "post_frequency_cs", "section": "Content Strategy Analysis"},
244
+ {"label": "Breakdown of Content by Format", "id": "content_format_breakdown_cs", "section": "Content Strategy Analysis"},
245
+ {"label": "Breakdown of Content by Topics", "id": "content_topic_breakdown_cs", "section": "Content Strategy Analysis"},
246
+ {"label": "Mentions Volume Over Time (Detailed)", "id": "mention_analysis_volume", "section": "Mention Analysis (Detailed)"},
247
+ {"label": "Breakdown of Mentions by Sentiment (Detailed)", "id": "mention_analysis_sentiment", "section": "Mention Analysis (Detailed)"}
248
+ ]
249
+ assert len(plot_configs) == 23, "Mismatch in number of plot configurations and expected plots."
250
 
251
+ # --- Build Analytics Tab UI using the function from ui_generators ---
252
+ # This function will create the gr.Markdown for sections and rows for plots.
253
+ # It needs to be called within this gr.Blocks() context.
254
+ plot_ui_objects = build_analytics_tab_ui_components(plot_configs)
 
 
 
 
 
 
255
 
256
+ active_insight_plot_id_state = gr.State(None) # Stores the plot_id of the currently open insight panel
257
+
258
+ # --- Bomb Button Click Handler ---
259
+ def handle_bomb_click(plot_id_clicked, current_active_plot_id, current_token_state):
260
+ logging.info(f"Bomb clicked for: {plot_id_clicked}. Currently active: {current_active_plot_id}")
261
+ updates = []
262
+ new_active_id = None
263
+
264
+ if plot_id_clicked == current_active_plot_id:
265
+ new_active_id = None # Toggle off
266
+ logging.info(f"Closing insights for {plot_id_clicked}")
267
+ else:
268
+ new_active_id = plot_id_clicked # Activate new one
269
+ logging.info(f"Opening insights for {plot_id_clicked}, closing others.")
270
+
271
+ for p_id_iter, ui_obj_dict in plot_ui_objects.items():
272
+ is_target_one = (p_id_iter == new_active_id)
273
+ updates.append(gr.update(visible=is_target_one)) # For insights_col visibility
274
+
275
+ if is_target_one:
276
+ # TODO: Implement actual insight generation logic here
277
+ insight_text = f"**Insights for {ui_obj_dict['label']}**\n\n"
278
+ insight_text += f"Plot ID: `{p_id_iter}`.\n"
279
+ insight_text += "Detailed analysis would involve examining trends, anomalies, and correlations related to this specific chart.\n"
280
+ insight_text += "For example, for 'Posts Activity', we might look for days with unusually high or low activity and correlate with external events or content types."
281
+ updates.append(gr.update(value=insight_text))
282
+ else:
283
+ updates.append(gr.update(value=f"Click πŸ’£ for insights on {ui_obj_dict['label']}...")) # Reset placeholder
284
+
285
+ updates.append(new_active_id) # New value for active_insight_plot_id_state
286
+ logging.info(f"Returning {len(updates)-1} UI updates. New active ID: {new_active_id}")
287
+ return updates
288
+
289
+ # --- Connect Bomb Buttons ---
290
+ bomb_click_dynamic_outputs = []
291
+ # The order of items in bomb_click_dynamic_outputs must match the order of iteration
292
+ # in handle_bomb_click when it creates its `updates` list.
293
+ # plot_ui_objects is a dictionary, so .keys() gives an arbitrary order if not Python 3.7+
294
+ # To be safe, iterate based on plot_configs order for constructing outputs.
295
+ for config in plot_configs:
296
+ p_id_key = config["id"]
297
+ bomb_click_dynamic_outputs.append(plot_ui_objects[p_id_key]["insights_col"])
298
+ bomb_click_dynamic_outputs.append(plot_ui_objects[p_id_key]["insights_md"])
299
+ bomb_click_dynamic_outputs.append(active_insight_plot_id_state)
300
+
301
+ for config in plot_configs:
302
+ plot_id = config["id"]
303
+ components_dict = plot_ui_objects[plot_id]
304
+ components_dict["bomb"].click(
305
+ fn=handle_bomb_click,
306
+ inputs=[gr.State(value=plot_id), active_insight_plot_id_state, token_state],
307
+ outputs=bomb_click_dynamic_outputs,
308
+ api_name=f"show_insights_{plot_id}" # Gradio handles None api_name if plot_id is None (though it shouldn't be)
309
+ )
310
 
311
+ # --- Function to Refresh All Analytics UI (Plots + Reset Insights) ---
312
+ def refresh_all_analytics_ui_elements(current_token_state, date_filter_val, custom_start_val, custom_end_val):
313
+ logging.info("Refreshing all analytics UI elements.")
314
+ plot_generation_results = update_analytics_plots_figures(
315
+ current_token_state, date_filter_val, custom_start_val, custom_end_val
316
+ )
317
+
318
+ status_message_update = plot_generation_results[0]
319
+ generated_plot_figures = plot_generation_results[1:]
320
+
321
+ all_updates = [status_message_update]
322
+
323
+ # Plot figure updates - iterate based on plot_configs to ensure order
324
+ for i, config in enumerate(plot_configs):
325
+ p_id_key = config["id"]
326
+ if i < len(generated_plot_figures):
327
+ all_updates.append(generated_plot_figures[i])
328
+ else:
329
+ logging.error(f"Mismatch: Expected figure for {p_id_key} but not enough figures generated.")
330
+ all_updates.append(create_placeholder_plot("Figure Error", f"No figure for {p_id_key}"))
331
+
332
+ # Insight column visibility and markdown content reset - iterate based on plot_configs
333
+ for config in plot_configs:
334
+ p_id_key = config["id"]
335
+ ui_obj_dict_val = plot_ui_objects[p_id_key]
336
+ all_updates.append(gr.update(visible=False)) # Hide insights_col
337
+ all_updates.append(gr.update(value=f"Click πŸ’£ for insights on {ui_obj_dict_val['label']}...")) # Reset insights_md
338
+
339
+ all_updates.append(None) # Reset active_insight_plot_id_state
340
+ return all_updates
341
+
342
+ # --- Define outputs for the apply_filter_btn and sync.then() ---
343
+ apply_filter_and_sync_outputs = [analytics_status_md]
344
+ # Iterate based on plot_configs to ensure order
345
+ for config in plot_configs: # Plot components
346
+ apply_filter_and_sync_outputs.append(plot_ui_objects[config["id"]]["plot"])
347
+ for config in plot_configs: # Insight column components
348
+ apply_filter_and_sync_outputs.append(plot_ui_objects[config["id"]]["insights_col"])
349
+ for config in plot_configs: # Insight markdown components
350
+ apply_filter_and_sync_outputs.append(plot_ui_objects[config["id"]]["insights_md"])
351
+ apply_filter_and_sync_outputs.append(active_insight_plot_id_state) # State component
352
+
353
+ # --- Connect Apply Filter Button ---
354
  apply_filter_btn.click(
355
+ fn=refresh_all_analytics_ui_elements,
 
 
 
 
 
 
 
 
356
  inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
357
+ outputs=apply_filter_and_sync_outputs,
358
  show_progress="full"
359
  )
360
 
361
  with gr.TabItem("3️⃣ Mentions", id="tab_mentions"):
362
  refresh_mentions_display_btn = gr.Button("πŸ”„ Refresh Mentions Display (from local data)", variant="secondary")
363
  mentions_html = gr.HTML("Mentions data loads from Bubble after sync. Click refresh to view current local data.")
364
+ mentions_sentiment_dist_plot = gr.Plot(label="Mention Sentiment Distribution")
365
  refresh_mentions_display_btn.click(
366
  fn=run_mentions_tab_display, inputs=[token_state],
367
+ outputs=[mentions_html, mentions_sentiment_dist_plot],
368
  show_progress="full"
369
  )
370
 
 
372
  refresh_follower_stats_btn = gr.Button("πŸ”„ Refresh Follower Stats Display (from local data)", variant="secondary")
373
  follower_stats_html = gr.HTML("Follower statistics load from Bubble after sync. Click refresh to view current local data.")
374
  with gr.Row():
375
+ fs_plot_monthly_gains = gr.Plot(label="Monthly Follower Gains")
376
  with gr.Row():
377
  fs_plot_seniority = gr.Plot(label="Followers by Seniority (Top 10 Organic)")
378
  fs_plot_industry = gr.Plot(label="Followers by Industry (Top 10 Organic)")
379
 
380
  refresh_follower_stats_btn.click(
381
  fn=run_follower_stats_tab_display, inputs=[token_state],
382
+ outputs=[follower_stats_html, fs_plot_monthly_gains, fs_plot_seniority, fs_plot_industry],
383
  show_progress="full"
384
  )
385
 
386
+ # --- Define the full sync_click_event chain HERE, now that analytics outputs are known ---
387
+ sync_event_part1 = sync_data_btn.click(
388
+ fn=sync_all_linkedin_data_orchestrator,
389
+ inputs=[token_state],
390
+ outputs=[sync_status_html_output, token_state],
391
+ show_progress="full"
392
+ )
393
+ sync_event_part2 = sync_event_part1.then(
394
+ fn=process_and_store_bubble_token,
395
+ inputs=[url_user_token_display, org_urn_display, token_state],
396
+ outputs=[status_box, token_state, sync_data_btn],
397
+ show_progress=False
398
+ )
399
+ sync_event_part3 = sync_event_part2.then(
400
+ fn=display_main_dashboard,
401
+ inputs=[token_state],
402
+ outputs=[dashboard_display_html],
403
+ show_progress=False
404
+ )
405
+ sync_event_final = sync_event_part3.then(
406
+ fn=refresh_all_analytics_ui_elements,
407
+ inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
408
+ outputs=apply_filter_and_sync_outputs,
409
+ show_progress="full"
410
+ )
411
+
412
 
413
  if __name__ == "__main__":
414
  if not os.environ.get(LINKEDIN_CLIENT_ID_ENV_VAR):
 
424
  logging.error("Matplotlib is not installed. Plots will not be generated.")
425
 
426
  app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
427
+