Spaces:
Running
Running
# ui_generators.py | |
""" | |
Generates HTML content and Matplotlib plots for the Gradio UI tabs, | |
and UI components for the Analytics tab. | |
""" | |
import pandas as pd | |
import logging | |
import matplotlib.pyplot as plt | |
import matplotlib # To ensure backend is switched before any plt import from other modules if app structure changes | |
import gradio as gr # Added for UI components | |
# Switch backend for Matplotlib to Agg for Gradio compatibility | |
matplotlib.use('Agg') | |
# Assuming config.py contains all necessary constants | |
from config import ( | |
BUBBLE_POST_DATE_COLUMN_NAME, BUBBLE_MENTIONS_DATE_COLUMN_NAME, BUBBLE_MENTIONS_ID_COLUMN_NAME, | |
FOLLOWER_STATS_TYPE_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN, FOLLOWER_STATS_ORGANIC_COLUMN, | |
FOLLOWER_STATS_PAID_COLUMN, FOLLOWER_STATS_CATEGORY_COLUMN_DT, UI_DATE_FORMAT, UI_MONTH_FORMAT | |
) | |
# Configure logging for this module if not already configured at app level | |
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s') | |
# --- Constants for Button Icons/Text --- | |
# These are also defined/imported in app.py, ensure consistency | |
BOMB_ICON = "π£" | |
EXPLORE_ICON = "π§" | |
FORMULA_ICON = "Ζ" | |
ACTIVE_ICON = "β Close" # Ensure this matches app.py | |
def build_home_tab_ui(): | |
""" | |
Constructs the entire UI for the Home tab, including the header, | |
overview, feature cards, and navigation buttons. | |
Returns: | |
tuple: A tuple containing the Gradio Button components for | |
Graphs, Reports, and OKR Table, allowing app.py to | |
attach click handlers for tab navigation. | |
""" | |
with gr.Column(scale=1, elem_classes="home-page-container"): | |
# Main header with welcome message | |
gr.Markdown(""" | |
<div style="text-align: center; padding: 30px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; margin-bottom: 30px; box-shadow: 0 8px 25px rgba(0,0,0,0.15); color: white;"> | |
<h1 style="color: white; margin-bottom: 20px; font-size: 2.5em; text-shadow: 2px 2px 4px rgba(0,0,0,0.3);"> | |
π LinkedIn Employer Brand Analytics Dashboard | |
</h1> | |
<p style="font-size: 1.3em; line-height: 1.8; margin-bottom: 15px; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);"> | |
Transform your LinkedIn presence with data-driven insights and actionable strategies | |
</p> | |
<p style="font-size: 1.1em; opacity: 0.9; text-shadow: 1px 1px 2px rgba(0,0,0,0.3);"> | |
Measure, analyze, and enhance your employer brand to attract top talent | |
</p> | |
</div> | |
""") | |
# Overview section | |
gr.Markdown(""" | |
<div style="background-color: #f8f9fa; padding: 25px; border-radius: 12px; margin-bottom: 25px; border-left: 5px solid #007bff;"> | |
<h2 style="color: #2c3e50; margin-bottom: 15px; display: flex; align-items: center;"> | |
<span style="margin-right: 10px;">π</span> What This Dashboard Offers | |
</h2> | |
<p style="font-size: 1.1em; color: #495057; line-height: 1.7; margin-bottom: 15px;"> | |
Our comprehensive analytics platform helps you understand and optimize your LinkedIn employer brand performance through: | |
</p> | |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-top: 20px;"> | |
<div style="display: flex; align-items: center; padding: 10px;"> | |
<span style="font-size: 1.5em; margin-right: 12px;">π</span> | |
<span style="color: #495057;">Real-time data visualization and trend analysis</span> | |
</div> | |
<div style="display: flex; align-items: center; padding: 10px;"> | |
<span style="font-size: 1.5em; margin-right: 12px;">π</span> | |
<span style="color: #495057;">Automated quarterly and weekly performance reports</span> | |
</div> | |
<div style="display: flex; align-items: center; padding: 10px;"> | |
<span style="font-size: 1.5em; margin-right: 12px;">π―</span> | |
<span style="color: #495057;">AI-powered OKRs and actionable recommendations</span> | |
</div> | |
<div style="display: flex; align-items: center; padding: 10px;"> | |
<span style="font-size: 1.5em; margin-right: 12px;">π</span> | |
<span style="color: #495057;">Strategic insights to improve employer branding</span> | |
</div> | |
</div> | |
</div> | |
""") | |
# Main navigation cards | |
with gr.Row(equal_height=True): | |
with gr.Column(): | |
gr.Markdown(""" | |
<div style="background: linear-gradient(135deg, #4CAF50, #45a049); padding: 25px; border-radius: 15px; min-height: 220px; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 6px 20px rgba(76, 175, 80, 0.3); transition: transform 0.3s ease;"> | |
<div> | |
<h3 style="color: white; margin-bottom: 15px; font-size: 1.4em; display: flex; align-items: center;"> | |
<span style="font-size: 1.8em; margin-right: 12px;">π</span> Interactive Graphs | |
</h3> | |
<p style="color: rgba(255,255,255,0.95); line-height: 1.6; font-size: 1.05em; margin-bottom: 20px;"> | |
Explore dynamic visualizations of your LinkedIn performance metrics. Track post engagement, | |
follower growth, mentions sentiment, and identify trends over time with interactive charts | |
and filtering options. | |
</p> | |
</div> | |
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;"> | |
<div style="color: rgba(255,255,255,0.8); font-size: 0.9em;"> | |
β¨ Real-time analytics<br/> | |
π Multiple chart types<br/> | |
π Advanced filtering | |
</div> | |
<div style="background: rgba(255,255,255,0.2); padding: 8px; border-radius: 8px;"> | |
<span style="font-size: 2em;">π</span> | |
</div> | |
</div> | |
</div> | |
""") | |
btn_graphs = gr.Button("π Explore Graphs", variant="primary", size="lg", | |
elem_classes="nav-button", scale=1) | |
with gr.Column(): | |
gr.Markdown(""" | |
<div style="background: linear-gradient(135deg, #2196F3, #1976D2); padding: 25px; border-radius: 15px; min-height: 220px; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 6px 20px rgba(33, 150, 243, 0.3); transition: transform 0.3s ease;"> | |
<div> | |
<h3 style="color: white; margin-bottom: 15px; font-size: 1.4em; display: flex; align-items: center;"> | |
<span style="font-size: 1.8em; margin-right: 12px;">π</span> Analysis Reports | |
</h3> | |
<p style="color: rgba(255,255,255,0.95); line-height: 1.6; font-size: 1.05em; margin-bottom: 20px;"> | |
Access comprehensive quarterly and weekly reports powered by AI analysis. Get detailed | |
insights into your employer brand performance, competitor analysis, and market positioning | |
with automated report generation. | |
</p> | |
</div> | |
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;"> | |
<div style="color: rgba(255,255,255,0.8); font-size: 0.9em;"> | |
π Automated reports<br/> | |
π€ AI-powered insights<br/> | |
π Weekly & quarterly | |
</div> | |
<div style="background: rgba(255,255,255,0.2); padding: 8px; border-radius: 8px;"> | |
<span style="font-size: 2em;">π</span> | |
</div> | |
</div> | |
</div> | |
""") | |
btn_reports = gr.Button("π View Reports", variant="primary", size="lg", | |
elem_classes="nav-button", scale=1) | |
with gr.Row(equal_height=True): | |
with gr.Column(): | |
gr.Markdown(""" | |
<div style="background: linear-gradient(135deg, #FF9800, #F57C00); padding: 25px; border-radius: 15px; min-height: 220px; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 6px 20px rgba(255, 152, 0, 0.3); transition: transform 0.3s ease;"> | |
<div> | |
<h3 style="color: white; margin-bottom: 15px; font-size: 1.4em; display: flex; align-items: center;"> | |
<span style="font-size: 1.8em; margin-right: 12px;">π―</span> OKR Action Plan | |
</h3> | |
<p style="color: rgba(255,255,255,0.95); line-height: 1.6; font-size: 1.05em; margin-bottom: 20px;"> | |
Discover AI-generated Objectives and Key Results (OKRs) with concrete action items. | |
Transform data insights into measurable goals and strategic initiatives to enhance | |
your employer brand effectively. | |
</p> | |
</div> | |
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;"> | |
<div style="color: rgba(255,255,255,0.8); font-size: 0.9em;"> | |
π― Strategic objectives<br/> | |
β Actionable tasks<br/> | |
π Measurable outcomes | |
</div> | |
<div style="background: rgba(255,255,255,0.2); padding: 8px; border-radius: 8px;"> | |
<span style="font-size: 2em;">π―</span> | |
</div> | |
</div> | |
</div> | |
""") | |
btn_okr = gr.Button("π― Access OKRs", variant="primary", size="lg", | |
elem_classes="nav-button", scale=1) | |
# Quick stats or tips column | |
with gr.Column(): | |
gr.Markdown(""" | |
<div style="background: linear-gradient(135deg, #9C27B0, #7B1FA2); padding: 25px; border-radius: 15px; min-height: 220px; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 6px 20px rgba(156, 39, 176, 0.3);"> | |
<div> | |
<h3 style="color: white; margin-bottom: 15px; font-size: 1.4em; display: flex; align-items: center;"> | |
<span style="font-size: 1.8em; margin-right: 12px;">π‘</span> Getting Started | |
</h3> | |
<p style="color: rgba(255,255,255,0.95); line-height: 1.6; font-size: 1.05em;"> | |
New to employer brand analytics? Start with the <strong>Graphs</strong> section to | |
understand your current performance, then check <strong>Reports</strong> for detailed | |
analysis, and finally explore <strong>OKRs</strong> for actionable next steps. | |
</p> | |
</div> | |
<div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.1); border-radius: 8px;"> | |
<div style="color: rgba(255,255,255,0.9); font-size: 0.95em; text-align: center;"> | |
<strong>πͺ Pro Tip:</strong><br/> | |
<span style="font-size: 0.9em;">Regular monitoring leads to 40% better employer brand performance</span> | |
</div> | |
</div> | |
</div> | |
""") | |
# Optional: Add a help or documentation button | |
btn_help = gr.Button("π Documentation", variant="secondary", size="lg", | |
elem_classes="nav-button", scale=1) | |
# Additional information section | |
gr.Markdown(""" | |
<div style="background-color: #e8f4fd; padding: 20px; border-radius: 12px; margin-top: 25px; border: 1px solid #b8daff;"> | |
<h3 style="color: #004085; margin-bottom: 15px; display: flex; align-items: center;"> | |
<span style="margin-right: 10px;">βΉοΈ</span> How It Works | |
</h3> | |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px;"> | |
<div style="display: flex; align-items: start;"> | |
<div style="background: #007bff; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin-right: 15px; flex-shrink: 0; font-weight: bold;">1</div> | |
<div> | |
<strong style="color: #004085;">Data Collection</strong><br/> | |
<span style="color: #495057; font-size: 0.95em;">Automatically syncs with your LinkedIn organization data</span> | |
</div> | |
</div> | |
<div style="display: flex; align-items: start;"> | |
<div style="background: #007bff; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin-right: 15px; flex-shrink: 0; font-weight: bold;">2</div> | |
<div> | |
<strong style="color: #004085;">AI Analysis</strong><br/> | |
<span style="color: #495057; font-size: 0.95em;">Advanced algorithms analyze trends and generate insights</span> | |
</div> | |
</div> | |
<div style="display: flex; align-items: start;"> | |
<div style="background: #007bff; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin-right: 15px; flex-shrink: 0; font-weight: bold;">3</div> | |
<div> | |
<strong style="color: #004085;">Actionable Results</strong><br/> | |
<span style="color: #495057; font-size: 0.95em;">Receive specific recommendations and measurable goals</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
""") | |
return btn_graphs, btn_reports, btn_okr, btn_help # Return all buttons for click handlers | |
def run_mentions_tab_display(token_state): | |
"""Generates HTML and a plot for the Mentions tab.""" | |
logging.info("Updating Mentions Tab display.") | |
if not token_state or not token_state.get("token"): | |
logging.warning("Mentions tab: Access denied. No token.") | |
return "β Access denied. No token available for mentions.", None | |
mentions_df = token_state.get("bubble_mentions_df", pd.DataFrame()) | |
if mentions_df.empty: | |
logging.info("Mentions tab: No mentions data in Bubble.") | |
return "<p style='text-align:center;'>No mentions data in Bubble. Try syncing.</p>", None | |
html_parts = ["<h3 style='text-align:center;'>Recent Mentions</h3>"] | |
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] | |
mentions_df_display = mentions_df.copy() | |
if BUBBLE_MENTIONS_DATE_COLUMN_NAME in mentions_df_display.columns: | |
try: | |
mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = pd.to_datetime(mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME], errors='coerce') | |
mentions_df_display = mentions_df_display.sort_values(by=BUBBLE_MENTIONS_DATE_COLUMN_NAME, ascending=False) | |
mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME] = mentions_df_display[BUBBLE_MENTIONS_DATE_COLUMN_NAME].dt.strftime(UI_DATE_FORMAT) | |
except Exception as e: | |
logging.error(f"Error formatting mention dates for tab display: {e}") | |
html_parts.append("<p>Error formatting mention dates.</p>") | |
if not display_columns or mentions_df_display[display_columns].empty: | |
html_parts.append("<p>Required columns for mentions display are missing or no data after processing.</p>") | |
else: | |
html_parts.append(mentions_df_display[display_columns].head(20).to_html(escape=False, index=False, classes="table table-sm")) | |
mentions_html_output = "\n".join(html_parts) | |
fig = None | |
fig_plot_local = None | |
if not mentions_df.empty and "sentiment_label" in mentions_df.columns: | |
try: | |
fig_plot_local, ax = plt.subplots(figsize=(6,4)) # Keep figsize for aspect ratio | |
sentiment_counts = mentions_df["sentiment_label"].value_counts() | |
sentiment_counts.plot(kind='bar', ax=ax, color=['#4CAF50', '#FFC107', '#F44336', '#9E9E9E', '#2196F3']) | |
ax.set_title("Mention Sentiment Distribution", y=1.03) | |
ax.set_ylabel("Count") | |
plt.xticks(rotation=45, ha='right') | |
plt.tight_layout() | |
fig_plot_local.subplots_adjust(top=0.90) | |
fig = fig_plot_local | |
logging.info("Mentions tab: Sentiment distribution plot generated.") | |
except Exception as e: | |
logging.error(f"Error generating mentions plot: {e}", exc_info=True) | |
fig = None | |
finally: | |
# Ensure plt.close is called on the figure object, not plt itself if it's not the same | |
if fig_plot_local and fig_plot_local is not plt: # Check if fig_plot_local is a Figure object | |
plt.close(fig_plot_local) | |
return mentions_html_output, fig | |
def run_follower_stats_tab_display(token_state): | |
"""Generates HTML and plots for the Follower Stats tab.""" | |
logging.info("Updating Follower Stats Tab display.") | |
if not token_state or not token_state.get("token"): | |
logging.warning("Follower stats tab: Access denied. No token.") | |
return "β Access denied. No token available for follower stats.", None, None, None | |
follower_stats_df_orig = token_state.get("bubble_follower_stats_df", pd.DataFrame()) | |
if follower_stats_df_orig.empty: | |
logging.info("Follower stats tab: No follower stats data in Bubble.") | |
return "<p style='text-align:center;'>No follower stats data in Bubble. Try syncing.</p>", None, None, None | |
follower_stats_df = follower_stats_df_orig.copy() | |
html_parts = ["<div style='padding:10px;'><h3 style='text-align:center;'>Follower Statistics Overview</h3>"] | |
plot_monthly_gains = None | |
plot_seniority_dist = None | |
plot_industry_dist = None | |
# Monthly Gains Plot | |
fig_gains_local = None | |
try: | |
monthly_gains_df = follower_stats_df[ | |
(follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_gains_monthly') & | |
(follower_stats_df[FOLLOWER_STATS_CATEGORY_COLUMN].notna()) & | |
(follower_stats_df[FOLLOWER_STATS_ORGANIC_COLUMN].notna()) & | |
(follower_stats_df[FOLLOWER_STATS_PAID_COLUMN].notna()) | |
].copy() | |
if not monthly_gains_df.empty: | |
monthly_gains_df.loc[:, FOLLOWER_STATS_CATEGORY_COLUMN_DT] = pd.to_datetime(monthly_gains_df[FOLLOWER_STATS_CATEGORY_COLUMN], errors='coerce') | |
monthly_gains_df_sorted_table = monthly_gains_df.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=False) | |
html_parts.append("<h4>Monthly Follower Gains (Last 13 Months):</h4>") | |
table_display_df = monthly_gains_df_sorted_table.copy() | |
table_display_df.loc[:,FOLLOWER_STATS_CATEGORY_COLUMN] = table_display_df[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT) | |
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")) | |
monthly_gains_df_sorted_plot = monthly_gains_df.sort_values(by=FOLLOWER_STATS_CATEGORY_COLUMN_DT, ascending=True).copy() | |
monthly_gains_df_sorted_plot.loc[:, '_plot_month'] = monthly_gains_df_sorted_plot[FOLLOWER_STATS_CATEGORY_COLUMN_DT].dt.strftime(UI_MONTH_FORMAT) | |
plot_data = monthly_gains_df_sorted_plot.groupby('_plot_month').agg( | |
organic=(FOLLOWER_STATS_ORGANIC_COLUMN, 'sum'), | |
paid=(FOLLOWER_STATS_PAID_COLUMN, 'sum') | |
).reset_index() | |
plot_data['_plot_month_dt'] = pd.to_datetime(plot_data['_plot_month'], format=UI_MONTH_FORMAT) # Ensure correct month format | |
plot_data = plot_data.sort_values(by='_plot_month_dt') | |
fig_gains_local, ax_gains = plt.subplots(figsize=(10,5)) # Keep figsize for aspect ratio | |
ax_gains.plot(plot_data['_plot_month'], plot_data['organic'], marker='o', linestyle='-', label='Organic Gain') | |
ax_gains.plot(plot_data['_plot_month'], plot_data['paid'], marker='x', linestyle='--', label='Paid Gain') | |
ax_gains.set_title("Monthly Follower Gains Over Time", y=1.03) | |
ax_gains.set_ylabel("Follower Count") | |
ax_gains.set_xlabel("Month (YYYY-MM)") | |
plt.xticks(rotation=45, ha='right') | |
ax_gains.legend() | |
plt.grid(True, linestyle='--', alpha=0.7) | |
plt.tight_layout() | |
fig_gains_local.subplots_adjust(top=0.90) | |
plot_monthly_gains = fig_gains_local | |
logging.info("Follower stats tab: Monthly gains plot generated.") | |
else: | |
html_parts.append("<p>No monthly follower gain data available or required columns missing.</p>") | |
except Exception as e: | |
logging.error(f"Error processing or plotting monthly gains: {e}", exc_info=True) | |
html_parts.append("<p>Error displaying monthly follower gain data.</p>") | |
plot_monthly_gains = None | |
finally: | |
if fig_gains_local and fig_gains_local is not plt: | |
plt.close(fig_gains_local) | |
html_parts.append("<hr/>") | |
# Seniority Plot | |
fig_seniority_local = None | |
try: | |
seniority_df = follower_stats_df[ | |
(follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_seniority') & | |
(follower_stats_df[FOLLOWER_STATS_CATEGORY_COLUMN].notna()) & | |
(follower_stats_df[FOLLOWER_STATS_ORGANIC_COLUMN].notna()) | |
].copy() | |
if not seniority_df.empty: | |
seniority_df_sorted = seniority_df.sort_values(by=FOLLOWER_STATS_ORGANIC_COLUMN, ascending=False) | |
html_parts.append("<h4>Followers by Seniority (Top 10 Organic):</h4>") | |
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")) | |
fig_seniority_local, ax_seniority = plt.subplots(figsize=(8,5)) # Keep figsize for aspect ratio | |
top_n_seniority = seniority_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN) | |
ax_seniority.bar(top_n_seniority[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_seniority[FOLLOWER_STATS_ORGANIC_COLUMN], color='skyblue') | |
ax_seniority.set_title("Follower Distribution by Seniority (Top 10 Organic)", y=1.03) | |
ax_seniority.set_ylabel("Organic Follower Count") | |
plt.xticks(rotation=45, ha='right') | |
plt.grid(axis='y', linestyle='--', alpha=0.7) | |
plt.tight_layout() | |
fig_seniority_local.subplots_adjust(top=0.88) | |
plot_seniority_dist = fig_seniority_local | |
logging.info("Follower stats tab: Seniority distribution plot generated.") | |
else: | |
html_parts.append("<p>No follower seniority data available or required columns missing.</p>") | |
except Exception as e: | |
logging.error(f"Error processing or plotting seniority data: {e}", exc_info=True) | |
html_parts.append("<p>Error displaying follower seniority data.</p>") | |
plot_seniority_dist = None | |
finally: | |
if fig_seniority_local and fig_seniority_local is not plt: | |
plt.close(fig_seniority_local) | |
html_parts.append("<hr/>") | |
# Industry Plot | |
fig_industry_local = None | |
try: | |
industry_df = follower_stats_df[ | |
(follower_stats_df[FOLLOWER_STATS_TYPE_COLUMN] == 'follower_industry') & | |
(follower_stats_df[FOLLOWER_STATS_CATEGORY_COLUMN].notna()) & | |
(follower_stats_df[FOLLOWER_STATS_ORGANIC_COLUMN].notna()) | |
].copy() | |
if not industry_df.empty: | |
industry_df_sorted = industry_df.sort_values(by=FOLLOWER_STATS_ORGANIC_COLUMN, ascending=False) | |
html_parts.append("<h4>Followers by Industry (Top 10 Organic):</h4>") | |
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")) | |
fig_industry_local, ax_industry = plt.subplots(figsize=(8,5)) # Keep figsize for aspect ratio | |
top_n_industry = industry_df_sorted.nlargest(10, FOLLOWER_STATS_ORGANIC_COLUMN) | |
ax_industry.bar(top_n_industry[FOLLOWER_STATS_CATEGORY_COLUMN], top_n_industry[FOLLOWER_STATS_ORGANIC_COLUMN], color='lightcoral') | |
ax_industry.set_title("Follower Distribution by Industry (Top 10 Organic)", y=1.03) | |
ax_industry.set_ylabel("Organic Follower Count") | |
plt.xticks(rotation=45, ha='right') | |
plt.grid(axis='y', linestyle='--', alpha=0.7) | |
plt.tight_layout() | |
fig_industry_local.subplots_adjust(top=0.88) | |
plot_industry_dist = fig_industry_local | |
logging.info("Follower stats tab: Industry distribution plot generated.") | |
else: | |
html_parts.append("<p>No follower industry data available or required columns missing.</p>") | |
except Exception as e: | |
logging.error(f"Error processing or plotting industry data: {e}", exc_info=True) | |
html_parts.append("<p>Error displaying follower industry data.</p>") | |
plot_industry_dist = None | |
finally: | |
if fig_industry_local and fig_industry_local is not plt: | |
plt.close(fig_industry_local) | |
html_parts.append("</div>") | |
follower_html_output = "\n".join(html_parts) | |
return follower_html_output, plot_monthly_gains, plot_seniority_dist, plot_industry_dist | |
def create_analytics_plot_panel(plot_label_str, plot_id_str): | |
""" | |
Creates an individual plot panel with its plot component and action buttons. | |
Plot title and action buttons are on the same row. | |
Returns the panel (Column), plot component, and button components. | |
""" | |
# Icons are defined globally or imported. For this function, ensure they are accessible. | |
# If not using from config directly here, you might need to pass them or use fixed strings. | |
# Using fixed strings as a fallback if import fails, though they should be available via app.py's import. | |
local_bomb_icon, local_explore_icon, local_formula_icon = BOMB_ICON, EXPLORE_ICON, FORMULA_ICON | |
with gr.Column(visible=True) as panel_component: # Main container for this plot | |
with gr.Row(variant="compact"): | |
gr.Markdown(f"#### {plot_label_str}") # Plot title (scale might help balance) | |
with gr.Row(elem_classes="plot-actions", scale=1): # Action buttons container, give it some min_width | |
bomb_button = gr.Button(value=local_bomb_icon, variant="secondary", size="sm", min_width=30, elem_id=f"bomb_btn_{plot_id_str}") | |
formula_button = gr.Button(value=local_formula_icon, variant="secondary", size="sm", min_width=30, elem_id=f"formula_btn_{plot_id_str}") | |
explore_button = gr.Button(value=local_explore_icon, variant="secondary", size="sm", min_width=30, elem_id=f"explore_btn_{plot_id_str}") | |
# MODIFIED: Added height to gr.Plot for consistent sizing | |
plot_component = gr.Plot(label=plot_label_str, show_label=False) # Adjust height as needed | |
logging.debug(f"Created analytics panel for: {plot_label_str} (ID: {plot_id_str}) with fixed plot height.") | |
return panel_component, plot_component, bomb_button, explore_button, formula_button | |
def build_analytics_tab_plot_area(plot_configs): | |
""" | |
Builds the main plot area for the Analytics tab, arranging plot panels into rows of two, | |
with section titles appearing before their respective plots. | |
Returns a tuple: | |
- plot_ui_objects (dict): Dictionary of plot UI objects. | |
- section_titles_map (dict): Dictionary mapping section names to their gr.Markdown title components. | |
""" | |
logging.info(f"Building plot area for {len(plot_configs)} analytics plots with interleaved section titles.") | |
plot_ui_objects = {} | |
section_titles_map = {} | |
last_rendered_section = None | |
idx = 0 | |
while idx < len(plot_configs): | |
current_plot_config = plot_configs[idx] | |
current_section_name = current_plot_config["section"] | |
# Render section title if it's new for this block of plots | |
if current_section_name != last_rendered_section: | |
if current_section_name not in section_titles_map: | |
# Create the Markdown component for the section title | |
section_md_component = gr.Markdown(f"## {current_section_name}", visible=True) | |
section_titles_map[current_section_name] = section_md_component | |
logging.debug(f"Rendered and stored Markdown for section: {current_section_name}") | |
# No 'else' needed here for visibility, as it's handled by click handlers if sections are hidden/shown. | |
# The component is created once and its visibility is controlled elsewhere. | |
last_rendered_section = current_section_name | |
with gr.Row(equal_height=True): # Row for one or two plots. equal_height=False allows plots to define their height. | |
# --- Process the first plot in the row (config1) --- | |
config1 = plot_configs[idx] | |
# Safety check for section consistency (should always pass if configs are ordered by section) | |
if config1["section"] != current_section_name: | |
logging.warning(f"Plot {config1['id']} section mismatch. Expected {current_section_name}, got {config1['section']}. This might affect layout if a new section title was expected.") | |
# If a new section starts unexpectedly, ensure its title is created if missing | |
if config1["section"] not in section_titles_map: | |
sec_md = gr.Markdown(f"### {config1['section']}", visible=True) # Create and make visible | |
section_titles_map[config1['section']] = sec_md | |
last_rendered_section = config1["section"] # Update the current section context | |
panel_col1, plot_comp1, bomb_btn1, explore_btn1, formula_btn1 = \ | |
create_analytics_plot_panel(config1["label"], config1["id"]) | |
plot_ui_objects[config1["id"]] = { | |
"plot_component": plot_comp1, "bomb_button": bomb_btn1, | |
"explore_button": explore_btn1, "formula_button": formula_btn1, | |
"label": config1["label"], "panel_component": panel_col1, # This is the gr.Column containing the plot and its actions | |
"section": config1["section"] | |
} | |
logging.debug(f"Created UI panel for plot_id: {config1['id']} in section {config1['section']}") | |
idx += 1 | |
# --- Process the second plot in the row (config2), if applicable --- | |
if idx < len(plot_configs): | |
config2 = plot_configs[idx] | |
# Only add to the same row if it's part of the same section | |
if config2["section"] == current_section_name: | |
panel_col2, plot_comp2, bomb_btn2, explore_btn2, formula_btn2 = \ | |
create_analytics_plot_panel(config2["label"], config2["id"]) | |
plot_ui_objects[config2["id"]] = { | |
"plot_component": plot_comp2, "bomb_button": bomb_btn2, | |
"explore_button": explore_btn2, "formula_button": formula_btn2, | |
"label": config2["label"], "panel_component": panel_col2, | |
"section": config2["section"] | |
} | |
logging.debug(f"Created UI panel for plot_id: {config2['id']} in same row, section {config2['section']}") | |
idx += 1 | |
# If the next plot is in a new section, it will be handled in the next iteration of the while loop, | |
# starting with a new section title and a new gr.Row. | |
logging.info(f"Finished building plot area. Total plot objects: {len(plot_ui_objects)}. Section titles created: {len(section_titles_map)}") | |
if len(plot_ui_objects) != len(plot_configs): | |
logging.error(f"MISMATCH: Expected {len(plot_configs)} plot objects, but created {len(plot_ui_objects)}.") | |
return plot_ui_objects, section_titles_map | |