Build A Personal AI Agent – Episode 8
Add Visualizations and Charts to Automated Reports
A Practical Guide for Evaluators and M&E Professionals
Why Add Visualizations to Automated Reports
Visualizations transform raw data into compelling stories. When combined with automated reporting, charts and graphs help stakeholders quickly understand performance, identify trends, and make data-driven decisions. This tutorial shows you how to embed charts directly into your automated reports.
Part 1: Setting Up the Visualization-Reporting Integration
Step 1: Install Required Libraries
pip install matplotlib seaborn pandas plotly openpyxl PillowStep 2: Visualization Integration Functions
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime
import os
import base64
from io import BytesIO
class ReportVisualizer:
"""Handles visualization generation for reports."""
def __init__(self, output_dir='report_charts'):
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.chart_paths = []
def create_target_actual_chart(self, df, output_format='png'):
"""Create target vs actual bar chart."""
plt.figure(figsize=(12, 6))
x = range(len(df))
width = 0.35
bars1 = plt.bar(x, df['target'], width, label='Target', color='#4a9eff')
bars2 = plt.bar([i + width for i in x], df['actual'], width, label='Actual', color='#6c5ce7')
plt.xlabel('Indicators', fontsize=12)
plt.ylabel('Values', fontsize=12)
plt.title('Target vs Actual Performance', fontsize=16, fontweight='bold')
plt.xticks([i + width/2 for i in x], df['indicator_name'], rotation=45, ha='right')
plt.legend()
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
# Save chart
filename = f"{self.output_dir}/target_actual_{datetime.now().strftime('%Y%m%d_%H%M')}.{output_format}"
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
self.chart_paths.append(filename)
return self._encode_image(filename)
def create_achievement_gauge(self, achievement_percent, output_format='png'):
"""Create a gauge chart for overall achievement."""
fig, ax = plt.subplots(figsize=(8, 6))
# Create gauge using pie chart
if achievement_percent <= 100:
values = [achievement_percent, 100 - achievement_percent]
colors = ['#28a745', '#e9ecef']
else:
values = [100, 0]
colors = ['#28a745', '#e9ecef']
wedges, texts = ax.pie(values, colors=colors, startangle=90,
wedgeprops={'width': 0.3})
# Add center text
ax.text(0, 0, f'{achievement_percent:.0f}%',
ha='center', va='center', fontsize=24, fontweight='bold')
ax.text(0, -0.15, 'Overall Achievement',
ha='center', va='center', fontsize=14, color='#666')
plt.title('Program Performance', fontsize=16, fontweight='bold')
plt.tight_layout()
filename = f"{self.output_dir}/gauge_{datetime.now().strftime('%Y%m%d_%H%M')}.{output_format}"
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
self.chart_paths.append(filename)
return self._encode_image(filename)
def create_trend_chart(self, df, indicator_name=None, output_format='png'):
"""Create trend line chart."""
plt.figure(figsize=(12, 6))
if indicator_name:
df_plot = df[df['indicator_name'] == indicator_name].sort_values('date')
plt.plot(df_plot['date'], df_plot['actual'], marker='o',
label='Actual', color='#4a9eff', linewidth=2)
plt.plot(df_plot['date'], df_plot['target'], marker='s',
label='Target', color='#dc3545', linewidth=2, linestyle='--')
title = f'{indicator_name} - Progress Over Time'
else:
# Aggregate by date
df_plot = df.groupby('date')['achievement'].mean().reset_index()
plt.plot(df_plot['date'], df_plot['achievement'], marker='o',
color='#4a9eff', linewidth=2)
plt.axhline(y=80, color='#ffc107', linestyle='--', label='Target (80%)')
title = 'Overall Progress Over Time'
plt.xlabel('Date', fontsize=12)
plt.ylabel('Value / Achievement (%)', fontsize=12)
plt.title(title, fontsize=16, fontweight='bold')
plt.legend()
plt.grid(True, alpha=0.3)
plt.xticks(rotation=45)
plt.tight_layout()
filename = f"{self.output_dir}/trend_{datetime.now().strftime('%Y%m%d_%H%M')}.{output_format}"
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
self.chart_paths.append(filename)
return self._encode_image(filename)
def create_heatmap(self, df, output_format='png'):
"""Create achievement heatmap."""
if 'category' in df.columns:
pivot = df.pivot_table(values='achievement',
index='category',
columns='indicator_name',
aggfunc='mean')
else:
df['date_str'] = df['date'].dt.strftime('%Y-%m')
pivot = df.pivot_table(values='achievement',
index='date_str',
columns='indicator_name',
aggfunc='mean')
plt.figure(figsize=(14, 8))
sns.heatmap(pivot, annot=True, fmt='.0f', cmap='RdYlGn',
cbar_kws={'label': 'Achievement (%)'},
linewidths=0.5, linecolor='white')
plt.title('Achievement Heatmap', fontsize=16, fontweight='bold')
plt.tight_layout()
filename = f"{self.output_dir}/heatmap_{datetime.now().strftime('%Y%m%d_%H%M')}.{output_format}"
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
self.chart_paths.append(filename)
return self._encode_image(filename)
def create_status_pie(self, df, output_format='png'):
"""Create status distribution pie chart."""
if 'status' in df.columns:
status_counts = df['status'].value_counts()
colors = {
'on_track': '#28a745',
'at_risk': '#ffc107',
'off_track': '#dc3545',
'completed': '#4a9eff'
}
pie_colors = [colors.get(status, '#6c757d') for status in status_counts.index]
plt.figure(figsize=(8, 8))
plt.pie(status_counts.values, labels=status_counts.index,
autopct='%1.1f%%', colors=pie_colors, startangle=90)
plt.title('Status Distribution', fontsize=16, fontweight='bold')
plt.tight_layout()
filename = f"{self.output_dir}/status_pie_{datetime.now().strftime('%Y%m%d_%H%M')}.{output_format}"
plt.savefig(filename, dpi=300, bbox_inches='tight')
plt.close()
self.chart_paths.append(filename)
return self._encode_image(filename)
def _encode_image(self, filename):
"""Encode image to base64 for embedding in HTML reports."""
with open(filename, 'rb') as f:
image_data = f.read()
encoded = base64.b64encode(image_data).decode('utf-8')
# Determine file extension
ext = filename.split('.')[-1]
mime_type = f'image/{ext}'
return f'data:{mime_type};base64,{encoded}'
def get_all_charts(self, df):
"""Generate and return all chart encodings."""
charts = {
'target_actual': self.create_target_actual_chart(df),
'achievement_gauge': self.create_achievement_gauge(df['achievement'].mean()),
'status_pie': self.create_status_pie(df),
'trend': self.create_trend_chart(df),
'heatmap': self.create_heatmap(df)
}
return chartsPart 2: HTML Report with Embedded Charts
class HTMLReportGenerator:
"""Generate HTML reports with embedded charts."""
def __init__(self, visualizer, title="M&E Performance Report"):
self.visualizer = visualizer
self.title = title
def generate_html_report(self, df, metrics, charts):
"""Generate complete HTML report with charts."""
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{self.title}</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f8f9fa;
padding: 20px;
color: #333;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
h1 {{
color: #1a1a2e;
border-bottom: 3px solid #4a9eff;
padding-bottom: 10px;
margin-bottom: 20px;
text-align: center;
}}
.summary-box {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}}
.metric-card {{
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #4a9eff;
}}
.metric-value {{
font-size: 24px;
font-weight: bold;
color: #1a1a2e;
}}
.metric-label {{
font-size: 14px;
color: #666;
}}
.chart-grid {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 25px 0;
}}
.chart-container {{
background: white;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 15px;
text-align: center;
}}
.chart-container img {{
max-width: 100%;
height: auto;
border-radius: 4px;
}}
.chart-full {{
grid-column: 1 / -1;
}}
.section {{
margin: 30px 0;
}}
.section h2 {{
color: #4a9eff;
border-bottom: 2px solid #e9ecef;
padding-bottom: 8px;
margin-bottom: 15px;
}}
.status {{
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}}
.status-on_track {{ background: #28a745; color: white; }}
.status-at_risk {{ background: #ffc107; color: #333; }}
.status-off_track {{ background: #dc3545; color: white; }}
table {{
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}}
th {{
background: #1a1a2e;
color: white;
padding: 10px;
text-align: left;
}}
td {{
padding: 8px 10px;
border-bottom: 1px solid #e9ecef;
}}
tr:hover {{
background: #f8f9fa;
}}
.footer {{
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: center;
font-size: 12px;
color: #666;
}}
@media (max-width: 768px) {{
.chart-grid {{
grid-template-columns: 1fr;
}}
.summary-box {{
grid-template-columns: 1fr 1fr;
}}
}}
</style>
</head>
<body>
<div class="container">
<h1>{self.title}</h1>
<p style="text-align: center; color: #666;">Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<!-- Executive Summary -->
<div class="section">
<h2>Executive Summary</h2>
<p>{self._generate_summary(metrics, df)}</p>
</div>
<!-- Key Metrics -->
<div class="section">
<h2>Key Metrics</h2>
<div class="summary-box">
<div class="metric-card">
<div class="metric-value">{len(df)}</div>
<div class="metric-label">Total Indicators</div>
</div>
<div class="metric-card">
<div class="metric-value" style="color: #28a745;">{metrics.get('on_track', 0)}</div>
<div class="metric-label">On Track</div>
</div>
<div class="metric-card">
<div class="metric-value" style="color: #ffc107;">{metrics.get('at_risk', 0)}</div>
<div class="metric-label">At Risk</div>
</div>
<div class="metric-card">
<div class="metric-value" style="color: #dc3545;">{metrics.get('off_track', 0)}</div>
<div class="metric-label">Off Track</div>
</div>
<div class="metric-card">
<div class="metric-value">{metrics.get('avg_achievement', 0):.1f}%</div>
<div class="metric-label">Average Achievement</div>
</div>
</div>
</div>
<!-- Charts -->
<div class="section">
<h2>Performance Visualizations</h2>
<div class="chart-grid">
<div class="chart-container chart-full">
<h3>Overall Achievement</h3>
<img src="{charts.get('achievement_gauge', '')}" alt="Achievement Gauge">
</div>
<div class="chart-container">
<h3>Status Distribution</h3>
<img src="{charts.get('status_pie', '')}" alt="Status Distribution">
</div>
<div class="chart-container">
<h3>Target vs Actual</h3>
<img src="{charts.get('target_actual', '')}" alt="Target vs Actual">
</div>
<div class="chart-container chart-full">
<h3>Progress Trend</h3>
<img src="{charts.get('trend', '')}" alt="Progress Trend">
</div>
<div class="chart-container chart-full">
<h3>Achievement Heatmap</h3>
<img src="{charts.get('heatmap', '')}" alt="Heatmap">
</div>
</div>
</div>
<!-- Data Table -->
<div class="section">
<h2>Detailed Indicator Data</h2>
{self._generate_table(df)}
</div>
<!-- Recommendations -->
<div class="section">
<h2>Recommendations</h2>
<ul>
{''.join(f'<li>{rec}</li>' for rec in self._generate_recommendations(df))}
</ul>
</div>
<div class="footer">
<p>Report generated by AI Agent for M&E • {datetime.now().strftime('%Y')}</p>
</div>
</div>
</body>
</html>
"""
return html
def _generate_summary(self, metrics, df):
"""Generate executive summary text."""
return f"""This report covers {len(df)} indicators with an overall achievement of {metrics.get('avg_achievement', 0):.1f}%.
{metrics.get('on_track', 0)} indicators are on track, {metrics.get('at_risk', 0)} require attention,
and {metrics.get('off_track', 0)} are off track and need immediate intervention.
Key achievements include {self._get_achievements(df)[:1]}."""
def _get_achievements(self, df):
"""Get top achievements."""
if 'achievement' in df.columns:
top = df.nlargest(3, 'achievement')
return [f"{row['indicator_name']} ({row['achievement']:.0f}%)" for _, row in top.iterrows()]
return ["No achievement data available"]
def _generate_table(self, df):
"""Generate HTML table from data."""
html = """
<table>
<thead>
<tr>
<th>Indicator</th>
<th>Target</th>
<th>Actual</th>
<th>Achievement</th>
<th>Status</th>
</tr>
</thead>
<tbody>
"""
for _, row in df.iterrows():
status_class = f"status-{row['status'].replace(' ', '_')}" if 'status' in row else ""
html += f"""
<tr>
<td>{row.get('indicator_name', 'N/A')}</td>
<td>{row.get('target', 'N/A')}</td>
<td>{row.get('actual', 'N/A')}</td>
<td>{row.get('achievement', 0):.1f}%</td>
<td><span class="status {status_class}">{row.get('status', 'N/A')}</span></td>
</tr>
"""
html += """
</tbody>
</table>
"""
return html
def _generate_recommendations(self, df):
"""Generate recommendations based on data."""
recs = []
if 'status' in df.columns:
off_track = df[df['status'] == 'off_track']
if len(off_track) > 0:
recs.append(f"Prioritize interventions for {len(off_track)} off-track indicators")
if len(df[df['status'] == 'at_risk']) > 0:
recs.append("Provide additional support to at-risk areas")
recs.append("Continue regular monitoring and reporting")
recs.append("Consider adjusting targets based on current performance")
return recs[:4]Part 3: Complete Automated Reporting System
class AutomatedReportSystem:
"""Complete system for generating reports with visualizations."""
def __init__(self, data_source='excel', file_path='indicator_data.xlsx'):
self.data_source = data_source
self.file_path = file_path
self.visualizer = ReportVisualizer()
self.report_generator = HTMLReportGenerator(self.visualizer)
self.df = None
def load_data(self):
"""Load data from source."""
if self.data_source == 'excel':
if not os.path.exists(self.file_path):
raise FileNotFoundError(f"File not found: {self.file_path}")
self.df = pd.read_excel(self.file_path)
self._prepare_data()
return self.df
def _prepare_data(self):
"""Prepare and clean data."""
if 'date' in self.df.columns:
self.df['date'] = pd.to_datetime(self.df['date'])
if 'target' in self.df.columns and 'actual' in self.df.columns:
self.df['achievement'] = (self.df['actual'] / self.df['target'] * 100).round(1)
return self.df
def calculate_metrics(self):
"""Calculate key metrics."""
metrics = {
'total_indicators': len(self.df),
'avg_achievement': self.df['achievement'].mean() if 'achievement' in self.df else 0,
'on_track': len(self.df[self.df['status'] == 'on_track']) if 'status' in self.df else 0,
'at_risk': len(self.df[self.df['status'] == 'at_risk']) if 'status' in self.df else 0,
'off_track': len(self.df[self.df['status'] == 'off_track']) if 'status' in self.df else 0
}
return metrics
def generate_report(self, include_charts=True):
"""Generate complete report with charts."""
if self.df is None:
self.load_data()
# Calculate metrics
metrics = self.calculate_metrics()
# Generate charts
charts = {}
if include_charts:
print("Generating charts...")
charts = self.visualizer.get_all_charts(self.df)
# Generate HTML report
print("Generating HTML report...")
html = self.report_generator.generate_html_report(self.df, metrics, charts)
# Save report
filename = f"report_{datetime.now().strftime('%Y%m%d_%H%M')}.html"
with open(filename, 'w', encoding='utf-8') as f:
f.write(html)
print(f"Report saved: {filename}")
return filename
def generate_and_save(self):
"""Generate report and save with timestamp."""
return self.generate_report(include_charts=True)Part 4: Complete Usage Example
# Complete workflow for automated reporting with visualizations
def main():
# Step 1: Create sample data
sample_data = pd.DataFrame({
'indicator_name': ['Children Vaccinated', 'Teachers Trained', 'Schools Reached',
'Community Events', 'Health Centers', 'Water Access', 'Sanitation'],
'target': [5000, 200, 100, 50, 30, 80, 60],
'actual': [4800, 180, 85, 40, 20, 72, 55],
'date': ['2024-01-15', '2024-01-15', '2024-01-15',
'2024-01-15', '2024-01-15', '2024-01-15', '2024-01-15'],
'status': ['on_track', 'on_track', 'at_risk', 'at_risk',
'off_track', 'on_track', 'on_track'],
'category': ['Health', 'Education', 'Education', 'Community',
'Health', 'WASH', 'WASH']
})
# Add multiple dates for trend
dates = ['2024-01-15', '2024-02-15', '2024-03-15', '2024-04-15']
trend_data = []
for i, date in enumerate(dates):
for idx, row in sample_data.iterrows():
new_row = row.copy()
new_row['date'] = date
new_row['actual'] = row['actual'] * (0.9 + 0.2 * i / len(dates))
trend_data.append(new_row)
df_trend = pd.DataFrame(trend_data)
df_trend.to_excel('indicator_data.xlsx', index=False)
# Step 2: Initialize the report system
system = AutomatedReportSystem('excel', 'indicator_data.xlsx')
# Step 3: Generate report with charts
print("Generating automated report with visualizations...")
report_file = system.generate_report()
print(f"\nReport generated successfully!")
print(f"Open {report_file} in your web browser to view the complete report.")
print("\nThe report includes:")
print("- Executive summary with key metrics")
print("- Performance dashboard with gauges and charts")
print("- Target vs actual comparison")
print("- Trend analysis")
print("- Heatmap of achievement by category")
print("- Detailed data table")
print("- Actionable recommendations")
if __name__ == "__main__":
main()
Troubleshooting Common Issues
| Issue | Solution |
|---|---|
| Charts not showing in HTML | Check base64 encoding and image paths |
| Font too small in charts | Adjust fontsize parameters in chart functions |
| Missing columns in data | Ensure required columns: indicator_name, target, actual |
| Report generation fails | Check data types and handle missing values |
| Slow report generation | Reduce chart resolution or number of charts |
Best Practices for Report Visualizations
- Use Consistent Colors: Maintain color schemes across charts (green=good, red=bad)
- Include Context: Add titles, labels, and legends to every chart
- Keep It Simple: Avoid cluttered charts with too many elements
- Optimize for Mobile: Use responsive design for viewing on all devices
- Automate Updates: Schedule regular report generation
- Include Data Tables: Always provide raw data alongside visualizations
Next Steps: Advanced Reporting Features
- Interactive Dashboards: Use Plotly for interactive charts
- PDF Export: Generate PDF reports with embedded charts
- Email Delivery: Automatically email reports to stakeholders
- Multi-Language Reports: Translate reports for international audiences
- Real-Time Updates: Connect to live data sources for up-to-date reporting
Master Automated Reporting with Visualizations
The AI Agents for Evaluators Certificate teaches you to build complete M&E solutions including automated reports with rich visualizations, interactive dashboards, and stakeholder-ready outputs.
What you will learn:
- Build automated reporting pipelines with visualizations
- Create professional HTML reports with embedded charts
- Integrate multiple chart types for comprehensive analysis
- Generate stakeholder-ready reports automatically
Course Features: 32 lectures · Lifetime access · Certificate included · 241 students enrolled
Enroll Now – $249 Lifetime Access
Bundle with AI in M&E course and save 30%
This guide is part of the AI Agents for Evaluators series. Continue your learning journey with the full certificate course above.
