Spaces:
Paused
Paused
File size: 13,963 Bytes
ee5970e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | # src/chart_generator.py
import matplotlib
matplotlib.use('Agg') # Use a non-interactive backend suitable for scripts/web apps
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import os # Needed for path operations
import numpy as np # For radar charts
# Define some appealing color palettes or specific colors
COLOR_FEEDBACK = '#3498db' # Blue
COLOR_INSTRUCTOR = '#e74c3c' # Red
PALETTE_DISTRIBUTION_FB = 'Blues' # Sequential palette for feedback distribution
PALETTE_DISTRIBUTION_IR = 'Reds' # Sequential palette for instructor distribution
PALETTE_CORR = 'coolwarm' # Diverging for correlation
COLOR_RADAR_FB = '#1f77b4' # Matplotlib default blue
COLOR_RADAR_IR = '#ff7f0e' # Matplotlib default orange
# Helper function to save charts
def save_chart(fig: plt.Figure, output_dir: str, filename: str) -> str | None:
"""Saves a Matplotlib figure to a file and returns the full path."""
try:
if not filename.lower().endswith(".png"):
filename += ".png"
os.makedirs(output_dir, exist_ok=True)
filepath = os.path.join(output_dir, filename)
# Save with good resolution and tight bounding box
fig.savefig(filepath, format='png', bbox_inches='tight', dpi=150)
print(f"Chart saved: {filepath}")
return filepath
except Exception as e:
print(f"Error saving chart '{filename}' to '{output_dir}': {e}")
return None
finally:
plt.close(fig) # Ensure figure is closed to free memory
# --- Updated Plotting Functions ---
def plot_feedback_distribution_per_subject(
feedback_dist: dict,
subject_name: str,
output_dir: str,
filename: str
) -> str | None:
"""Plots feedback distribution and saves as PNG, returning filepath."""
if subject_name not in feedback_dist or not feedback_dist[subject_name]:
print(f"Chart Gen: No feedback data to plot for {subject_name}.")
return None
data = feedback_dist[subject_name]
stars = sorted(data.keys())
counts = [data[s] for s in stars]
if not stars or not counts: return None
fig, ax = plt.subplots(figsize=(8, 5))
sns.barplot(x=stars, y=counts, hue=stars, palette=PALETTE_DISTRIBUTION_FB, order=stars, legend=False, ax=ax)
ax.set_title(f'Feedback Star Distribution: {subject_name}', fontsize=16, fontweight='bold', pad=15)
ax.set_xlabel('Star Rating', fontsize=12)
ax.set_ylabel('Number of Responses', fontsize=12)
ax.tick_params(axis='both', which='major', labelsize=10)
ax.yaxis.grid(True, linestyle='--', linewidth=0.5, alpha=0.7) # Add horizontal grid lines
ax.spines['top'].set_visible(False) # Remove top border
ax.spines['right'].set_visible(False) # Remove right border
# Add count labels on top of bars
for i, count in enumerate(counts):
ax.text(i, count + max(counts)*0.02, f'{count}', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
return save_chart(fig, output_dir, filename)
def plot_instructor_rating_distribution_per_subject(
instructor_rating_dist: dict,
subject_name: str,
output_dir: str,
filename: str
) -> str | None:
"""Plots instructor rating distribution and saves as PNG, returning filepath."""
if subject_name not in instructor_rating_dist or not instructor_rating_dist[subject_name]:
print(f"Chart Gen: No instructor rating data to plot for {subject_name}.")
return None
data = instructor_rating_dist[subject_name]
stars = sorted(data.keys())
counts = [data[s] for s in stars]
if not stars or not counts: return None
fig, ax = plt.subplots(figsize=(8, 5))
sns.barplot(x=stars, y=counts, hue=stars, palette=PALETTE_DISTRIBUTION_IR, order=stars, legend=False, ax=ax)
ax.set_title(f'Instructor Rating Distribution: {subject_name}', fontsize=16, fontweight='bold', pad=15)
ax.set_xlabel('Star Rating', fontsize=12)
ax.set_ylabel('Number of Responses', fontsize=12)
ax.tick_params(axis='both', which='major', labelsize=10)
ax.yaxis.grid(True, linestyle='--', linewidth=0.5, alpha=0.7)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Add count labels on top of bars
for i, count in enumerate(counts):
ax.text(i, count + max(counts)*0.02, f'{count}', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
return save_chart(fig, output_dir, filename)
def plot_avg_scores_per_subject(
avg_scores_subject_df: pd.DataFrame,
output_dir: str,
filename: str
) -> str | None:
"""Plots average scores per subject and saves as PNG, returning filepath."""
if avg_scores_subject_df is None or avg_scores_subject_df.empty: return None
plot_df = avg_scores_subject_df.copy()
plot_df['Average_Feedback_Stars'] = pd.to_numeric(plot_df['Average_Feedback_Stars'], errors='coerce')
plot_df['Average_Instructor_Rating'] = pd.to_numeric(plot_df['Average_Instructor_Rating'], errors='coerce')
plot_df.dropna(subset=['Average_Feedback_Stars', 'Average_Instructor_Rating'], how='all', inplace=True)
if plot_df.empty: return None
try:
fig, ax = plt.subplots(figsize=(max(10, len(plot_df['Subject'].unique()) * 0.9), 6)) # Adjusted size
plot_df_indexed = plot_df.set_index('Subject')
# Use specified colors
plot_df_indexed[['Average_Feedback_Stars', 'Average_Instructor_Rating']].plot(
kind='bar', ax=ax, width=0.8, color=[COLOR_FEEDBACK, COLOR_INSTRUCTOR]
)
ax.set_title('Average Scores per Subject', fontsize=18, fontweight='bold', pad=20)
ax.set_ylabel('Average Rating (1-5)', fontsize=13)
ax.set_xlabel(None) # Remove x-axis label if subjects are clear
ax.tick_params(axis='x', rotation=45, labelsize=11)
plt.setp(ax.get_xticklabels(), ha="right", rotation_mode="anchor")
ax.tick_params(axis='y', labelsize=11)
ax.legend(['Avg Feedback Stars', 'Avg Instructor Rating'], title='Score Type', fontsize=11, title_fontsize=12)
ax.set_ylim(0, 5.5)
ax.yaxis.grid(True, linestyle='--', linewidth=0.5, alpha=0.7)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False) # Hide left axis line for cleaner look
ax.tick_params(axis='y', which='both', left=False) # Hide y-axis ticks
# Add value labels (rounded to 1 decimal)
for container in ax.containers:
ax.bar_label(container, fmt='%.1f', label_type='edge', fontsize=9, padding=3, color='dimgray')
plt.tight_layout(rect=[0, 0.03, 1, 0.95]) # Adjust layout slightly
return save_chart(fig, output_dir, filename)
except Exception as e:
print(f"Chart Gen: Error plotting average scores per subject: {e}")
if 'fig' in locals(): plt.close(fig)
return None
def plot_avg_scores_per_department(
avg_scores_dept_df: pd.DataFrame,
output_dir: str,
filename: str
) -> str | None:
"""Plots average scores per department and saves as PNG, returning filepath."""
if avg_scores_dept_df is None or avg_scores_dept_df.empty: return None
plot_df = avg_scores_dept_df.copy()
plot_df['Average_Feedback_Stars'] = pd.to_numeric(plot_df['Average_Feedback_Stars'], errors='coerce')
plot_df['Average_Instructor_Rating'] = pd.to_numeric(plot_df['Average_Instructor_Rating'], errors='coerce')
plot_df.dropna(subset=['Average_Feedback_Stars', 'Average_Instructor_Rating'], how='all', inplace=True)
if plot_df.empty: return None
try:
fig, ax = plt.subplots(figsize=(max(10, len(plot_df['Department'].unique()) * 0.9), 6))
plot_df_indexed = plot_df.set_index('Department')
plot_df_indexed[['Average_Feedback_Stars', 'Average_Instructor_Rating']].plot(
kind='bar', ax=ax, width=0.8, color=[COLOR_FEEDBACK, COLOR_INSTRUCTOR]
)
ax.set_title('Average Scores per Department', fontsize=18, fontweight='bold', pad=20)
ax.set_ylabel('Average Rating (1-5)', fontsize=13)
ax.set_xlabel(None)
ax.tick_params(axis='x', rotation=45, labelsize=11)
plt.setp(ax.get_xticklabels(), ha="right", rotation_mode="anchor")
ax.tick_params(axis='y', labelsize=11)
ax.legend(['Avg Feedback Stars', 'Avg Instructor Rating'], title='Score Type', fontsize=11, title_fontsize=12)
ax.set_ylim(0, 5.5)
ax.yaxis.grid(True, linestyle='--', linewidth=0.5, alpha=0.7)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.tick_params(axis='y', which='both', left=False)
for container in ax.containers:
ax.bar_label(container, fmt='%.1f', label_type='edge', fontsize=9, padding=3, color='dimgray')
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
return save_chart(fig, output_dir, filename)
except Exception as e:
print(f"Chart Gen: Error plotting average scores per department: {e}")
if 'fig' in locals(): plt.close(fig)
return None
def plot_correlation_heatmap(
df: pd.DataFrame,
output_dir: str,
filename: str,
title_suffix: str = "Overall"
) -> str | None:
"""Plots correlation heatmap and saves as PNG, returning filepath."""
if df is None or df.empty: return None
corr_df = df[['Feedback_Stars', 'Instructor_Rating']].copy()
corr_df['Feedback_Stars'] = pd.to_numeric(corr_df['Feedback_Stars'], errors='coerce')
corr_df['Instructor_Rating'] = pd.to_numeric(corr_df['Instructor_Rating'], errors='coerce')
corr_df.dropna(how='any', inplace=True)
if len(corr_df) < 2 or corr_df['Feedback_Stars'].nunique() < 2 or corr_df['Instructor_Rating'].nunique() < 2: return None
correlation_matrix = corr_df.corr()
if correlation_matrix.isnull().all().all(): return None
fig, ax = plt.subplots(figsize=(6, 5))
sns.heatmap(
correlation_matrix,
annot=True,
cmap=PALETTE_CORR,
fmt=".2f", # Keep .2f for correlation precision
ax=ax,
vmin=-1, vmax=1,
annot_kws={"size": 12}, # Slightly larger annotation
linewidths=.5, # Add lines between cells
linecolor='lightgray'
)
ax.set_title(f'Correlation ({title_suffix})', fontsize=14, fontweight='bold', pad=15)
# Improve tick labels
ax.set_xticklabels(ax.get_xticklabels(), rotation=0, fontsize=11)
ax.set_yticklabels(ax.get_yticklabels(), rotation=0, fontsize=11)
plt.tight_layout(rect=[0, 0, 1, 0.95])
return save_chart(fig, output_dir, filename)
def plot_radar_chart_subject_department(
avg_scores_subject_dept_df: pd.DataFrame,
department_name: str,
output_dir: str,
filename: str
) -> str | None:
"""Plots radar chart and saves as PNG, returning filepath."""
if avg_scores_subject_dept_df is None or avg_scores_subject_dept_df.empty: return None
dept_data_full = avg_scores_subject_dept_df[avg_scores_subject_dept_df['Department'] == department_name]
if dept_data_full.empty: return None
dept_data = dept_data_full.copy()
dept_data['Average_Feedback_Stars'] = pd.to_numeric(dept_data['Average_Feedback_Stars'], errors='coerce')
dept_data['Average_Instructor_Rating'] = pd.to_numeric(dept_data['Average_Instructor_Rating'], errors='coerce')
dept_data.dropna(subset=['Average_Feedback_Stars', 'Average_Instructor_Rating'], how='all', inplace=True)
# Use .loc for fillna
dept_data.loc[:, 'Average_Feedback_Stars'] = dept_data['Average_Feedback_Stars'].fillna(0)
dept_data.loc[:, 'Average_Instructor_Rating'] = dept_data['Average_Instructor_Rating'].fillna(0)
if dept_data.empty: return None
labels = dept_data['Subject'].values
num_vars = len(labels)
if num_vars < 3:
print(f"Chart Gen: Not enough subjects ({num_vars}) for radar chart (Dept: {department_name}). Needs >= 3.")
return None
angles = np.linspace(0, 2 * np.pi, num_vars, endpoint=False).tolist()
angles += angles[:1]
fig, ax = plt.subplots(figsize=(7, 7), subplot_kw=dict(polar=True))
# Feedback Stars
feedback_values = dept_data['Average_Feedback_Stars'].tolist()
feedback_values += feedback_values[:1]
ax.plot(angles, feedback_values, linewidth=2, linestyle='solid', label='Avg Feedback Stars', color=COLOR_RADAR_FB, marker='o', markersize=5)
ax.fill(angles, feedback_values, COLOR_RADAR_FB, alpha=0.25)
# Instructor Rating
instructor_values = dept_data['Average_Instructor_Rating'].tolist()
instructor_values += instructor_values[:1]
ax.plot(angles, instructor_values, linewidth=2, linestyle='solid', label='Avg Instructor Rating', color=COLOR_RADAR_IR, marker='o', markersize=5)
ax.fill(angles, instructor_values, COLOR_RADAR_IR, alpha=0.25)
ax.set_theta_offset(np.pi / 2)
ax.set_theta_direction(-1)
ax.set_xticks(angles[:-1])
# Wrap long subject labels if necessary
wrapped_labels = ['\n'.join(label[i:i+15] for i in range(0, len(label), 15)) for label in labels]
ax.set_xticklabels(wrapped_labels, fontsize=9) # Use wrapped labels
ax.set_yticks(np.arange(0, 6, 1))
ax.set_yticklabels([str(i) for i in np.arange(0, 6, 1)], fontsize=10, color="grey")
ax.set_ylim(0, 5)
ax.grid(color="grey", linestyle='--', linewidth=0.5) # Style grid lines
plt.title(f'Performance Radar: {department_name}', size=16, fontweight='bold', y=1.12) # Adjust title position
ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1), fontsize=10) # Adjust legend position
# fig.patch.set_alpha(0) # Make background transparent if needed for embedding
# ax.patch.set_alpha(0)
# Note: tight_layout might not work well with polar plots + adjusted legend/title. Manual adjustment might be needed if overlaps occur.
# plt.tight_layout() # Use cautiously with polar plots
return save_chart(fig, output_dir, filename)
|