ReportLab Charts — Deep Dive
ReportLab graphics architecture
ReportLab’s charting system is built on reportlab.graphics, which provides a renderer-independent drawing model. A Drawing contains Shape objects (rectangles, circles, paths, strings, groups) that can be rendered to PDF, SVG, PNG, or EPS. Charts are specialized Widget subclasses that generate shapes based on data.
from reportlab.graphics.shapes import Drawing, String, Line
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.legends import Legend
from reportlab.lib.colors import HexColor
Building a complete bar chart
from reportlab.graphics.shapes import Drawing
from reportlab.graphics.charts.barcharts import VerticalBarChart
from reportlab.graphics.charts.legends import Legend
from reportlab.lib.colors import HexColor
from reportlab.graphics import renderPDF
drawing = Drawing(500, 300)
chart = VerticalBarChart()
chart.x = 60
chart.y = 50
chart.width = 380
chart.height = 200
chart.data = [
(45, 62, 78, 55, 90, 72),
(38, 55, 65, 48, 82, 68),
]
chart.categoryAxis.categoryNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
chart.categoryAxis.labels.fontSize = 10
chart.categoryAxis.labels.fontName = "Helvetica"
chart.valueAxis.valueMin = 0
chart.valueAxis.valueMax = 100
chart.valueAxis.valueStep = 20
chart.valueAxis.labels.fontSize = 9
# Series styling
colors = [HexColor("#3498db"), HexColor("#e74c3c")]
for i, color in enumerate(colors):
chart.bars[i].fillColor = color
chart.bars[i].strokeColor = None
chart.barWidth = 12
chart.groupSpacing = 15
chart.barSpacing = 3
drawing.add(chart)
# Legend
legend = Legend()
legend.x = 180
legend.y = 280
legend.alignment = "right"
legend.columnMaximum = 1
legend.colorNamePairs = [
(colors[0], "2025 Revenue"),
(colors[1], "2024 Revenue"),
]
legend.fontSize = 10
drawing.add(legend)
renderPDF.drawToFile(drawing, "bar_chart.pdf", "Revenue Comparison")
Line charts with area fills
from reportlab.graphics.charts.linecharts import HorizontalLineChart
chart = HorizontalLineChart()
chart.x = 60
chart.y = 50
chart.width = 380
chart.height = 200
chart.data = [
(12, 18, 25, 32, 28, 35, 42, 38, 45, 50, 48, 55),
]
chart.categoryAxis.categoryNames = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
chart.categoryAxis.labels.angle = 45
chart.categoryAxis.labels.fontSize = 8
chart.lines[0].strokeColor = HexColor("#2ecc71")
chart.lines[0].strokeWidth = 2.5
chart.lines[0].symbol = makeMarker("Circle")
# Area fill beneath the line
chart.lines[0].fillColor = HexColor("#2ecc7133")
The makeMarker function from reportlab.graphics.widgets.markers creates point markers (Circle, Square, Diamond, Triangle).
Pie charts with exploded slices
from reportlab.graphics.charts.piecharts import Pie
pie = Pie()
pie.x = 150
pie.y = 50
pie.width = 200
pie.height = 200
pie.data = [35, 25, 20, 12, 8]
pie.labels = ["Product A", "Product B", "Product C", "Product D", "Other"]
palette = ["#3498db", "#e74c3c", "#2ecc71", "#f39c12", "#95a5a6"]
for i, color in enumerate(palette):
pie.slices[i].fillColor = HexColor(color)
pie.slices[i].strokeColor = HexColor("#ffffff")
pie.slices[i].strokeWidth = 2
# Explode the largest slice
pie.slices[0].popout = 15
pie.sideLabels = True
pie.simpleLabels = False
pie.slices.labelRadius = 1.2
pie.slices.fontSize = 10
Multi-chart report with Platypus
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
styles = getSampleStyleSheet()
story = []
# Title
story.append(Paragraph("Monthly Performance Report", styles["Title"]))
story.append(Spacer(1, 0.5 * cm))
# Summary text
story.append(Paragraph(
"Revenue increased 15% quarter-over-quarter, driven primarily by "
"Product A growth in the EMEA region.",
styles["BodyText"]
))
story.append(Spacer(1, 1 * cm))
# Bar chart
bar_drawing = Drawing(450, 250)
# ... build chart as above ...
bar_drawing.add(chart)
story.append(bar_drawing)
story.append(Spacer(1, 1 * cm))
# Data table beneath the chart
table_data = [
["Month", "2025", "2024", "Change"],
["Jan", "$45K", "$38K", "+18%"],
["Feb", "$62K", "$55K", "+13%"],
# ...
]
table = Table(table_data, colWidths=[80, 80, 80, 80])
table.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), HexColor("#3498db")),
("TEXTCOLOR", (0, 0), (-1, 0), HexColor("#ffffff")),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("GRID", (0, 0), (-1, -1), 0.5, HexColor("#cccccc")),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
]))
story.append(table)
# Page break before next section
story.append(PageBreak())
doc = SimpleDocTemplate("report.pdf", pagesize=A4)
doc.build(story)
Custom chart widgets
When built-in charts do not suffice, create custom widgets:
from reportlab.graphics.widgetbase import Widget
from reportlab.graphics.shapes import Group, Rect, String
from reportlab.lib.validators import isNumber
class ProgressBar(Widget):
_attrMap = {
'x': isNumber, 'y': isNumber,
'width': isNumber, 'height': isNumber,
'value': isNumber, 'maxValue': isNumber,
}
def __init__(self):
self.x = 0
self.y = 0
self.width = 200
self.height = 20
self.value = 65
self.maxValue = 100
def draw(self):
g = Group()
# Background
g.add(Rect(self.x, self.y, self.width, self.height,
fillColor=HexColor("#ecf0f1"), strokeColor=None))
# Fill
fill_width = (self.value / self.maxValue) * self.width
g.add(Rect(self.x, self.y, fill_width, self.height,
fillColor=HexColor("#3498db"), strokeColor=None))
# Label
g.add(String(self.x + self.width + 10, self.y + 5,
f"{self.value}%", fontSize=12))
return g
Custom widgets integrate seamlessly with the drawing system and can be reused across reports.
Batch generation patterns
Template-based generation
import json
def generate_report(client_data, output_path):
story = []
story.append(Paragraph(f"Report for {client_data['name']}", styles["Title"]))
drawing = Drawing(450, 250)
chart = VerticalBarChart()
chart.data = [client_data["monthly_revenue"]]
chart.categoryAxis.categoryNames = client_data["months"]
# ... styling ...
drawing.add(chart)
story.append(drawing)
doc = SimpleDocTemplate(output_path, pagesize=A4)
doc.build(story)
# Process all clients
with open("clients.json") as f:
clients = json.load(f)
for client in clients:
generate_report(client, f"reports/{client['id']}.pdf")
Performance for high-volume generation
- Reuse font objects — Font registration happens once globally. Avoid re-registering in loops.
- Minimize Drawing complexity — Hundreds of data points per chart are fine; thousands can slow rendering. Pre-aggregate large datasets.
- Multiprocessing — PDF generation is CPU-bound. Use
multiprocessing.Poolto parallelize across clients. - Memory — Each
SimpleDocTemplate.build()creates the full PDF in memory before writing. For very large documents (1000+ pages), consider writing sections separately and merging withPyPDF2.
Multi-format output
The same Drawing can be rendered to different formats:
from reportlab.graphics import renderPDF, renderSVG
renderPDF.drawToFile(drawing, "chart.pdf")
renderSVG.drawToFile(drawing, "chart.svg")
# PNG requires rlextra or Pillow conversion from SVG
This makes ReportLab charts usable beyond PDF — for web display (SVG) or embedding in presentations.
Tradeoffs
- Styling verbosity — Configuring every visual property takes many lines. Consider building a helper function that applies your brand defaults.
- No interactivity — PDFs are static documents. For interactive charts, use Plotly or Bokeh.
- Limited chart types — No treemaps, heatmaps, or Sankey diagrams out of the box. You would build these from primitives.
- Commercial vs open source —
reportlab(open source) covers most needs.rlextra(commercial) adds advanced features like bitmap chart export and some additional chart types.
One thing to remember
ReportLab’s charting module turns data into publication-ready vector graphics inside PDF documents — and because charts are Platypus flowables, they integrate seamlessly with text, tables, and page layouts for fully automated, data-driven report generation at scale.
See Also
- Python Adaptive Learning Systems How Python builds learning apps that adjust to each student like a personal tutor who knows exactly what you need next.
- Python Airflow Learn Airflow as a timetable manager that makes sure data tasks run in the right order every day.
- Python Altair Learn Altair through the idea of drawing charts by describing rules, not by hand-placing every visual element.
- Python Automated Grading How Python grades homework and exams automatically, from simple answer keys to understanding written essays.
- Python Batch Vs Stream Processing Batch processing is like doing laundry once a week; stream processing is like a self-cleaning shirt that cleans itself constantly.