Alex Pearwin

Exponent labels in matplotlib

This post has been archived. It's pretty old and likely concerns topics that are avoidable by using more modern tools and techniques. The original text is preserved below for posterity but it may no longer be relevant or correct.

Creating nice-looking plots in Python is easy with matplotlib. Here’s an example of plotting some data as a histogram.

import numpy as np
from matplotlib import pyplot as plt

fig = plt.figure(figsize=(5, 4))
# Generate some data
mu, sigma = 0, 1
s = np.random.normal(mu, sigma, 10000)
# Plot it
plt.hist(s, 30, histtype='step')
# Format it
ax = plt.gca()
ax.minorticks_on()
ax.set_xlabel('Vertex position [mm]', x=1, ha='right')
ax.set_ylabel('Candidates', y=1, ha='right')
fig.set_tight_layout(True)
fig.savefig('vertex-position.svg')

I like that looking at a matplotlib script is reasonably self-explanatory. Most of the default values are sensible, so it doesn’t take much to create something that looks alright. The above code produces the following.

A histogram of a randomly-sampled Gaussian distribution made with matplotlib.

The only interesting configuration we do is to slightly shrink the figure size, shift the x and y labels to the right of their respective axes, and to tighten up the figure margins. All of these are just cosmetic changes to bring the look inline with what I’m used to.

The problem comes when the axis tick values are large enough to warrant an exponent or offset. If we change mu and sigma to some large values, we can see the problem.

mu, sigma = 1e7, 1e6

This shifts the mean of the distribution to ten million, and changes to width to one million. Running the code above with this change, we get the following.

A Gaussian distribution with a very large mean and width.

The problem is that the exponential prefix is covered by the axis label. (This prefix is equivalent to scientific notation, where numbers like 1230 are represented as 1.230e3, read as ‘one point two three zero times ten to the power three’.)

I came across this while creating a scipt to convert ROOT canvases to matplotlib figures (available soon!). Googling didn’t help, so I ended up looking throught the matplotlib source code. To access the Text object that holds the exponential label, one uses

ax.get_xaxis().get_offset_text()
# Or equivalently
# ax.xaxis.offsetText

where ax is the Axes instance. Once you have this, you can manipulate the object’s size, position, colour, and a bunch of other properties. To shift the exponential label (called ‘offset text’ in matplotlib jargon, as it can also hold an offset value), you can do

ax.get_xaxis().get_offset_text().set_x(0)

(The x property is the x-position of the Text object, from 0 to 1 left-to-right.)

Making this modification, the plot now looks like this.

The previous Gaussian, but with the exponential label now visible.

Much better! But it stills look odd. It’s more conventional to have the exponential label on the right. So, let’s shift the axis label slightly to the left, and keep the exponential fully to the right.

ax.set_xlabel('Vertex position [mm]', x=0.9, ha='right')
# This line's not necessary as x=1 is the default
ax.get_xaxis().get_offset_text().set_x(1)

Now we’re pretty much there.

The previous Gaussian, but with the labels looking conventional.

Nice. My only problem with this result is that the offset text is sitting on a slightly higher baseline than the axis label. This happens because the offset text is actually part of the axis, whereas the label is not. This also means its y-position can’t be modified.

If this really bugs you, you can append the offset text to the axis label and hide the offset text itself. You need to draw the figure first in order for the text to be created.

ax.set_xlabel('Vertex position [mm]', x=1, ha='right')
fig.savefig('vertex-position.svg')
offset = ax.get_xaxis().get_offset_text()
ax.set_xlabel('{0} {1}'.format(ax.get_xlabel(), offset.get_text()))
offset.set_visible(False)
fig.savefig('vertex-position.svg')

All these changes gives the following.

The final figure.

The full code for this final figure is available as a gist.

This solution isn’t ideal, it could look better and be implemented more elegantly, but it’s not a bad start.