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:
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.
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)