Search code examples
pythonmatplotlibplotbar-chartreportlab

Incomplete Tick Labels On Matplotlib In Reportlab


I'm working on creating a pdf document containing plots using reportlab and matplotlib and I recently experienced a problem where I can't seem to get the x-axis tick labels to line up properly with the data on a simple "side by side" bar plot.

The document is quite long and involved so I've stripped it right back to the essentials. Hopefully Ive whittled it down to a minimal complete and verifiable example.

My distilled code is as follows:

#!/usr/bin/env python3

import numpy as np
import reportlab.lib, reportlab.platypus
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, inch, cm
from reportlab.pdfgen import canvas
import matplotlib.pyplot as plt
from io import BytesIO

class flowable(reportlab.platypus.Flowable):
    def __init__(self, imgdata):
        reportlab.platypus.Flowable.__init__(self)
        self.img = reportlab.lib.utils.ImageReader(imgdata)

    def draw(self):
        self.canv.drawImage(self.img, 0, 0, height = -4.5*inch, width=7*inch)

class LetterMaker(object):
    """"""

    #----------------------------------------------------------------------

    def __init__(self, pdf_file):
        self.c = canvas.Canvas(pdf_file, pagesize=A4)
        self.styles = getSampleStyleSheet()
        self.width, self.height = A4

    #----------------------------------------------------------------------
    def createDocument(self):
        """"""
        rx = [14, 44, 16, 155, 214, 187, 222, 405, 314, 199]
        tx = [56, 92, 103, 28, 22, 12, 24, 75, 20, 15]
        dates = [4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
        labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
        fig = plt.figure()
        ax = fig.add_subplot(1, 1, 1) # nrows, ncols, index
        indices = np.arange(len(dates))
        bar_width = np.min(np.diff(indices))/3.
        ax.axes.set_xticklabels(labels)
        plt.bar(indices-bar_width/2, rx, bar_width)
        plt.bar(indices+bar_width/2, tx, bar_width)
        imgdata = BytesIO()
        fig.savefig(imgdata, format='png', edgecolor='#79b85f', facecolor=fig.get_facecolor())
        imgdata.seek(0)  # rewind the data

        pic = flowable(imgdata)
        pic.wrapOn(self.c, self.width, self.height)
        pic.drawOn(self.c, *self.coord( 2, 14, cm))


    def coord(self, x, y, unit=1):
        x, y = x * unit, self.height -  y * unit
        return x, y

    def savePDF(self):
        """"""
        self.c.save()

if __name__ == "__main__":
    doc = LetterMaker("testfile.pdf")
    doc.createDocument()
    doc.savePDF()

When I run this code the resultant plot looks like this:

enter image description here

I cannot explain why the x-axis labels are not following the complete set as per the labels list.

Initially the dates data was a list of integers.

I suspected that matplotlib was trying to be helpful and fitting the numerical data to the range but only finding the intersecion of the two lists so I provided it as a list of strings but the problem persists.

I've looked around the net and SO for pointers but can't seem to find anything directly relevant.

Ive used the following resources as guidance to write my code and also as troubleshooting assistance:

Reportlab: Mixing Fixed Content and Flowables

api example code: barchart_demo.py

and I have also used this SO question as a guide

How to plot bar graphs with same X coordinates side by side ('dodged')

Could somebody please explain why this is happening?

EDIT

As per the comment by @Paul H, Ive stripped out the reportlab code and still get the same result.

Updated code follows:

#!/usr/bin/env python3

import matplotlib.pyplot as plt
import numpy as np

rx = [14, 44, 16, 155, 214, 187, 222, 405, 314, 199]
tx = [56, 92, 103, 28, 22, 12, 24, 75, 20, 15]
dates = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1) # nrows, ncols, index
indices = np.arange(len(dates))
bar_width = np.min(np.diff(indices))/3.
ax.axes.set_xticklabels(labels)
plt.bar(indices-bar_width/2, rx, bar_width)
plt.bar(indices+bar_width/2, tx, bar_width)
fig.savefig('plot.png')

The result is still the same.


Solution

  • I think you want to use matplotlib ticker and locator functionality here, which helps compute unused/unlabel-able x-values.

    First you want to set the locator to mark every integer with ax.xaxis.set_major_locator(plt.MultipleLocator(1))

    Then set the formatter to a FuncFormatter and pass in a function that takes the x-value and computes/looks up the x-label.

    import matplotlib.pyplot as plt
    import numpy as np
    
    rx = [14, 44, 16, 155, 214, 187, 222, 405, 314, 199]
    tx = [56, 92, 103, 28, 22, 12, 24, 75, 20, 15]
    dates = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1) # nrows, ncols, index
    indices = np.arange(len(dates))
    bar_width = np.min(np.diff(indices))/3.
    ax.bar(indices-bar_width/2, rx, bar_width)
    ax.bar(indices+bar_width/2, tx, bar_width)
    
    
    @plt.FuncFormatter
    def ticks(x, pos):
        try:
            return labels[int(x)]
        except IndexError:
            return ''
    
    ax.xaxis.set_major_locator(plt.MultipleLocator(1))
    ax.xaxis.set_major_formatter(ticks)
    

    enter image description here