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.Pool to 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 with PyPDF2.

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 sourcereportlab (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.

pythonreportlabchartspdf-generation

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.