I use matplotlib all the time, but one of the most common problems I run into is when my axes are plotted on a scale that uses scientific notation. This is especially annoying when you’re trying to make two plots stacked on top of each other which share an x-axis. Here’s a simple example of the issue:
import matplotlib
matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 1000)
y = np.sin(x)
z = 1e12\*(1-np.exp(-x))
fig = plt.figure()
fig.subplots_adjust(hspace=0.05)
upperAxes = fig.add_subplot(211)
upperAxes.plot(x, y, color='k', linestyle='-')
upperAxes.set_xticklabels([])
upperAxes.set_ylabel("y")
lowerAxes = fig.add_subplot(212)
lowerAxes.plot(x,z, color='r', linestyle='-')
upperAxes.set_xlabel("x")
upperAxes.set_ylabel("y")
plt.savefig("example.svg")
This code produces the following:
You can immediately see the problem - the exponent is jammed into the plot above. This behavior
turns out to be harder to fix than you think. If you spend some time googling (which you’ve probably
already done if you’re here), most of the solutions to this problem suggest using some variation on
axis.yaxis.get_offset_text().set_y(yn)
to shift the exponent label to new location yn. I have
two main issues with this solution:
- It only allows you to shift the label along one direction, not actually place it wherever you want
- Depending on the placement, it can be ambiguous whether the exponent is associated with the top plot or the bottom plot
pyqtgraph does this so much better than matplotlib by letting users specify units for the axes. Then the exponent is affixed to the axis units label by appropriate SI prefixes, i.e. an exponent of 109 becomes a unit prefix G (for giga-). With this system in mind, we can sort of emulate that functionality by moving the exponent into the axis label, and using a callback function to update the label when the axes limits change:
def label_offset(ax, axis="y"):
if axis == "y":
fmt = ax.yaxis.get_major_formatter()
ax.yaxis.offsetText.set_visible(False)
set_label = ax.set_ylabel
label = ax.get_ylabel()
elif axis == "x":
fmt = ax.xaxis.get_major_formatter()
ax.xaxis.offsetText.set_visible(False)
set_label = ax.set_xlabel
label = ax.get_xlabel()
def update_label(event_axes):
offset = fmt.get_offset()
if offset == '':
set_label("{}".format(label))
else:
set_label("{} ({})".format(label, offset))
return
ax.callbacks.connect("ylim_changed", update_label)
ax.callbacks.connect("xlim_changed", update_label)
ax.figure.canvas.draw()
update_label(None)
return
Now, just call this function on your axis when you instantiate them, and it will take care of the rest. From the example above we add the following two lines to the bottom:
label_offset(lowerAxes, "y")
label_offset(upperAxes, "y")
When we use plt.show()
, the axes and labels work exactly how we want them to. Best of all, the
factor of 1012 will change appropriately if we interact with the plot.
This isn’t a perfect solution, but it’s a good start. We need to be careful not to call the
label_offset
function on an axis more than once, because we are actively altering the axis label,
which means if we do call it more than once on the same axis it might append multiple copies of the
exponent into the label, which isn’t what we want. It also means we can’t actually set the axis
label after this function is called, since that would replace the exponent. As a first attempt at a
solution, however, it works pretty well.