Spaces:
Running
Running
Create ui_generators.py
Browse files- ui_generators.py +280 -0
ui_generators.py
ADDED
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ui_generators.py
|
2 |
+
"""
|
3 |
+
Generates HTML content and Matplotlib plots for the Gradio UI tabs.
|
4 |
+
"""
|
5 |
+
import pandas as pd
|
6 |
+
import logging
|
7 |
+
import matplotlib.pyplot as plt
|
8 |
+
import matplotlib # To ensure backend is switched before any plt import from other modules if app structure changes
|
9 |
+
|
10 |
+
# Switch backend for Matplotlib to Agg for Gradio compatibility
|
11 |
+
matplotlib.use('Agg')
|
12 |
+
|
13 |
+
|
14 |
+
# Assuming config.py contains all necessary constants
|
15 |
+
from config import (
|
16 |
+
BUBBLE_POST_DATE_COLUMN_NAME, BUBBLE_MENTIONS_DATE_COLUMN_NAME, BUBBLE_MENTIONS_ID_COLUMN_NAME,
|
17 |
+
FOLLOWER_STATS_TYPE_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN,
|
18 |
+
FOLLOWER_STATS_PAID_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN_DT, UI_DATE_FORMAT, UI_MONTH_FORMAT
|
19 |
+
)
|
20 |
+
|
21 |
+
def display_main_dashboard(token_state):
|
22 |
+
"""Generates HTML for the main dashboard display using data from token_state."""
|
23 |
+
if not token_state or not token_state.get("token"):
|
24 |
+
logging.warning("Dashboard display: Access denied. No token available.")
|
25 |
+
return "❌ Access denied. No token available for dashboard."
|
26 |
+
|
27 |
+
html_parts = ["<div style='padding:10px;'><h3>Dashboard Overview</h3>"]
|
28 |
+
|
29 |
+
# Display Recent Posts
|
30 |
+
posts_df = token_state.get("bubble_posts_df", pd.DataFrame())
|
31 |
+
html_parts.append(f"<h4>Recent Posts ({len(posts_df)} in Bubble):</h4>")
|
32 |
+
if not posts_df.empty:
|
33 |
+
cols_to_show_posts = [col for col in [BUBBLE_POST_DATE_COLUMN_NAME, 'text', 'sentiment', 'summary_text', 'li_eb_label'] if col in posts_df.columns]
|
34 |
+
if not cols_to_show_posts:
|
35 |
+
html_parts.append("<p>No relevant post columns found to display.</p>")
|
36 |
+
else:
|
37 |
+
display_df_posts = posts_df.copy()
|
38 |
+
if BUBBLE_POST_DATE_COLUMN_NAME in display_df_posts.columns:
|
39 |
+
try:
|
40 |
+
display_df_posts[BUBBLE_POST_DATE_COLUMN_NAME] = pd.to_datetime(display_df_posts[BUBBLE_POST_DATE_COLUMN_NAME], errors='coerce').dt.strftime(UI_DATE_FORMAT)
|
41 |
+
display_df_posts = display_df_posts.sort_values(by=BUBBLE_POST_DATE_COLUMN_NAME, ascending=False)
|
42 |
+
except Exception as e:
|
43 |
+
logging.error(f"Error formatting post dates for display: {e}")
|
44 |
+
html_parts.append("<p>Error formatting post dates.</p>")
|
45 |
+
html_parts.append(display_df_posts[cols_to_show_posts].head().to_html(escape=False, index=False, classes="table table-striped table-sm"))
|
46 |
+
else:
|
47 |
+
html_parts.append("<p>No posts loaded from Bubble.</p>")
|
48 |
+
html_parts.append("<hr/>")
|
49 |
+
|
50 |
+
# Display Recent Mentions
|
51 |
+
mentions_df = token_state.get("bubble_mentions_df", pd.DataFrame())
|
52 |
+
html_parts.append(f"<h4>Recent Mentions ({len(mentions_df)} in Bubble):</h4>")
|
53 |
+
if not mentions_df.empty:
|
54 |
+
cols_to_show_mentions = [col for col in [BUBBLE_MENTIONS_DATE_COLUMN_NAME, "mention_text", "sentiment_label"] if col in mentions_df.columns]
|
55 |
+
if not cols_to_show_mentions:
|
56 |
+
html_parts.append("<p>No relevant mention columns found to display.</p>")
|
57 |
+
else:
|
58 |
+
display_df_mentions = mentions_df.copy()
|
59 |
+
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in display_df_mentions.columns:
|
60 |
+
try:
|
61 |
+
display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(display_df_mentions[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce').dt.strftime(UI_DATE_FORMAT)
|
62 |
+
display_df_mentions = display_df_mentions.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False)
|
63 |
+
except Exception as e:
|
64 |
+
logging.error(f"Error formatting mention dates for display: {e}")
|
65 |
+
html_parts.append("<p>Error formatting mention dates.</p>")
|
66 |
+
html_parts.append(display_df_mentions[cols_to_show_mentions].head().to_html(escape=False, index=False, classes="table table-striped table-sm"))
|
67 |
+
else:
|
68 |
+
html_parts.append("<p>No mentions loaded from Bubble.</p>")
|
69 |
+
html_parts.append("<hr/>")
|
70 |
+
|
71 |
+
# Display Follower Statistics Summary
|
72 |
+
follower_stats_df = token_state.get("bubble_follower_stats_df", pd.DataFrame())
|
73 |
+
html_parts.append(f"<h4>Follower Statistics ({len(follower_stats_df)} entries in Bubble):</h4>")
|
74 |
+
if not follower_stats_df.empty:
|
75 |
+
monthly_gains = follower_stats_df[follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_gains_monthly'].copy()
|
76 |
+
if not monthly_gains.empty and FOLLOWER_STATS_CATEGORY_COLUMN in monthly_gains.columns and \
|
77 |
+
FOLLOWER_STATS_ORGANIC_COLUMN in monthly_gains.columns and FOLLOWER_STATS_PAID_COLUMN in monthly_gains.columns:
|
78 |
+
try:
|
79 |
+
# FOLLOWER_STATS_CATEGORY_COLUMN for monthly gains is 'YYYY-MM-DD'
|
80 |
+
monthly_gains.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN_DT] = pd.to_datetime(monthly_gains[FOLLOWER_STATS_CATEGORY_COLUMN], errors='coerce')
|
81 |
+
# Format original date column for display after sorting by datetime
|
82 |
+
monthly_gains_display = monthly_gains.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=False)
|
83 |
+
latest_gain = monthly_gains_display.head(1).copy() # Work with a copy for modification
|
84 |
+
if not latest_gain.empty:
|
85 |
+
latest_gain.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN] = latest_gain[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_DATE_FORMAT) # or UI_MONTH_FORMAT
|
86 |
+
html_parts.append("<h5>Latest Monthly Follower Gain:</h5>")
|
87 |
+
html_parts.append(latest_gain[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].to_html(escape=True, index=False, classes="table table-sm"))
|
88 |
+
else:
|
89 |
+
html_parts.append("<p>No valid monthly follower gain data to display after processing.</p>")
|
90 |
+
except Exception as e:
|
91 |
+
logging.error(f"Error formatting follower gain dates for display: {e}", exc_info=True)
|
92 |
+
html_parts.append("<p>Error displaying monthly follower gain data.</p>")
|
93 |
+
else:
|
94 |
+
html_parts.append("<p>No monthly follower gain data or required columns are missing.</p>")
|
95 |
+
|
96 |
+
demographics_count = len(follower_stats_df[follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] != 'follower_gains_monthly'])
|
97 |
+
html_parts.append(f"<p>Total demographic entries (seniority, industry, etc.): {demographics_count}</p>")
|
98 |
+
else:
|
99 |
+
html_parts.append("<p>No follower statistics loaded from Bubble.</p>")
|
100 |
+
|
101 |
+
html_parts.append("</div>")
|
102 |
+
return "".join(html_parts)
|
103 |
+
|
104 |
+
|
105 |
+
def run_mentions_tab_display(token_state):
|
106 |
+
"""Generates HTML and a plot for the Mentions tab."""
|
107 |
+
logging.info("Updating Mentions Tab display.")
|
108 |
+
if not token_state or not token_state.get("token"):
|
109 |
+
logging.warning("Mentions tab: Access denied. No token.")
|
110 |
+
return "❌ Access denied. No token available for mentions.", None
|
111 |
+
|
112 |
+
mentions_df = token_state.get("bubble_mentions_df", pd.DataFrame())
|
113 |
+
if mentions_df.empty:
|
114 |
+
logging.info("Mentions tab: No mentions data in Bubble.")
|
115 |
+
return "<p style='text-align:center;'>No mentions data in Bubble. Try syncing.</p>", None
|
116 |
+
|
117 |
+
html_parts = ["<h3 style='text-align:center;'>Recent Mentions</h3>"]
|
118 |
+
display_columns = [col for col in [BUBBLE_MENTIONS_DATE_COLUMN_NAME, "mention_text", "sentiment_label", BUBBLE_MENTIONS_ID_COLUMN_NAME] if col in mentions_df.columns]
|
119 |
+
|
120 |
+
mentions_df_display = mentions_df.copy()
|
121 |
+
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in mentions_df_display.columns:
|
122 |
+
try:
|
123 |
+
mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce').dt.strftime(UI_DATE_FORMAT)
|
124 |
+
mentions_df_display = mentions_df_display.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False)
|
125 |
+
except Exception as e:
|
126 |
+
logging.error(f"Error formatting mention dates for tab display: {e}")
|
127 |
+
html_parts.append("<p>Error formatting mention dates.</p>")
|
128 |
+
|
129 |
+
if not display_columns or mentions_df_display[display_columns].empty:
|
130 |
+
html_parts.append("<p>Required columns for mentions display are missing or no data after processing.</p>")
|
131 |
+
else:
|
132 |
+
html_parts.append(mentions_df_display[display_columns].head(20).to_html(escape=False, index=False, classes="table table-sm"))
|
133 |
+
|
134 |
+
mentions_html_output = "\n".join(html_parts)
|
135 |
+
fig = None
|
136 |
+
if not mentions_df.empty and "sentiment_label" in mentions_df.columns:
|
137 |
+
try:
|
138 |
+
fig_plot, ax = plt.subplots(figsize=(6,4))
|
139 |
+
sentiment_counts = mentions_df["sentiment_label"].value_counts()
|
140 |
+
sentiment_counts.plot(kind='bar', ax=ax, color=['#4CAF50', '#FFC107', '#F44336', '#9E9E9E', '#2196F3'])
|
141 |
+
ax.set_title("Mention Sentiment Distribution")
|
142 |
+
ax.set_ylabel("Count")
|
143 |
+
plt.xticks(rotation=45, ha='right')
|
144 |
+
plt.tight_layout()
|
145 |
+
fig = fig_plot
|
146 |
+
logging.info("Mentions tab: Sentiment distribution plot generated.")
|
147 |
+
except Exception as e:
|
148 |
+
logging.error(f"Error generating mentions plot: {e}", exc_info=True)
|
149 |
+
fig = None
|
150 |
+
else:
|
151 |
+
logging.info("Mentions tab: Not enough data or 'sentiment_label' column missing for plot.")
|
152 |
+
|
153 |
+
return mentions_html_output, fig
|
154 |
+
|
155 |
+
|
156 |
+
def run_follower_stats_tab_display(token_state):
|
157 |
+
"""Generates HTML and plots for the Follower Stats tab."""
|
158 |
+
logging.info("Updating Follower Stats Tab display.")
|
159 |
+
if not token_state or not token_state.get("token"):
|
160 |
+
logging.warning("Follower stats tab: Access denied. No token.")
|
161 |
+
return "❌ Access denied. No token available for follower stats.", None, None, None
|
162 |
+
|
163 |
+
follower_stats_df_orig = token_state.get("bubble_follower_stats_df", pd.DataFrame())
|
164 |
+
if follower_stats_df_orig.empty:
|
165 |
+
logging.info("Follower stats tab: No follower stats data in Bubble.")
|
166 |
+
return "<p style='text-align:center;'>No follower stats data in Bubble. Try syncing.</p>", None, None, None
|
167 |
+
|
168 |
+
follower_stats_df = follower_stats_df_orig.copy()
|
169 |
+
html_parts = ["<div style='padding:10px;'><h3 style='text-align:center;'>Follower Statistics Overview</h3>"]
|
170 |
+
|
171 |
+
plot_monthly_gains = None
|
172 |
+
plot_seniority_dist = None
|
173 |
+
plot_industry_dist = None
|
174 |
+
|
175 |
+
# --- Monthly Gains Table & Plot ---
|
176 |
+
monthly_gains_df = follower_stats_df[
|
177 |
+
(follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_gains_monthly') &
|
178 |
+
(follower_stats_df[FOLLOWER_STATS_CATEGORY_COLUMN].notna()) &
|
179 |
+
(follower_stats_df[FOLLOWER_STATS_ORGANIC_COLUMN].notna()) &
|
180 |
+
(follower_stats_df[FOLLOWER_STATS_PAID_COLUMN].notna())
|
181 |
+
].copy()
|
182 |
+
|
183 |
+
if not monthly_gains_df.empty:
|
184 |
+
try:
|
185 |
+
monthly_gains_df.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN_DT] = pd.to_datetime(monthly_gains_df[FOLLOWER_STATS_CATEGORY_COLUMN], errors='coerce')
|
186 |
+
monthly_gains_df_sorted_table = monthly_gains_df.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=False)
|
187 |
+
|
188 |
+
html_parts.append("<h4>Monthly Follower Gains (Last 13 Months):</h4>")
|
189 |
+
table_display_df = monthly_gains_df_sorted_table.copy()
|
190 |
+
table_display_df.loc[:,FOLLOWER_STATS_CATEGORY_COLUMN] = table_display_df[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT) # Use YYYY-MM for table
|
191 |
+
html_parts.append(table_display_df[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(13).to_html(escape=True, index=False, classes="table table-sm"))
|
192 |
+
|
193 |
+
monthly_gains_df_sorted_plot = monthly_gains_df.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=True).copy()
|
194 |
+
# For plotting, group by month string to ensure unique x-ticks if multiple entries exist for a month (though unlikely for this data type)
|
195 |
+
monthly_gains_df_sorted_plot.loc[:, '_plot_month'] = monthly_gains_df_sorted_plot[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT)
|
196 |
+
plot_data = monthly_gains_df_sorted_plot.groupby('_plot_month').agg(
|
197 |
+
organic=(FOLLOWER_STATS_ORGANIC_COLUMN, 'sum'),
|
198 |
+
paid=(FOLLOWER_STATS_PAID_COLUMN, 'sum')
|
199 |
+
).reset_index().sort_values(by='_plot_month')
|
200 |
+
|
201 |
+
|
202 |
+
fig_gains, ax_gains = plt.subplots(figsize=(10,5))
|
203 |
+
ax_gains.plot(plot_data['_plot_month'], plot_data['organic'], marker='o', linestyle='-', label='Organic Gain')
|
204 |
+
ax_gains.plot(plot_data['_plot_month'], plot_data['paid'], marker='x', linestyle='--', label='Paid Gain')
|
205 |
+
ax_gains.set_title("Monthly Follower Gains Over Time")
|
206 |
+
ax_gains.set_ylabel("Follower Count")
|
207 |
+
ax_gains.set_xlabel("Month (YYYY-MM)")
|
208 |
+
plt.xticks(rotation=45, ha='right')
|
209 |
+
ax_gains.legend()
|
210 |
+
plt.grid(True, linestyle='--', alpha=0.7)
|
211 |
+
plt.tight_layout()
|
212 |
+
plot_monthly_gains = fig_gains
|
213 |
+
logging.info("Follower stats tab: Monthly gains plot generated.")
|
214 |
+
except Exception as e:
|
215 |
+
logging.error(f"Error processing or plotting monthly gains: {e}", exc_info=True)
|
216 |
+
html_parts.append("<p>Error displaying monthly follower gain data.</p>")
|
217 |
+
else:
|
218 |
+
html_parts.append("<p>No monthly follower gain data available or required columns missing.</p>")
|
219 |
+
html_parts.append("<hr/>")
|
220 |
+
|
221 |
+
# --- Seniority Table & Plot ---
|
222 |
+
seniority_df = follower_stats_df[
|
223 |
+
(follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_seniority') &
|
224 |
+
(follower_stats_df[FOLLOWER_STATS_CATEGORY_COLUMN].notna()) &
|
225 |
+
(follower_stats_df[FOLLOWER_STATS_ORGANIC_COLUMN].notna())
|
226 |
+
].copy()
|
227 |
+
if not seniority_df.empty:
|
228 |
+
try:
|
229 |
+
seniority_df_sorted = seniority_df.sort_values(by=FOLLOWER_STATS_ORGANIC_COLUMN, ascending=False)
|
230 |
+
html_parts.append("<h4>Followers by Seniority (Top 10 Organic):</h4>")
|
231 |
+
html_parts.append(seniority_df_sorted[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(10).to_html(escape=True, index=False, classes="table table-sm"))
|
232 |
+
|
233 |
+
fig_seniority, ax_seniority = plt.subplots(figsize=(8,5))
|
234 |
+
top_n_seniority = seniority_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN)
|
235 |
+
ax_seniority.bar(top_n_seniority[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_seniority[FOLLOWER_STATS_ORGANIC_COLUMN], color='skyblue')
|
236 |
+
ax_seniority.set_title("Follower Distribution by Seniority (Top 10 Organic)")
|
237 |
+
ax_seniority.set_ylabel("Organic Follower Count")
|
238 |
+
plt.xticks(rotation=45, ha='right')
|
239 |
+
plt.grid(axis='y', linestyle='--', alpha=0.7)
|
240 |
+
plt.tight_layout()
|
241 |
+
plot_seniority_dist = fig_seniority
|
242 |
+
logging.info("Follower stats tab: Seniority distribution plot generated.")
|
243 |
+
except Exception as e:
|
244 |
+
logging.error(f"Error processing or plotting seniority data: {e}", exc_info=True)
|
245 |
+
html_parts.append("<p>Error displaying follower seniority data.</p>")
|
246 |
+
else:
|
247 |
+
html_parts.append("<p>No follower seniority data available or required columns missing.</p>")
|
248 |
+
html_parts.append("<hr/>")
|
249 |
+
|
250 |
+
# --- Industry Table & Plot ---
|
251 |
+
industry_df = follower_stats_df[
|
252 |
+
(follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_industry') &
|
253 |
+
(follower_stats_df[FOLLOWER_STATS_CATEGORY_COLUMN].notna()) &
|
254 |
+
(follower_stats_df[FOLLOWER_STATS_ORGANIC_COLUMN].notna())
|
255 |
+
].copy()
|
256 |
+
if not industry_df.empty:
|
257 |
+
try:
|
258 |
+
industry_df_sorted = industry_df.sort_values(by=FOLLOWER_STATS_ORGANIC_COLUMN, ascending=False)
|
259 |
+
html_parts.append("<h4>Followers by Industry (Top 10 Organic):</h4>")
|
260 |
+
html_parts.append(industry_df_sorted[[FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, FOLLOWER_STATS_PAID_COLUMN]].head(10).to_html(escape=True, index=False, classes="table table-sm"))
|
261 |
+
|
262 |
+
fig_industry, ax_industry = plt.subplots(figsize=(8,5))
|
263 |
+
top_n_industry = industry_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN)
|
264 |
+
ax_industry.bar(top_n_industry[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_industry[FOLLOWER_STATS_ORGANIC_COLUMN], color='lightcoral')
|
265 |
+
ax_industry.set_title("Follower Distribution by Industry (Top 10 Organic)")
|
266 |
+
ax_industry.set_ylabel("Organic Follower Count")
|
267 |
+
plt.xticks(rotation=45, ha='right')
|
268 |
+
plt.grid(axis='y', linestyle='--', alpha=0.7)
|
269 |
+
plt.tight_layout()
|
270 |
+
plot_industry_dist = fig_industry
|
271 |
+
logging.info("Follower stats tab: Industry distribution plot generated.")
|
272 |
+
except Exception as e:
|
273 |
+
logging.error(f"Error processing or plotting industry data: {e}", exc_info=True)
|
274 |
+
html_parts.append("<p>Error displaying follower industry data.</p>")
|
275 |
+
else:
|
276 |
+
html_parts.append("<p>No follower industry data available or required columns missing.</p>")
|
277 |
+
|
278 |
+
html_parts.append("</div>")
|
279 |
+
follower_html_output = "\n".join(html_parts)
|
280 |
+
return follower_html_output, plot_monthly_gains, plot_seniority_dist, plot_industry_dist
|