LinkedinMonitor / ui /analytics_plot_generator.py
GuglielmoTor's picture
Update ui/analytics_plot_generator.py
95677de verified
import pandas as pd
import matplotlib.pyplot as plt
import logging
from io import BytesIO
import base64
import numpy as np
import matplotlib.ticker as mticker
import matplotlib.patches as patches # Added for rounded corners
import ast # For safely evaluating string representations of lists
from data_processing.analytics_data_processing import (
generate_chatbot_data_summaries,
prepare_filtered_analytics_data
)
# Configure logging for this module
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
# Helper function to clean non-printable characters from the entire file content if needed
# For now, I will manually ensure the code below is clean.
# If the error persists, you might need a script to clean the .py file itself.
def _apply_rounded_corners_and_transparent_bg(fig, ax):
"""Helper to apply rounded corners to axes and transparent background."""
fig.patch.set_alpha(0.0) # Make figure background transparent
ax.patch.set_alpha(0.0) # Make default axes background transparent
# Turn off original spines, as we'll draw a new background
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
# Add a new rounded background for the axes
# Using FancyBboxPatch to create a rounded rectangle background for the plot area
# Coordinates are relative to axes (0,0 is bottom-left, 1,1 is top-right)
rounded_rect_bg = patches.FancyBboxPatch(
(0, 0), # (x,y) position of the bounding box
1, # width of the bounding box
1, # height of the bounding box
boxstyle="round,pad=0,rounding_size=0.015", # Style: round, no padding, size of rounding
transform=ax.transAxes, # Coordinates are relative to the axes
facecolor='whitesmoke', # Background color of the rounded area
edgecolor='lightgray', # Border color for the rounded area
linewidth=0.5, # Border line width
zorder=-1 # Put it behind other plot elements like gridlines and data
)
ax.add_patch(rounded_rect_bg)
# Ensure grid is drawn on top of the new background if used
if ax.axison and any(line.get_visible() for line in ax.get_xgridlines() + ax.get_ygridlines()):
ax.grid(True, linestyle='--', alpha=0.6, zorder=0) # Redraw grid with zorder
def create_placeholder_plot(title="No Data or Plot Error", message="Data might be empty or an error occurred."):
"""Creates a placeholder Matplotlib plot indicating no data or an error."""
try:
fig, ax = plt.subplots(figsize=(8, 4))
_apply_rounded_corners_and_transparent_bg(fig, ax) # Apply rounded corners and transparent BG
ax.text(0.5, 0.5, f"{title}\n{message}", ha='center', va='center', fontsize=10, wrap=True, zorder=1)
ax.axis('off') # Turn off axis for placeholder text display
# No tight_layout here as it might interfere with the manual patch for background
fig.subplots_adjust(top=0.90, bottom=0.10, left=0.10, right=0.90) # General padding
return fig
except Exception as e:
logging.error(f"Error creating placeholder plot: {e}")
# Fallback placeholder if the above fails (less styling)
fig_err, ax_err = plt.subplots(figsize=(8,4))
fig_err.patch.set_alpha(0.0)
ax_err.patch.set_alpha(0.0)
ax_err.text(0.5, 0.5, "Fatal: Plot generation error", ha='center', va='center', zorder=1)
ax_err.axis('off')
return fig_err
def generate_posts_activity_plot(df, date_column='published_at'):
"""Generates a plot for posts activity over time."""
logging.info(f"Generating posts activity plot. Date column: '{date_column}'. Input df rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
logging.warning(f"Posts activity: DataFrame is empty.")
return create_placeholder_plot(title="Posts Activity Over Time", message="No data available for the selected period.")
if date_column not in df.columns:
logging.warning(f"Posts activity: Date column '{date_column}' is missing. Cols: {df.columns.tolist()}.")
return create_placeholder_plot(title="Posts Activity Over Time", message=f"Date column '{date_column}' not found.")
fig = None
try:
df_copy = df.copy()
if not pd.api.types.is_datetime64_any_dtype(df_copy[date_column]):
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column])
if df_copy.empty:
logging.info("Posts activity: DataFrame empty after NaNs dropped from date column.")
return create_placeholder_plot(title="Posts Activity Over Time", message="No valid date entries found.")
posts_over_time = df_copy.set_index(date_column).resample('D').size()
if posts_over_time.empty:
logging.info("Posts activity: No posts after resampling by day.")
return create_placeholder_plot(title="Posts Activity Over Time", message="No posts in the selected period.")
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
posts_over_time.plot(kind='line', ax=ax, marker='o', linestyle='-', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Number of Posts')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0) # Ensure grid is behind plot line
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5) # Add some padding
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95) # Adjusted spacing
logging.info("Successfully generated posts activity plot.")
return fig
except Exception as e:
logging.error(f"Error generating posts activity plot: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title="Posts Activity Error", message=str(e))
def generate_mentions_activity_plot(df, date_column='date'):
"""Generates a plot for mentions activity over time."""
logging.info(f"Generating mentions activity plot. Date column: '{date_column}'. Input df rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
logging.warning(f"Mentions activity: DataFrame is empty.")
return create_placeholder_plot(title="Mentions Activity Over Time", message="No data available for the selected period.")
if date_column not in df.columns:
logging.warning(f"Mentions activity: Date column '{date_column}' is missing. Cols: {df.columns.tolist()}.")
return create_placeholder_plot(title="Mentions Activity Over Time", message=f"Date column '{date_column}' not found.")
fig = None
try:
df_copy = df.copy()
if not pd.api.types.is_datetime64_any_dtype(df_copy[date_column]):
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column])
if df_copy.empty:
logging.info("Mentions activity: DataFrame empty after NaNs dropped from date column.")
return create_placeholder_plot(title="Mentions Activity Over Time", message="No valid date entries found.")
mentions_over_time = df_copy.set_index(date_column).resample('D').size()
if mentions_over_time.empty:
logging.info("Mentions activity: No mentions after resampling by day.")
return create_placeholder_plot(title="Mentions Activity Over Time", message="No mentions in the selected period.")
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
mentions_over_time.plot(kind='line', ax=ax, marker='o', linestyle='-', color='purple', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Number of Mentions')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95) # Adjusted spacing
logging.info("Successfully generated mentions activity plot.")
return fig
except Exception as e:
logging.error(f"Error generating mentions activity plot: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title="Mentions Activity Error", message=str(e))
def generate_mention_sentiment_plot(df, sentiment_column='sentiment_label'):
"""Generates a pie chart for mention sentiment distribution."""
logging.info(f"Generating mention sentiment plot. Sentiment column: '{sentiment_column}'. Input df rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
logging.warning("Mention sentiment: DataFrame is empty.")
return create_placeholder_plot(title="Mention Sentiment Distribution", message="No data available for the selected period.")
if sentiment_column not in df.columns:
msg = f"Mention sentiment: Column '{sentiment_column}' is missing. Available: {df.columns.tolist()}"
logging.warning(msg)
return create_placeholder_plot(title="Mention Sentiment Distribution", message=msg)
fig = None
try:
df_copy = df.copy()
sentiment_counts = df_copy[sentiment_column].value_counts()
if sentiment_counts.empty:
logging.info("Mention sentiment: No sentiment data after value_counts.")
return create_placeholder_plot(title="Mention Sentiment Distribution", message="No sentiment data available.")
fig, ax = plt.subplots(figsize=(8, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax) # Apply before plotting pie
# Define a list of distinct colors for the pie slices
pie_slice_colors = plt.cm.get_cmap('Pastel2', len(sentiment_counts))
# Removed zorder from ax.pie
wedges, texts, autotexts = ax.pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%', startangle=90,
colors=[pie_slice_colors(i) for i in range(len(sentiment_counts))])
# Set zorder for pie elements if needed, though usually not necessary as they draw on top of the background patch
for wedge in wedges:
wedge.set_zorder(1)
for text_item in texts + autotexts:
text_item.set_zorder(2)
ax.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
# fig.tight_layout(pad=0.5) # tight_layout can sometimes mess with pie charts if labels are long
fig.subplots_adjust(top=0.95, bottom=0.05, left=0.05, right=0.95) # Give pie chart space
logging.info("Successfully generated mention sentiment plot.")
return fig
except Exception as e:
logging.error(f"Error generating mention sentiment plot: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title="Mention Sentiment Error", message=str(e))
def generate_followers_count_over_time_plot(df, date_info_column='category_name',
organic_count_col='follower_count_organic',
paid_count_col='follower_count_paid',
type_filter_column='follower_count_type',
type_value='follower_gains_monthly'):
title = f"Followers Count Over Time ({type_value})"
logging.info(f"Generating {title}. Date Info: '{date_info_column}', Organic: '{organic_count_col}', Paid: '{paid_count_col}', Type Filter: '{type_filter_column}=={type_value}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No follower data available.")
required_cols = [date_info_column, organic_count_col, paid_count_col, type_filter_column]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
return create_placeholder_plot(title=title, message=f"Missing columns: {missing_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_filtered = df_copy[df_copy[type_filter_column] == type_value].copy()
if df_filtered.empty:
return create_placeholder_plot(title=title, message=f"No data for type '{type_value}'.")
df_filtered['datetime_obj'] = pd.to_datetime(df_filtered[date_info_column], errors='coerce')
df_filtered[organic_count_col] = pd.to_numeric(df_filtered[organic_count_col], errors='coerce').fillna(0)
df_filtered[paid_count_col] = pd.to_numeric(df_filtered[paid_count_col], errors='coerce').fillna(0)
df_filtered = df_filtered.dropna(subset=['datetime_obj', organic_count_col, paid_count_col]).sort_values(by='datetime_obj')
if df_filtered.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning and filtering.")
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(df_filtered['datetime_obj'], df_filtered[organic_count_col], marker='o', linestyle='-', color='dodgerblue', label='Organic Followers', zorder=1)
ax.plot(df_filtered['datetime_obj'], df_filtered[paid_count_col], marker='x', linestyle='--', color='seagreen', label='Paid Followers', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Follower Count')
legend = ax.legend() # Removed zorder from legend call
if legend: legend.set_zorder(2) # Set zorder on the legend object itself
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_followers_growth_rate_plot(df, date_info_column='category_name',
organic_count_col='follower_count_organic',
paid_count_col='follower_count_paid',
type_filter_column='follower_count_type',
type_value='follower_gains_monthly'):
title = f"Follower Growth Rate ({type_value})"
logging.info(f"Generating {title}. Date Info: '{date_info_column}', Organic: '{organic_count_col}', Paid: '{paid_count_col}', Type Filter: '{type_filter_column}=={type_value}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No follower data available.")
required_cols = [date_info_column, organic_count_col, paid_count_col, type_filter_column]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
return create_placeholder_plot(title=title, message=f"Missing columns: {missing_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_filtered = df_copy[df_copy[type_filter_column] == type_value].copy()
if df_filtered.empty:
return create_placeholder_plot(title=title, message=f"No data for type '{type_value}'.")
df_filtered['datetime_obj'] = pd.to_datetime(df_filtered[date_info_column], errors='coerce')
df_filtered[organic_count_col] = pd.to_numeric(df_filtered[organic_count_col], errors='coerce')
df_filtered[paid_count_col] = pd.to_numeric(df_filtered[paid_count_col], errors='coerce')
df_filtered = df_filtered.dropna(subset=['datetime_obj']).sort_values(by='datetime_obj').set_index('datetime_obj')
if df_filtered.empty or len(df_filtered) < 2:
return create_placeholder_plot(title=title, message="Not enough data points to calculate growth rate.")
df_filtered['organic_growth_rate'] = df_filtered[organic_count_col].pct_change() * 100
df_filtered['paid_growth_rate'] = df_filtered[paid_count_col].pct_change() * 100
df_filtered.replace([np.inf, -np.inf], np.nan, inplace=True)
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
plotted_organic = False
if 'organic_growth_rate' in df_filtered.columns and not df_filtered['organic_growth_rate'].dropna().empty:
ax.plot(df_filtered.index, df_filtered['organic_growth_rate'], marker='o', linestyle='-', color='lightcoral', label='Organic Growth Rate', zorder=1)
plotted_organic = True
plotted_paid = False
if 'paid_growth_rate' in df_filtered.columns and not df_filtered['paid_growth_rate'].dropna().empty:
ax.plot(df_filtered.index, df_filtered['paid_growth_rate'], marker='x', linestyle='--', color='mediumpurple', label='Paid Growth Rate', zorder=1)
plotted_paid = True
if not plotted_organic and not plotted_paid:
return create_placeholder_plot(title=title, message="No valid growth rate data to display after calculation.")
ax.set_xlabel('Date')
ax.set_ylabel('Growth Rate (%)')
ax.yaxis.set_major_formatter(mticker.PercentFormatter())
legend = ax.legend() # Removed zorder from legend call
if legend: legend.set_zorder(2) # Set zorder on the legend object itself
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_followers_by_demographics_plot(df, category_col='category_name',
organic_count_col='follower_count_organic',
paid_count_col='follower_count_paid',
type_filter_column='follower_count_type',
type_value=None, plot_title="Followers by Demographics"):
logging.info(f"Generating {plot_title}. Category: '{category_col}', Organic: '{organic_count_col}', Paid: '{paid_count_col}', Type Filter: '{type_filter_column}=={type_value}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=plot_title, message="No follower data available.")
required_cols = [category_col, organic_count_col, paid_count_col, type_filter_column]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
return create_placeholder_plot(title=plot_title, message=f"Missing columns: {missing_cols}. Available: {df.columns.tolist()}")
if type_value is None:
return create_placeholder_plot(title=plot_title, message="Demographic type (type_value) not specified.")
fig = None
try:
df_copy = df.copy()
df_filtered = df_copy[df_copy[type_filter_column] == type_value].copy()
if df_filtered.empty:
return create_placeholder_plot(title=plot_title, message=f"No data for demographic type '{type_value}'.")
df_filtered[organic_count_col] = pd.to_numeric(df_filtered[organic_count_col], errors='coerce').fillna(0)
df_filtered[paid_count_col] = pd.to_numeric(df_filtered[paid_count_col], errors='coerce').fillna(0)
demographics_data = df_filtered.groupby(category_col)[[organic_count_col, paid_count_col]].sum()
demographics_data['total_for_sort'] = demographics_data[organic_count_col] + demographics_data[paid_count_col]
demographics_data = demographics_data.sort_values(by='total_for_sort', ascending=False).drop(columns=['total_for_sort'])
if demographics_data.empty:
return create_placeholder_plot(title=plot_title, message="No demographic data to display after filtering and aggregation.")
top_n = 10
if len(demographics_data) > top_n:
demographics_data = demographics_data.head(top_n)
fig, ax = plt.subplots(figsize=(12, 7) if len(demographics_data) > 5 else (10,6) )
_apply_rounded_corners_and_transparent_bg(fig, ax)
bar_width = 0.35
index = np.arange(len(demographics_data.index))
color_organic = plt.cm.get_cmap('tab10')(0)
color_paid = plt.cm.get_cmap('tab10')(1)
bars1 = ax.bar(index - bar_width/2, demographics_data[organic_count_col], bar_width, label='Organic', color=color_organic, zorder=1)
bars2 = ax.bar(index + bar_width/2, demographics_data[paid_count_col], bar_width, label='Paid', color=color_paid, zorder=1)
ax.set_xlabel(category_col.replace('_', ' ').title())
ax.set_ylabel('Number of Followers')
ax.set_xticks(index)
ax.set_xticklabels(demographics_data.index, rotation=45, ha="right")
legend = ax.legend() # Removed zorder from legend call
if legend: legend.set_zorder(2) # Set zorder on the legend object itself
ax.grid(axis='y', linestyle='--', alpha=0.6, zorder=0)
for bar_group in [bars1, bars2]:
for bar_item in bar_group:
yval = bar_item.get_height()
if yval > 0:
ax.text(bar_item.get_x() + bar_item.get_width()/2.0, yval + (0.01 * ax.get_ylim()[1]),
str(int(yval)), ha='center', va='bottom', fontsize=8, zorder=2)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.25, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {plot_title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{plot_title} Error", message=str(e))
def generate_engagement_rate_over_time_plot(df, date_column='published_at', engagement_rate_col='engagement'):
title = "Engagement Rate Over Time"
logging.info(f"Generating {title}. Date: '{date_column}', Rate Col: '{engagement_rate_col}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No post data for engagement rate.")
required_cols = [date_column, engagement_rate_col]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
return create_placeholder_plot(title=title, message=f"Missing columns: {missing_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy[engagement_rate_col] = pd.to_numeric(df_copy[engagement_rate_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column, engagement_rate_col]).set_index(date_column)
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning.")
engagement_over_time = df_copy.resample('D')[engagement_rate_col].mean()
engagement_over_time = engagement_over_time.dropna()
if engagement_over_time.empty:
return create_placeholder_plot(title=title, message="No engagement rate data to display after resampling.")
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(engagement_over_time.index, engagement_over_time.values, marker='.', linestyle='-', color='darkorange', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Engagement Rate')
max_rate_val = engagement_over_time.max() if not engagement_over_time.empty else 0
formatter_xmax = 1.0 if 0 <= max_rate_val <= 1.5 else 100.0
if max_rate_val > 1.5 and formatter_xmax == 1.0:
formatter_xmax = 100.0
elif max_rate_val > 100 and formatter_xmax == 1.0:
formatter_xmax = max_rate_val
ax.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=formatter_xmax))
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_reach_over_time_plot(df, date_column='published_at', reach_col='clickCount'):
title = "Reach Over Time (Clicks)"
logging.info(f"Generating {title}. Date: '{date_column}', Reach Col: '{reach_col}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No post data for reach.")
required_cols = [date_column, reach_col]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
return create_placeholder_plot(title=title, message=f"Missing columns: {missing_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy[reach_col] = pd.to_numeric(df_copy[reach_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column, reach_col]).set_index(date_column)
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning for reach plot.")
reach_over_time = df_copy.resample('D')[reach_col].sum()
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(reach_over_time.index, reach_over_time.values, marker='.', linestyle='-', color='mediumseagreen', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Total Clicks')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_impressions_over_time_plot(df, date_column='published_at', impressions_col='impressionCount'):
title = "Impressions Over Time"
logging.info(f"Generating {title}. Date: '{date_column}', Impressions Col: '{impressions_col}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No post data for impressions.")
required_cols = [date_column, impressions_col]
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
return create_placeholder_plot(title=title, message=f"Missing columns: {missing_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy[impressions_col] = pd.to_numeric(df_copy[impressions_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column, impressions_col]).set_index(date_column)
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning for impressions plot.")
impressions_over_time = df_copy.resample('D')[impressions_col].sum()
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(impressions_over_time.index, impressions_over_time.values, marker='.', linestyle='-', color='slateblue', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Total Impressions')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_likes_over_time_plot(df, date_column='published_at', likes_col='likeCount'):
title = "Reactions (Likes) Over Time"
logging.info(f"Generating {title}. Date: '{date_column}', Likes Col: '{likes_col}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No post data for likes.")
required_cols = [date_column, likes_col]
if any(col not in df.columns for col in required_cols):
return create_placeholder_plot(title=title, message=f"Missing one of required columns: {required_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy[likes_col] = pd.to_numeric(df_copy[likes_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column, likes_col]).set_index(date_column)
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning.")
data_over_time = df_copy.resample('D')[likes_col].sum()
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(data_over_time.index, data_over_time.values, marker='.', linestyle='-', color='crimson', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Total Likes')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_clicks_over_time_plot(df, date_column='published_at', clicks_col='clickCount'):
# This function reuses generate_reach_over_time_plot logic
return generate_reach_over_time_plot(df, date_column, clicks_col)
def generate_shares_over_time_plot(df, date_column='published_at', shares_col='shareCount'):
title = "Shares Over Time"
logging.info(f"Generating {title}. Date: '{date_column}', Shares Col: '{shares_col}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No post data for shares.")
required_cols = [date_column, shares_col]
if any(col not in df.columns for col in required_cols):
return create_placeholder_plot(title=title, message=f"Missing one of required columns: {required_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy[shares_col] = pd.to_numeric(df_copy[shares_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column, shares_col]).set_index(date_column)
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning.")
data_over_time = df_copy.resample('D')[shares_col].sum()
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(data_over_time.index, data_over_time.values, marker='.', linestyle='-', color='teal', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Total Shares')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_comments_over_time_plot(df, date_column='published_at', comments_col='commentCount'):
title = "Comments Over Time"
logging.info(f"Generating {title}. Date: '{date_column}', Comments Col: '{comments_col}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No post data for comments.")
required_cols = [date_column, comments_col]
if any(col not in df.columns for col in required_cols):
return create_placeholder_plot(title=title, message=f"Missing one of required columns: {required_cols}. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy[comments_col] = pd.to_numeric(df_copy[comments_col], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column, comments_col]).set_index(date_column)
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid data after cleaning.")
data_over_time = df_copy.resample('D')[comments_col].sum()
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
ax.plot(data_over_time.index, data_over_time.values, marker='.', linestyle='-', color='gold', zorder=1)
ax.set_xlabel('Date')
ax.set_ylabel('Total Comments')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_comments_sentiment_breakdown_plot(df, sentiment_column='comment_sentiment', date_column=None):
title = "Breakdown of Comments by Sentiment"
logging.info(f"Generating {title}. Sentiment Col: '{sentiment_column}'. DF rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No data for comment sentiment.")
if sentiment_column not in df.columns:
if 'sentiment' in df.columns and sentiment_column != 'sentiment': # Check for a common alternative name
logging.warning(f"Sentiment column '{sentiment_column}' not found, attempting to use 'sentiment' column as fallback for comment sentiment plot.")
sentiment_column = 'sentiment'
if sentiment_column not in df.columns: # If fallback also not found
return create_placeholder_plot(title=title, message=f"Fallback sentiment column 'sentiment' also not found. Available: {df.columns.tolist()}")
else: # If original and 'sentiment' fallback are not found
return create_placeholder_plot(title=title, message=f"Sentiment column '{sentiment_column}' not found. Available: {df.columns.tolist()}")
if df[sentiment_column].isnull().all():
return create_placeholder_plot(title=title, message=f"Sentiment column '{sentiment_column}' contains no valid data.")
fig = None
try:
df_copy = df.copy()
df_copy[sentiment_column] = df_copy[sentiment_column].astype(str)
sentiment_counts = df_copy[sentiment_column].value_counts().dropna()
if sentiment_counts.empty or sentiment_counts.sum() == 0:
return create_placeholder_plot(title=title, message="No comment sentiment data to display after processing.")
fig, ax = plt.subplots(figsize=(8, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
pie_slice_colors = plt.cm.get_cmap('coolwarm', len(sentiment_counts))
# Removed zorder from ax.pie
wedges, texts, autotexts = ax.pie(sentiment_counts, labels=sentiment_counts.index, autopct='%1.1f%%', startangle=90,
colors=[pie_slice_colors(i) for i in range(len(sentiment_counts))])
for wedge in wedges:
wedge.set_zorder(1)
for text_item in texts + autotexts:
text_item.set_zorder(2)
ax.axis('equal')
# fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.95, bottom=0.05, left=0.05, right=0.95)
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_post_frequency_plot(df, date_column='published_at', resample_period='D'):
title = f"Post Frequency Over Time ({resample_period})"
logging.info(f"Generating {title}. Date column: '{date_column}'. Input df rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No data available.")
if date_column not in df.columns:
return create_placeholder_plot(title=title, message=f"Date column '{date_column}' not found.")
fig = None
try:
df_copy = df.copy()
if not pd.api.types.is_datetime64_any_dtype(df_copy[date_column]):
df_copy[date_column] = pd.to_datetime(df_copy[date_column], errors='coerce')
df_copy = df_copy.dropna(subset=[date_column])
if df_copy.empty:
return create_placeholder_plot(title=title, message="No valid date entries found.")
post_frequency = df_copy.set_index(date_column).resample(resample_period).size()
if post_frequency.empty:
return create_placeholder_plot(title=title, message=f"No posts found for the period after resampling by '{resample_period}'.")
fig, ax = plt.subplots(figsize=(10, 5))
_apply_rounded_corners_and_transparent_bg(fig, ax)
if resample_period in ['M', 'W']:
num_bars = len(post_frequency)
bar_colors = plt.cm.get_cmap('viridis', num_bars) # Or 'tab10'
post_frequency.plot(kind='bar', ax=ax, color=[bar_colors(i) for i in range(num_bars)], zorder=1)
for i, v in enumerate(post_frequency):
ax.text(i, v + (0.01 * post_frequency.max()), str(v), ha='center', va='bottom', zorder=2)
else:
post_frequency.plot(kind='line', ax=ax, marker='o', zorder=1)
ax.set_xlabel('Date' if resample_period == 'D' else 'Period')
ax.set_ylabel('Number of Posts')
ax.grid(True, linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.1, right=0.95)
logging.info(f"Successfully generated {title} plot.")
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def generate_content_format_breakdown_plot(df, format_col='media_type'):
title = "Breakdown of Content by Format"
logging.info(f"Generating {title}. Format column: '{format_col}'. Input df rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No data available.")
if format_col not in df.columns:
return create_placeholder_plot(title=title, message=f"Format column '{format_col}' not found. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
format_counts = df_copy[format_col].value_counts().dropna()
if format_counts.empty:
return create_placeholder_plot(title=title, message="No content format data available.")
fig, ax = plt.subplots(figsize=(8, 6))
_apply_rounded_corners_and_transparent_bg(fig, ax)
num_bars = len(format_counts)
bar_colors = plt.cm.get_cmap('tab10', num_bars) # Using tab10 for distinct colors
format_counts.plot(kind='bar', ax=ax, color=[bar_colors(i) for i in range(num_bars)], zorder=1)
ax.set_xlabel('Media Type')
ax.set_ylabel('Number of Posts')
ax.grid(axis='y', linestyle='--', alpha=0.6, zorder=0)
plt.xticks(rotation=45, ha="right")
for i, v in enumerate(format_counts):
ax.text(i, v + (0.01 * format_counts.max()), str(v), ha='center', va='bottom', zorder=2)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.20, left=0.15, right=0.95)
logging.info(f"Successfully generated {title} plot.")
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
def _parse_eb_label(label_data):
if isinstance(label_data, list):
return label_data
if isinstance(label_data, str):
try:
parsed = ast.literal_eval(label_data)
if isinstance(parsed, list):
return parsed
return [str(parsed)] # Ensure it's a list even if ast.literal_eval returns a single string
except (ValueError, SyntaxError):
# If not a valid list string, treat the whole string as one label if not empty
return [label_data.strip()] if label_data and label_data.strip() else []
if pd.isna(label_data):
return []
return [str(label_data)] # Fallback for other types, ensuring it's a list
def generate_content_topic_breakdown_plot(df, topics_col='li_eb_labels', top_n=15):
title = f"Breakdown of Content by Topics (Top {top_n})"
logging.info(f"Generating {title}. Topics column: '{topics_col}'. Input df rows: {len(df) if df is not None else 'None'}")
if df is None or df.empty:
return create_placeholder_plot(title=title, message="No data available.")
if topics_col not in df.columns:
return create_placeholder_plot(title=title, message=f"Topics column '{topics_col}' not found. Available: {df.columns.tolist()}")
fig = None
try:
df_copy = df.copy()
# Ensure all entries in topics_col are processed by _parse_eb_label
parsed_labels = df_copy[topics_col].apply(_parse_eb_label)
exploded_labels = parsed_labels.explode().dropna() # Explode lists into separate rows
# Filter out any empty strings that might result from parsing
exploded_labels = exploded_labels[exploded_labels != '']
if exploded_labels.empty:
return create_placeholder_plot(title=title, message="No topic data found after processing labels.")
topic_counts = exploded_labels.value_counts()
if topic_counts.empty:
return create_placeholder_plot(title=title, message="No topics to display after counting.")
top_topics = topic_counts.nlargest(top_n).sort_values(ascending=True)
fig, ax = plt.subplots(figsize=(10, 8 if len(top_topics) > 5 else 6))
_apply_rounded_corners_and_transparent_bg(fig, ax)
num_bars = len(top_topics)
bar_colors = plt.cm.get_cmap('YlGnBu', num_bars + 3) # Using a sequential colormap for horizontal bars
top_topics.plot(kind='barh', ax=ax, color=[bar_colors(i+3) for i in range(num_bars)], zorder=1) # +3 to get darker shades
ax.set_xlabel('Number of Posts')
ax.set_ylabel('Topic')
for i, (topic, count) in enumerate(top_topics.items()): # Use .items() for Series
ax.text(count + (0.01 * top_topics.max()), i, str(count), va='center', zorder=2)
fig.tight_layout(pad=0.5)
fig.subplots_adjust(top=0.92, bottom=0.1, left=0.3, right=0.95) # Adjusted left for long topic labels
logging.info(f"Successfully generated {title} plot.")
return fig
except Exception as e:
logging.error(f"Error generating {title}: {e}", exc_info=True)
if fig: plt.close(fig)
return create_placeholder_plot(title=f"{title} Error", message=str(e))
# --- Analytics Tab: Plot Figure Generation Function ---
def update_analytics_plots_figures(token_state_value, date_filter_option, custom_start_date, custom_end_date, current_plot_configs):
logging.info(f"Updating analytics plot figures. Filter: {date_filter_option}, Custom Start: {custom_start_date}, Custom End: {custom_end_date}")
num_expected_plots = 19 # Ensure this matches the number of plots generated
plot_data_summaries_for_chatbot = {} # Initialize dict for chatbot summaries
if not token_state_value or not token_state_value.get("token"):
message = "❌ Accesso negato. Nessun token. Impossibile generare le analisi."
logging.warning(message)
placeholder_figs = [create_placeholder_plot(title="Accesso Negato", message="Nessun token.") for _ in range(num_expected_plots)]
# For each plot_config, add a default "no data" summary
for p_cfg in current_plot_configs:
plot_data_summaries_for_chatbot[p_cfg["id"]] = "Accesso negato, nessun dato per il chatbot."
return [message] + placeholder_figs + [plot_data_summaries_for_chatbot]
try:
(filtered_merged_posts_df,
filtered_mentions_df,
date_filtered_follower_stats_df, # For time-based follower plots
raw_follower_stats_df, # For demographic follower plots
start_dt_for_msg, end_dt_for_msg) = \
prepare_filtered_analytics_data(
token_state_value, date_filter_option, custom_start_date, custom_end_date
)
# Generate data summaries for chatbot AFTER data preparation
plot_data_summaries_for_chatbot = generate_chatbot_data_summaries(
current_plot_configs, # Pass the plot_configs list
filtered_merged_posts_df,
filtered_mentions_df,
date_filtered_follower_stats_df,
raw_follower_stats_df,
token_state_value
)
except Exception as e:
error_msg = f"❌ Errore durante la preparazione dei dati per le analisi: {e}"
logging.error(error_msg, exc_info=True)
placeholder_figs = [create_placeholder_plot(title="Errore Preparazione Dati", message=str(e)) for _ in range(num_expected_plots)]
for p_cfg in current_plot_configs:
plot_data_summaries_for_chatbot[p_cfg["id"]] = f"Errore preparazione dati: {e}"
return [error_msg] + placeholder_figs + [plot_data_summaries_for_chatbot]
date_column_posts = token_state_value.get("config_date_col_posts", "published_at")
date_column_mentions = token_state_value.get("config_date_col_mentions", "date")
media_type_col_name = token_state_value.get("config_media_type_col", "media_type")
eb_labels_col_name = token_state_value.get("config_eb_labels_col", "li_eb_label")
plot_figs = [] # Initialize list to hold plot figures
plot_titles_for_errors = [p_cfg["label"] for p_cfg in current_plot_configs]
try:
# Dinamiche dei Follower (2 plots)
plot_figs.append(generate_followers_count_over_time_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly'))
plot_figs.append(generate_followers_growth_rate_plot(date_filtered_follower_stats_df, type_value='follower_gains_monthly')) # Assuming this uses 'follower_gains_monthly' to calculate rate
# Demografia Follower (4 plots)
plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_geo', plot_title="Follower per Località"))
plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_function', plot_title="Follower per Ruolo"))
plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_industry', plot_title="Follower per Settore"))
plot_figs.append(generate_followers_by_demographics_plot(raw_follower_stats_df, type_value='follower_seniority', plot_title="Follower per Anzianità"))
# Approfondimenti Performance Post (4 plots)
plot_figs.append(generate_engagement_rate_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
plot_figs.append(generate_reach_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
plot_figs.append(generate_impressions_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts)) # Ensure 'impressions_sum' or equivalent is used by this func
plot_figs.append(generate_likes_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
# Engagement Dettagliato Post nel Tempo (4 plots)
plot_figs.append(generate_clicks_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
plot_figs.append(generate_shares_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
plot_figs.append(generate_comments_over_time_plot(filtered_merged_posts_df, date_column=date_column_posts))
plot_figs.append(generate_comments_sentiment_breakdown_plot(filtered_merged_posts_df, sentiment_column='comment_sentiment')) # Make sure 'comment_sentiment' exists
# Analisi Strategia Contenuti (3 plots)
plot_figs.append(generate_post_frequency_plot(filtered_merged_posts_df, date_column=date_column_posts))
plot_figs.append(generate_content_format_breakdown_plot(filtered_merged_posts_df, format_col=media_type_col_name))
plot_figs.append(generate_content_topic_breakdown_plot(filtered_merged_posts_df, topics_col=eb_labels_col_name))
# Analisi Menzioni (Dettaglio) (2 plots)
plot_figs.append(generate_mentions_activity_plot(filtered_mentions_df, date_column=date_column_mentions))
plot_figs.append(generate_mention_sentiment_plot(filtered_mentions_df)) # Make sure this function handles empty/malformed df
if len(plot_figs) != num_expected_plots:
logging.warning(f"Mismatch in generated plots. Expected {num_expected_plots}, got {len(plot_figs)}. This will cause UI update issues.")
while len(plot_figs) < num_expected_plots:
plot_figs.append(create_placeholder_plot(title="Grafico Non Generato", message="Logica di generazione incompleta."))
message = f"📊 Analisi aggiornate per il periodo: {date_filter_option}"
if date_filter_option == "Intervallo Personalizzato":
s_display = start_dt_for_msg.strftime('%Y-%m-%d') if start_dt_for_msg else "Qualsiasi"
e_display = end_dt_for_msg.strftime('%Y-%m-%d') if end_dt_for_msg else "Qualsiasi"
message += f" (Da: {s_display} A: {e_display})"
final_plot_figs = []
for i, p_fig_candidate in enumerate(plot_figs):
if p_fig_candidate is not None and not isinstance(p_fig_candidate, str): # Basic check for a plot object
final_plot_figs.append(p_fig_candidate)
else:
err_title = plot_titles_for_errors[i] if i < len(plot_titles_for_errors) else f"Grafico {i+1}"
logging.warning(f"Plot {err_title} (index {i}) non è una figura valida: {p_fig_candidate}. Uso placeholder.")
final_plot_figs.append(create_placeholder_plot(title=f"Errore: {err_title}", message="Impossibile generare figura."))
return [message] + final_plot_figs[:num_expected_plots] + [plot_data_summaries_for_chatbot]
except (KeyError, ValueError) as e_plot_data:
logging.error(f"Errore dati durante la generazione di un grafico specifico: {e_plot_data}", exc_info=True)
error_msg_display = f"Errore dati in un grafico: {str(e_plot_data)[:100]}"
num_already_generated = len(plot_figs)
for i in range(num_already_generated, num_expected_plots):
err_title_fill = plot_titles_for_errors[i] if i < len(plot_titles_for_errors) else f"Grafico {i+1}"
plot_figs.append(create_placeholder_plot(title=f"Errore Dati: {err_title_fill}", message=f"Precedente errore: {str(e_plot_data)[:50]}"))
for p_cfg in current_plot_configs: # Ensure summaries dict is populated on error
if p_cfg["id"] not in plot_data_summaries_for_chatbot:
plot_data_summaries_for_chatbot[p_cfg["id"]] = f"Errore dati grafico: {e_plot_data}"
return [error_msg_display] + plot_figs[:num_expected_plots] + [plot_data_summaries_for_chatbot]
except Exception as e_general:
error_msg = f"❌ Errore generale durante la generazione dei grafici: {e_general}"
logging.error(error_msg, exc_info=True)
placeholder_figs_general = [create_placeholder_plot(title=plot_titles_for_errors[i] if i < len(plot_titles_for_errors) else f"Grafico {i+1}", message=str(e_general)) for i in range(num_expected_plots)]
for p_cfg in current_plot_configs: # Ensure summaries dict is populated on error
if p_cfg["id"] not in plot_data_summaries_for_chatbot:
plot_data_summaries_for_chatbot[p_cfg["id"]] = f"Errore generale grafici: {e_general}"
return [error_msg] + placeholder_figs_general + [plot_data_summaries_for_chatbot]