Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -32,7 +32,13 @@ from analytics_plot_generator import (
|
|
32 |
generate_engagement_rate_over_time_plot,
|
33 |
generate_reach_over_time_plot,
|
34 |
generate_impressions_over_time_plot,
|
35 |
-
create_placeholder_plot # For initializing plots
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
)
|
37 |
|
38 |
# Configure logging
|
@@ -45,14 +51,18 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
|
|
45 |
"""
|
46 |
logging.info(f"Updating analytics plots. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
|
47 |
|
|
|
|
|
|
|
48 |
if not token_state_value or not token_state_value.get("token"):
|
49 |
message = "β Access denied. No token. Cannot generate analytics."
|
50 |
logging.warning(message)
|
51 |
-
num_expected_plots = 13
|
52 |
placeholder_figs = [create_placeholder_plot(title="Access Denied", message="No token.") for _ in range(num_expected_plots)]
|
53 |
return [message] + placeholder_figs
|
54 |
|
55 |
try:
|
|
|
|
|
56 |
(filtered_merged_posts_df,
|
57 |
filtered_mentions_df,
|
58 |
date_filtered_follower_stats_df,
|
@@ -61,18 +71,22 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
|
|
61 |
prepare_filtered_analytics_data(
|
62 |
token_state_value, date_filter_option, custom_start_date, custom_end_date
|
63 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
except Exception as e:
|
65 |
error_msg = f"β Error preparing analytics data: {e}"
|
66 |
logging.error(error_msg, exc_info=True)
|
67 |
-
num_expected_plots = 13
|
68 |
placeholder_figs = [create_placeholder_plot(title="Data Preparation Error", message=str(e)) for _ in range(num_expected_plots)]
|
69 |
return [error_msg] + placeholder_figs
|
70 |
|
71 |
date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
|
72 |
date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
|
73 |
-
# This 'date_column_followers' from token_state is for the *source* DataFrame's date column,
|
74 |
-
# but the plot generator now uses 'date_info_column' for the 'category_name' that holds date strings.
|
75 |
-
# We'll use the default 'category_name' in the plot functions directly.
|
76 |
# config_date_col_followers_source = token_state_value.get("config_date_col_followers", "date")
|
77 |
|
78 |
|
@@ -80,22 +94,19 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
|
|
80 |
logging.info(f"Date-Filtered Follower Stats: {len(date_filtered_follower_stats_df)} rows, Raw Follower Stats: {len(raw_follower_stats_df)} rows.")
|
81 |
|
82 |
try:
|
|
|
83 |
plot_posts_activity = generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts)
|
84 |
plot_engagement_type = generate_engagement_type_plot(filtered_merged_posts_df)
|
85 |
plot_mentions_activity = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
|
86 |
plot_mention_sentiment = generate_mention_sentiment_plot(filtered_mentions_df)
|
87 |
|
88 |
-
# Corrected calls for follower plots: use date_info_column (defaults to 'category_name' in plot generator)
|
89 |
plot_followers_count = generate_followers_count_over_time_plot(
|
90 |
date_filtered_follower_stats_df,
|
91 |
-
|
92 |
-
# organic_count_col, paid_count_col are defaulted
|
93 |
-
type_filter_column='follower_count_type', # Ensure this column exists
|
94 |
type_value='follower_gains_monthly'
|
95 |
)
|
96 |
plot_followers_growth_rate = generate_followers_growth_rate_plot(
|
97 |
date_filtered_follower_stats_df,
|
98 |
-
# date_info_column is defaulted
|
99 |
type_filter_column='follower_count_type',
|
100 |
type_value='follower_gains_monthly'
|
101 |
)
|
@@ -109,6 +120,24 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
|
|
109 |
plot_reach_over_time = generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, reach_col='clickCount')
|
110 |
plot_impressions_over_time = generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, impressions_col='impressionCount')
|
111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
message = f"π Analytics updated for period: {date_filter_option}"
|
113 |
if date_filter_option == "Custom Range":
|
114 |
s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
|
@@ -119,23 +148,46 @@ def update_analytics_plots(token_state_value, date_filter_option, custom_start_d
|
|
119 |
plot_posts_activity, plot_engagement_type, plot_mentions_activity, plot_mention_sentiment,
|
120 |
plot_followers_count, plot_followers_growth_rate,
|
121 |
plot_followers_by_location, plot_followers_by_role, plot_followers_by_industry, plot_followers_by_seniority,
|
122 |
-
plot_engagement_rate, plot_reach_over_time, plot_impressions_over_time
|
|
|
|
|
|
|
|
|
123 |
]
|
124 |
num_plots_generated = sum(1 for p in all_generated_plots if p is not None and not isinstance(p, str))
|
125 |
-
logging.info(f"Successfully generated {num_plots_generated} plots.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
|
127 |
-
return [message] + all_generated_plots
|
128 |
except Exception as e:
|
129 |
error_msg = f"β Error generating analytics plots: {e}"
|
130 |
logging.error(error_msg, exc_info=True)
|
131 |
-
num_expected_plots = 13
|
132 |
placeholder_figs = [create_placeholder_plot(title="Plot Generation Error", message=str(e)) for _ in range(num_expected_plots)]
|
133 |
return [error_msg] + placeholder_figs
|
134 |
|
135 |
|
136 |
# --- Gradio UI Blocks ---
|
137 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
138 |
-
|
139 |
|
140 |
token_state = gr.State(value={
|
141 |
"token": None, "client_id": None, "org_urn": None,
|
@@ -143,11 +195,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
143 |
"bubble_post_stats_df": pd.DataFrame(),
|
144 |
"bubble_mentions_df": pd.DataFrame(),
|
145 |
"bubble_follower_stats_df": pd.DataFrame(),
|
|
|
146 |
"fetch_count_for_api": 0,
|
147 |
"url_user_token_temp_storage": None,
|
148 |
"config_date_col_posts": "published_at",
|
149 |
-
"config_date_col_mentions": "date",
|
150 |
-
"config_date_col_followers": "date"
|
151 |
})
|
152 |
|
153 |
gr.Markdown("# π LinkedIn Organization Dashboard")
|
@@ -207,8 +260,8 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
207 |
label="Select Date Range (for Posts, Mentions, and some Follower time-series)",
|
208 |
value="Last 30 Days"
|
209 |
)
|
210 |
-
custom_start_date_picker = gr.DateTime(label="Start Date (Custom)", visible=False, include_time=False, type="
|
211 |
-
custom_end_date_picker = gr.DateTime(label="End Date (Custom)", visible=False, include_time=False, type="
|
212 |
|
213 |
apply_filter_btn = gr.Button("π Apply Filter & Refresh Analytics", variant="primary")
|
214 |
|
@@ -248,9 +301,22 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
248 |
gr.Markdown("### Post Performance Insights (Filtered by Date)")
|
249 |
with gr.Row():
|
250 |
engagement_rate_plot = gr.Plot(label="Engagement Rate Over Time")
|
251 |
-
|
252 |
-
|
253 |
impressions_over_time_plot = gr.Plot(label="Impressions Over Time")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
254 |
|
255 |
analytics_plot_outputs = [
|
256 |
analytics_status_md, posts_activity_plot, engagement_type_plot,
|
@@ -258,7 +324,11 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
258 |
followers_count_plot, followers_growth_rate_plot,
|
259 |
followers_by_location_plot, followers_by_role_plot,
|
260 |
followers_by_industry_plot, followers_by_seniority_plot,
|
261 |
-
engagement_rate_plot, reach_over_time_plot, impressions_over_time_plot
|
|
|
|
|
|
|
|
|
262 |
]
|
263 |
|
264 |
apply_filter_btn.click(
|
@@ -268,6 +338,7 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
|
268 |
show_progress="full"
|
269 |
)
|
270 |
|
|
|
271 |
sync_click_event.then(
|
272 |
fn=update_analytics_plots,
|
273 |
inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
|
|
|
32 |
generate_engagement_rate_over_time_plot,
|
33 |
generate_reach_over_time_plot,
|
34 |
generate_impressions_over_time_plot,
|
35 |
+
create_placeholder_plot, # For initializing plots
|
36 |
+
# --- Import new plot functions ---
|
37 |
+
generate_likes_over_time_plot,
|
38 |
+
generate_clicks_over_time_plot,
|
39 |
+
generate_shares_over_time_plot,
|
40 |
+
generate_comments_over_time_plot,
|
41 |
+
generate_comments_sentiment_breakdown_plot
|
42 |
)
|
43 |
|
44 |
# Configure logging
|
|
|
51 |
"""
|
52 |
logging.info(f"Updating analytics plots. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
|
53 |
|
54 |
+
# --- Increased number of expected plots ---
|
55 |
+
num_expected_plots = 18 # Was 13, added 5 new plots
|
56 |
+
|
57 |
if not token_state_value or not token_state_value.get("token"):
|
58 |
message = "β Access denied. No token. Cannot generate analytics."
|
59 |
logging.warning(message)
|
|
|
60 |
placeholder_figs = [create_placeholder_plot(title="Access Denied", message="No token.") for _ in range(num_expected_plots)]
|
61 |
return [message] + placeholder_figs
|
62 |
|
63 |
try:
|
64 |
+
# prepare_filtered_analytics_data might need to be updated if new DFs are required for new plots (e.g. comment sentiment)
|
65 |
+
# For now, we assume it returns the same set of DFs and new plots will try to use them or handle missing data.
|
66 |
(filtered_merged_posts_df,
|
67 |
filtered_mentions_df,
|
68 |
date_filtered_follower_stats_df,
|
|
|
71 |
prepare_filtered_analytics_data(
|
72 |
token_state_value, date_filter_option, custom_start_date, custom_end_date
|
73 |
)
|
74 |
+
|
75 |
+
# Hypothetical: If prepare_filtered_analytics_data was updated to return comment sentiment data:
|
76 |
+
# filtered_comments_with_sentiment_df = ... # (This would be the 7th item in the tuple)
|
77 |
+
# For now, we will pass filtered_merged_posts_df to generate_comments_sentiment_breakdown_plot,
|
78 |
+
# and that function will handle missing sentiment columns by showing a placeholder.
|
79 |
+
# Or, if you have comment sentiment data in another DataFrame in token_state, retrieve it here.
|
80 |
+
# e.g., comments_df_with_sentiment = token_state_value.get("bubble_comments_sentiment_df", pd.DataFrame())
|
81 |
+
|
82 |
except Exception as e:
|
83 |
error_msg = f"β Error preparing analytics data: {e}"
|
84 |
logging.error(error_msg, exc_info=True)
|
|
|
85 |
placeholder_figs = [create_placeholder_plot(title="Data Preparation Error", message=str(e)) for _ in range(num_expected_plots)]
|
86 |
return [error_msg] + placeholder_figs
|
87 |
|
88 |
date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
|
89 |
date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
|
|
|
|
|
|
|
90 |
# config_date_col_followers_source = token_state_value.get("config_date_col_followers", "date")
|
91 |
|
92 |
|
|
|
94 |
logging.info(f"Date-Filtered Follower Stats: {len(date_filtered_follower_stats_df)} rows, Raw Follower Stats: {len(raw_follower_stats_df)} rows.")
|
95 |
|
96 |
try:
|
97 |
+
# Existing plots
|
98 |
plot_posts_activity = generate_posts_activity_plot(filtered_merged_posts_df, date_column=date_column_posts)
|
99 |
plot_engagement_type = generate_engagement_type_plot(filtered_merged_posts_df)
|
100 |
plot_mentions_activity = generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions)
|
101 |
plot_mention_sentiment = generate_mention_sentiment_plot(filtered_mentions_df)
|
102 |
|
|
|
103 |
plot_followers_count = generate_followers_count_over_time_plot(
|
104 |
date_filtered_follower_stats_df,
|
105 |
+
type_filter_column='follower_count_type',
|
|
|
|
|
106 |
type_value='follower_gains_monthly'
|
107 |
)
|
108 |
plot_followers_growth_rate = generate_followers_growth_rate_plot(
|
109 |
date_filtered_follower_stats_df,
|
|
|
110 |
type_filter_column='follower_count_type',
|
111 |
type_value='follower_gains_monthly'
|
112 |
)
|
|
|
120 |
plot_reach_over_time = generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, reach_col='clickCount')
|
121 |
plot_impressions_over_time = generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, impressions_col='impressionCount')
|
122 |
|
123 |
+
# --- Generate new plots ---
|
124 |
+
plot_likes_over_time = generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, likes_col='likeCount')
|
125 |
+
plot_clicks_over_time = generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, clicks_col='clickCount')
|
126 |
+
plot_shares_over_time = generate_shares_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, shares_col='shareCount')
|
127 |
+
plot_comments_over_time = generate_comments_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts, comments_col='commentCount')
|
128 |
+
|
129 |
+
# For comment sentiment, pass a DataFrame that is expected to have comment-level sentiment.
|
130 |
+
# If `filtered_merged_posts_df` is passed and lacks 'comment_sentiment' column, the plot function will show a placeholder.
|
131 |
+
# If you have a specific df for this, e.g., `filtered_comments_with_sentiment_df` from `prepare_filtered_analytics_data` (if modified)
|
132 |
+
# or from `token_state_value.get("bubble_comments_sentiment_df")`, use that one.
|
133 |
+
# For this example, we assume `filtered_merged_posts_df` is passed and the plot function handles it.
|
134 |
+
plot_comments_sentiment_breakdown = generate_comments_sentiment_breakdown_plot(
|
135 |
+
filtered_merged_posts_df, # Or your specific df with comment sentiments
|
136 |
+
sentiment_column='sentiment' # Assuming 'sentiment' column in post_df might be a proxy, or change to 'comment_sentiment' if that column exists
|
137 |
+
# The plot function will show a placeholder if this column isn't suitable or found.
|
138 |
+
)
|
139 |
+
|
140 |
+
|
141 |
message = f"π Analytics updated for period: {date_filter_option}"
|
142 |
if date_filter_option == "Custom Range":
|
143 |
s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Any"
|
|
|
148 |
plot_posts_activity, plot_engagement_type, plot_mentions_activity, plot_mention_sentiment,
|
149 |
plot_followers_count, plot_followers_growth_rate,
|
150 |
plot_followers_by_location, plot_followers_by_role, plot_followers_by_industry, plot_followers_by_seniority,
|
151 |
+
plot_engagement_rate, plot_reach_over_time, plot_impressions_over_time,
|
152 |
+
# --- Add new plot objects to the list ---
|
153 |
+
plot_likes_over_time, plot_clicks_over_time,
|
154 |
+
plot_shares_over_time, plot_comments_over_time,
|
155 |
+
plot_comments_sentiment_breakdown
|
156 |
]
|
157 |
num_plots_generated = sum(1 for p in all_generated_plots if p is not None and not isinstance(p, str))
|
158 |
+
logging.info(f"Successfully generated {num_plots_generated} plots out of {num_expected_plots} expected.")
|
159 |
+
|
160 |
+
# Ensure the number of returned plots matches num_expected_plots, padding with placeholders if necessary
|
161 |
+
# This is crucial if some plot functions might return None on error and we need to match the Gradio outputs list length
|
162 |
+
final_plots_list = []
|
163 |
+
for p in all_generated_plots:
|
164 |
+
if p is not None and not isinstance(p, str): # isinstance check for safety, though plots should be figs
|
165 |
+
final_plots_list.append(p)
|
166 |
+
else: # If a plot failed and returned None or an error string (which it shouldn't, should be placeholder fig)
|
167 |
+
logging.warning(f"A plot generation failed or returned unexpected type, using placeholder. Plot: {p}")
|
168 |
+
final_plots_list.append(create_placeholder_plot(title="Plot Error", message="Failed to generate this plot."))
|
169 |
+
|
170 |
+
# If fewer plots were generated than expected (e.g. due to early exit or major error in a plot function)
|
171 |
+
while len(final_plots_list) < num_expected_plots:
|
172 |
+
logging.warning(f"Padding missing plot with placeholder. Expected {num_expected_plots}, got {len(final_plots_list)} so far.")
|
173 |
+
final_plots_list.append(create_placeholder_plot(title="Missing Plot", message="Plot could not be generated."))
|
174 |
+
if len(final_plots_list) > num_expected_plots + 5: # Safety break
|
175 |
+
logging.error("Too many placeholders added, breaking loop.")
|
176 |
+
break
|
177 |
+
|
178 |
+
|
179 |
+
return [message] + final_plots_list[:num_expected_plots] # Ensure correct number of outputs
|
180 |
|
|
|
181 |
except Exception as e:
|
182 |
error_msg = f"β Error generating analytics plots: {e}"
|
183 |
logging.error(error_msg, exc_info=True)
|
|
|
184 |
placeholder_figs = [create_placeholder_plot(title="Plot Generation Error", message=str(e)) for _ in range(num_expected_plots)]
|
185 |
return [error_msg] + placeholder_figs
|
186 |
|
187 |
|
188 |
# --- Gradio UI Blocks ---
|
189 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
|
190 |
+
title="LinkedIn Organization Dashboard") as app:
|
191 |
|
192 |
token_state = gr.State(value={
|
193 |
"token": None, "client_id": None, "org_urn": None,
|
|
|
195 |
"bubble_post_stats_df": pd.DataFrame(),
|
196 |
"bubble_mentions_df": pd.DataFrame(),
|
197 |
"bubble_follower_stats_df": pd.DataFrame(),
|
198 |
+
# Consider adding "bubble_comments_sentiment_df": pd.DataFrame() if you plan to fetch this data
|
199 |
"fetch_count_for_api": 0,
|
200 |
"url_user_token_temp_storage": None,
|
201 |
"config_date_col_posts": "published_at",
|
202 |
+
"config_date_col_mentions": "date",
|
203 |
+
"config_date_col_followers": "date"
|
204 |
})
|
205 |
|
206 |
gr.Markdown("# π LinkedIn Organization Dashboard")
|
|
|
260 |
label="Select Date Range (for Posts, Mentions, and some Follower time-series)",
|
261 |
value="Last 30 Days"
|
262 |
)
|
263 |
+
custom_start_date_picker = gr.DateTime(label="Start Date (Custom)", visible=False, include_time=False, type="datetime") # Changed to datetime
|
264 |
+
custom_end_date_picker = gr.DateTime(label="End Date (Custom)", visible=False, include_time=False, type="datetime") # Changed to datetime
|
265 |
|
266 |
apply_filter_btn = gr.Button("π Apply Filter & Refresh Analytics", variant="primary")
|
267 |
|
|
|
301 |
gr.Markdown("### Post Performance Insights (Filtered by Date)")
|
302 |
with gr.Row():
|
303 |
engagement_rate_plot = gr.Plot(label="Engagement Rate Over Time")
|
304 |
+
reach_over_time_plot = gr.Plot(label="Reach Over Time (Clicks)") # This was originally in its own row
|
305 |
+
with gr.Row(): # Moved impressions to be paired with reach if desired, or keep separate
|
306 |
impressions_over_time_plot = gr.Plot(label="Impressions Over Time")
|
307 |
+
# New plots will start here, keeping 2 per row
|
308 |
+
likes_over_time_plot = gr.Plot(label="Reactions (Likes) Over Time")
|
309 |
+
|
310 |
+
gr.Markdown("### Detailed Post Engagement Over Time (Filtered by Date)")
|
311 |
+
with gr.Row():
|
312 |
+
clicks_over_time_plot = gr.Plot(label="Clicks Over Time")
|
313 |
+
shares_over_time_plot = gr.Plot(label="Shares Over Time")
|
314 |
+
with gr.Row():
|
315 |
+
comments_over_time_plot = gr.Plot(label="Comments Over Time")
|
316 |
+
# For the 5th new plot, "Breakdown of Comments by Sentiment"
|
317 |
+
# It will be alone in this row, or you can add another plot next to it later.
|
318 |
+
comments_sentiment_plot = gr.Plot(label="Breakdown of Comments by Sentiment")
|
319 |
+
|
320 |
|
321 |
analytics_plot_outputs = [
|
322 |
analytics_status_md, posts_activity_plot, engagement_type_plot,
|
|
|
324 |
followers_count_plot, followers_growth_rate_plot,
|
325 |
followers_by_location_plot, followers_by_role_plot,
|
326 |
followers_by_industry_plot, followers_by_seniority_plot,
|
327 |
+
engagement_rate_plot, reach_over_time_plot, impressions_over_time_plot,
|
328 |
+
# --- Add new plot components to the output list in the correct order ---
|
329 |
+
likes_over_time_plot, clicks_over_time_plot,
|
330 |
+
shares_over_time_plot, comments_over_time_plot,
|
331 |
+
comments_sentiment_plot
|
332 |
]
|
333 |
|
334 |
apply_filter_btn.click(
|
|
|
338 |
show_progress="full"
|
339 |
)
|
340 |
|
341 |
+
# Also update analytics after sync
|
342 |
sync_click_event.then(
|
343 |
fn=update_analytics_plots,
|
344 |
inputs=[token_state, date_filter_selector, custom_start_date_picker, custom_end_date_picker],
|