Export to PDF: Generate Complete Report PDFs with Embedded Charts
Build A Personal AI Agent – Episode 14
A Practical Guide for Evaluators and M&E Professionals
Why PDF Reports with Embedded Charts Matter
Professional PDF reports with embedded charts are essential for stakeholder communication. This tutorial shows you how to generate complete, polished PDF reports that automatically include charts, tables, and narrative insights—ready for distribution to donors, partners, and decision-makers.
Part 1: Setting Up the PDF Generation System
Step 1: Install Required Libraries
pip install reportlab matplotlib pandas numpy pillowStep 2: PDF Configuration
# pdf_config.py
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
class PDFConfig:
"""Configuration for PDF report generation."""
PAGE_SIZE = A4
MARGIN_TOP = 1.5 * cm
MARGIN_BOTTOM = 1.5 * cm
MARGIN_LEFT = 1.8 * cm
MARGIN_RIGHT = 1.8 * cm
COLORS = {
'primary': colors.HexColor('#1a1a2e'),
'secondary': colors.HexColor('#4a9eff'),
'accent': colors.HexColor('#6c5ce7'),
'success': colors.HexColor('#28a745'),
'warning': colors.HexColor('#ffc107'),
'danger': colors.HexColor('#dc3545'),
'light': colors.HexColor('#f8f9fa'),
'text': colors.HexColor('#333333')
}
@classmethod
def get_styles(cls):
"""Get custom paragraph styles."""
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
name='ReportTitle',
parent=styles['Title'],
fontName='Helvetica-Bold',
fontSize=24,
textColor=cls.COLORS['primary'],
alignment=TA_CENTER,
spaceAfter=20
))
styles.add(ParagraphStyle(
name='Heading1',
parent=styles['Heading1'],
fontName='Helvetica-Bold',
fontSize=18,
textColor=cls.COLORS['secondary'],
spaceAfter=12,
spaceBefore=16
))
styles.add(ParagraphStyle(
name='Heading2',
parent=styles['Heading2'],
fontName='Helvetica-Bold',
fontSize=16,
textColor=cls.COLORS['primary'],
spaceAfter=8,
spaceBefore=12
))
styles.add(ParagraphStyle(
name='BodyText',
parent=styles['Normal'],
fontName='Helvetica',
fontSize=11,
textColor=cls.COLORS['text'],
leading=16,
spaceAfter=6
))
return stylesPart 2: Building the PDF Report Generator
import os
from datetime import datetime
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import io
class PDFReportGenerator:
"""Generate complete PDF reports with embedded charts."""
def __init__(self, config=None, output_dir='pdf_reports'):
self.config = config or PDFConfig()
self.styles = self.config.get_styles()
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
self.elements = []
self.chart_paths = []
self.images = []
def generate_report(self, df, metrics, title, subtitle=None, narratives=None):
"""Generate complete PDF report."""
self.elements = []
self.chart_paths = []
self.images = []
filename = f"{self.output_dir}/report_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf"
doc = SimpleDocTemplate(
filename,
pagesize=self.config.PAGE_SIZE,
topMargin=self.config.MARGIN_TOP,
bottomMargin=self.config.MARGIN_BOTTOM,
leftMargin=self.config.MARGIN_LEFT,
rightMargin=self.config.MARGIN_RIGHT
)
# Build content
self._add_title(title, subtitle)
self._add_summary(metrics)
self._add_charts_with_narratives(df, narratives)
self._add_data_table(df)
self._add_recommendations(df)
self._add_footer()
doc.build(self.elements)
self._cleanup()
return filename
def _add_title(self, title, subtitle=None):
"""Add title section."""
self.elements.append(Paragraph(title, self.styles['ReportTitle']))
if subtitle:
self.elements.append(Paragraph(subtitle, self.styles['BodyText']))
self.elements.append(Spacer(1, 20))
def _add_summary(self, metrics):
"""Add key metrics summary."""
self.elements.append(Paragraph("Executive Summary", self.styles['Heading1']))
total = metrics.get('total_indicators', 0)
on_track = metrics.get('on_track', 0)
at_risk = metrics.get('at_risk', 0)
off_track = metrics.get('off_track', 0)
avg_achievement = metrics.get('avg_achievement', 0)
summary_text = f"""
This report covers {total} indicators with an overall achievement of {avg_achievement:.1f}%.
{on_track} indicators are on track, {at_risk} require attention,
and {off_track} are off track requiring immediate action.
"""
self.elements.append(Paragraph(summary_text.strip(), self.styles['BodyText']))
self.elements.append(Spacer(1, 10))
# Metrics table
data = [
['Metric', 'Value'],
['Total Indicators', str(total)],
['On Track', str(on_track)],
['At Risk', str(at_risk)],
['Off Track', str(off_track)],
['Average Achievement', f"{avg_achievement:.1f}%"]
]
table = Table(data, colWidths=[4*cm, 3*cm])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (1, 0), self.config.COLORS['secondary']),
('TEXTCOLOR', (0, 0), (1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (1, 0), 'CENTER'),
('FONTNAME', (0, 0), (1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (1, 0), 12),
('GRID', (0, 0), (1, 5), 0.5, self.config.COLORS['light']),
('PADDING', (0, 0), (1, 5), 6),
]))
self.elements.append(table)
self.elements.append(Spacer(1, 20))
def _add_charts_with_narratives(self, df, narratives=None):
"""Add charts with AI-generated narratives."""
self.elements.append(Paragraph("Performance Visualizations", self.styles['Heading1']))
# Generate and add charts
charts = self._create_charts(df)
for i, chart_info in enumerate(charts):
self.elements.append(Paragraph(chart_info['title'], self.styles['Heading2']))
# Add chart image
img = Image(chart_info['path'], width=14*cm, height=9*cm)
self.elements.append(img)
self.elements.append(Spacer(1, 5))
# Add narrative if available
if narratives and i < len(narratives):
narrative_key = ['target_actual', 'trend', 'status'][i] if i < 3 else None
if narrative_key and narrative_key in narratives:
narrative_text = narratives[narrative_key]
self.elements.append(Paragraph(
f"Insight: {narrative_text[:300]}...",
self.styles['BodyText']
))
self.elements.append(Spacer(1, 10))
self.elements.append(PageBreak())
def _create_charts(self, df):
"""Create charts for PDF embedding."""
charts = []
# 1. Target vs Actual
if 'target' in df.columns and 'actual' in df.columns:
fig, ax = plt.subplots(figsize=(10, 6))
x = range(len(df))
width = 0.35
ax.bar(x, df['target'], width, label='Target', color='#4a9eff')
ax.bar([i + width for i in x], df['actual'], width, label='Actual', color='#6c5ce7')
ax.set_xlabel('Indicators')
ax.set_ylabel('Values')
ax.set_title('Target vs Actual Performance')
ax.set_xticks([i + width/2 for i in x])
ax.set_xticklabels(df['indicator_name'], rotation=45, ha='right')
ax.legend()
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
path = self._save_chart(fig, 'target_actual')
charts.append({'path': path, 'title': 'Target vs Actual Performance'})
plt.close(fig)
# 2. Achievement Gauge
if 'achievement' in df.columns:
avg_achievement = df['achievement'].mean()
fig, ax = plt.subplots(figsize=(8, 6))
values = [avg_achievement, 100 - avg_achievement]
colors_gauge = ['#28a745' if avg_achievement >= 80 else '#ffc107' if avg_achievement >= 60 else '#dc3545', '#e9ecef']
ax.pie(values, colors=colors_gauge, startangle=90, wedgeprops={'width': 0.3})
ax.text(0, 0, f'{avg_achievement:.0f}%', ha='center', va='center', fontsize=24, fontweight='bold')
ax.text(0, -0.15, 'Overall Achievement', ha='center', va='center', fontsize=14, color='#666')
ax.set_title('Program Performance')
plt.tight_layout()
path = self._save_chart(fig, 'gauge')
charts.append({'path': path, 'title': 'Overall Achievement Gauge'})
plt.close(fig)
# 3. Status Distribution
if 'status' in df.columns:
status_counts = df['status'].value_counts()
fig, ax = plt.subplots(figsize=(8, 6))
colors_status = {'on_track': '#28a745', 'at_risk': '#ffc107', 'off_track': '#dc3545'}
pie_colors = [colors_status.get(s, '#6c757d') for s in status_counts.index]
ax.pie(status_counts.values, labels=status_counts.index,
autopct='%1.1f%%', colors=pie_colors, startangle=90)
ax.set_title('Status Distribution')
plt.tight_layout()
path = self._save_chart(fig, 'status_pie')
charts.append({'path': path, 'title': 'Indicator Status Distribution'})
plt.close(fig)
return charts
def _save_chart(self, fig, name):
"""Save chart image."""
path = f"{self.output_dir}/chart_{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
fig.savefig(path, dpi=150, bbox_inches='tight')
self.chart_paths.append(path)
return path
def _add_data_table(self, df):
"""Add data table."""
self.elements.append(Paragraph("Detailed Data", self.styles['Heading1']))
# Prepare table data
columns = ['indicator_name', 'target', 'actual', 'achievement', 'status']
available_cols = [c for c in columns if c in df.columns]
table_data = [[c.replace('_', ' ').title() for c in available_cols]]
for _, row in df.iterrows():
row_data = []
for col in available_cols:
val = row[col]
if col == 'achievement' and isinstance(val, (int, float)):
row_data.append(f"{val:.1f}%")
else:
row_data.append(str(val))
table_data.append(row_data)
table = Table(table_data, colWidths=[3.5*cm] + [2.5*cm] * (len(available_cols) - 1), repeatRows=1)
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), self.config.COLORS['secondary']),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 10),
('GRID', (0, 0), (-1, -1), 0.5, self.config.COLORS['light']),
('PADDING', (0, 0), (-1, -1), 4),
('FONTSIZE', (0, 1), (-1, -1), 9),
]))
self.elements.append(table)
self.elements.append(Spacer(1, 20))
def _add_recommendations(self, df):
"""Add recommendations section."""
self.elements.append(PageBreak())
self.elements.append(Paragraph("Key Recommendations", self.styles['Heading1']))
recommendations = self._generate_recommendations(df)
for i, rec in enumerate(recommendations, 1):
bullet = f"{i}. {rec}"
self.elements.append(Paragraph(bullet, self.styles['BodyText']))
self.elements.append(Spacer(1, 20))
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")
at_risk = df[df['status'] == 'at_risk']
if len(at_risk) > 0:
recs.append(f"Provide additional support to {len(at_risk)} at-risk indicators")
if 'achievement' in df.columns and df['achievement'].mean() < 80:
recs.append("Review program strategies to improve overall achievement")
recs.append("Continue regular monitoring and data quality checks")
recs.append("Schedule follow-up review in 30 days")
return recs[:5]
def _add_footer(self):
"""Add footer."""
footer_text = "Report generated by M&E AI Agent"
self.elements.append(Paragraph(footer_text, self.styles['BodyText']))
def _cleanup(self):
"""Remove temporary chart files."""
for path in self.chart_paths:
try:
os.remove(path)
except OSError:
passPart 3: Complete Report System
class CompleteReportSystem:
"""Complete PDF report generation system with charts and narratives."""
def __init__(self, data_connector):
self.data_connector = data_connector
self.pdf_generator = PDFReportGenerator()
self.df = None
def load_data(self):
"""Load and prepare data."""
self.df = self.data_connector.get_data()
self._prepare_data()
return self.df
def _prepare_data(self):
"""Clean and prepare 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)
if 'status' not in self.df.columns and 'achievement' in self.df.columns:
conditions = [
self.df['achievement'] >= 90,
self.df['achievement'] >= 70,
self.df['achievement'] < 70
]
choices = ['on_track', 'at_risk', 'off_track']
self.df['status'] = pd.Series(pd.cut(
self.df['achievement'],
bins=[0, 70, 90, 100],
labels=choices
))
def calculate_metrics(self):
"""Calculate performance metrics."""
if self.df is None:
self.load_data()
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_pdf(self, title=None, subtitle=None, narratives=None):
"""Generate complete PDF report."""
if self.df is None:
self.load_data()
metrics = self.calculate_metrics()
if not title:
title = f"M&E Performance Report - {datetime.now().strftime('%B %Y')}"
if not subtitle:
subtitle = f"Generated on {datetime.now().strftime('%B %d, %Y at %H:%M')}"
return self.pdf_generator.generate_report(
df=self.df,
metrics=metrics,
title=title,
subtitle=subtitle,
narratives=narratives
)Part 4: Complete Usage Example
import pandas as pd
import numpy as np
def create_sample_data():
"""Create sample data for PDF report."""
indicators = ['Children Vaccinated', 'Teachers Trained', 'Schools Reached',
'Community Events', 'Health Centers', 'Water Access', 'Sanitation']
categories = ['Health', 'Education', 'Education', 'Community', 'Health', 'WASH', 'WASH']
data = []
for i, indicator in enumerate(indicators):
target = np.random.randint(100, 1000)
achievement = np.random.uniform(55, 98)
actual = int(target * achievement / 100)
if achievement >= 90:
status = 'on_track'
elif achievement >= 70:
status = 'at_risk'
else:
status = 'off_track'
data.append({
'indicator_name': indicator,
'target': target,
'actual': actual,
'achievement': round(achievement, 1),
'status': status,
'category': categories[i],
'date': '2024-01-15'
})
return pd.DataFrame(data)
class SimpleConnector:
"""Simple data connector."""
def __init__(self, data):
self.data = data
def get_data(self):
return self.data
def main():
# Create sample data
df = create_sample_data()
print(f"Created {len(df)} records")
# Initialize connector
connector = SimpleConnector(df)
# Initialize report system
report_system = CompleteReportSystem(connector)
# Load data
report_system.load_data()
# Generate PDF
print("\nGenerating PDF report with charts...")
filename = report_system.generate_pdf(
title="Program Performance Report",
subtitle="Quarterly Review - January to March 2024"
)
print(f"\nPDF report generated successfully!")
print(f"File saved to: {filename}")
print("\nThe PDF includes:")
print("- Executive summary with key metrics")
print("- Target vs Actual chart")
print("- Achievement gauge")
print("- Status distribution chart")
print("- Detailed data table")
print("- Actionable recommendations")
if __name__ == "__main__":
main()
Troubleshooting PDF Generation
| Issue | Solution |
|---|---|
| Image not embedded | Check chart generation and file paths |
| Font not rendering | Use Helvetica/Helvetica-Bold fonts |
| Table overflow | Adjust colWidths for table |
| Missing columns | Ensure data has required columns |
Best Practices for PDF Reports
- Professional Branding: Use organization colors and logo
- Chart Resolution: Use 150 DPI for quality and file size balance
- Page Numbers: Include footer with page numbers
- Table of Contents: Add TOC for longer reports
- Metadata: Include document title, author, creation date
Next Steps: Advanced PDF Features
- Custom Cover Pages: Add branded cover pages
- Table of Contents: Auto-generate TOC with page numbers
- Watermarking: Add draft or confidential watermarks
- Batch Generation: Generate reports for multiple programs
- Email Delivery: Auto-email generated PDF reports
Master PDF Report Generation
The AI Agents for Evaluators Certificate teaches you to build complete M&E solutions including PDF reports, automated charts, and stakeholder communication.
What you will learn:
- Generate professional PDF reports
- Embed charts and visualizations
- Include AI-powered narratives
- Automate report generation
Course Features: 32 lectures · Lifetime access · Certificate included
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.
