The problem
Most of the time when one creates a plot in matplotlib
, whether it
being a line plot using plot()
, scatter plot using scatter()
, 2D
plot using imshow()
or contour()
, matplotlib
will automatically
tick the x- and y- axes and add the tick labels at reasonable
intervals. However, there might be times when you want to alter some
specific tick labels, with a different numerical value, or even a
different string.
Here is a stackoverflow question asking specifically how to Modify tick label text.
To set up the context and help formulate the question, consider this simple case:
import numpy as np import matplotlib.pyplot as plt x = np.arange(5)/1e6 y = x**2 fig, ax = plt.subplots() ax.plot(x, y) fig.show()
It draws a line plot of the parabola y = x^2
, as shown below in
Figure 1.
Suppose we want to change the tick label at position x = 1.0
to
x0
, and that at x = 1.5
to x0 + dx
. This is how one would do it
as suggested by the most voted solution in that
stackoverflow post:
fig, ax = plt.subplots() ax.plot(x, y) fig.canvas.draw() labels = [item.get_text() for item in ax.get_xticklabels()] labels[3] = 'x0' labels[4] = 'x0+dx' ax.set_xticklabels(labels) fig.show()
The result is shown in Figure 2.
The fig.canvas.draw()
line is necessary because the strings for the
tick labels won’t be updated unless explicitly told to. Without this
line, the return value of ax.get_xticklabels()
will be a list of
empty strings, same as the labels
list.
However, you might have noticed that something is missing in this
solution. The scaling factor of 1e-6
, or, using matplotlib
‘s
terminology, the offset
next to the x-axis, is missing.
My proposed solution
Below is my proposed solution:
def changeLabels(axis, pos, newlabels): '''Change specific x/y tick labels Args: axis (Axis): .xaxis or .yaxis obj. pos (list): indices for labels to change. newlabels (list): new labels corresponding to indices in <pos>. ''' if len(pos) != len(newlabels): raise Exception("Length of <pos> doesn't equal that of <newlabels>.") ticks = axis.get_majorticklocs() # get the default tick formatter formatter = axis.get_major_formatter() # format the ticks into strings labels = formatter.format_ticks(ticks) # Modify specific labels for pii, lii in zip(pos, newlabels): labels[pii] = lii # Update the ticks and ticklabels. Order is important here. # Need to first get the offset: offset = formatter.get_offset() # Then set the modified labels: axis.set_ticklabels(labels) # In doing so, matplotlib creates a new FixedFormatter and sets it to the xaxis # and the new FixedFormatter has no offset. So we need to query the # formatter again and re-assign the offset: axis.get_major_formatter().set_offset_string(offset) return fig, ax = plt.subplots() ax.plot(x, y) changeLabels(ax.xaxis, [3, 4], ['x0', 'x0+dx']) fig.show()
The result is shown in Figure 3:
More explanations
A bit of more explanations:
The ticking and labeling of the axes in a matplotlib
plot are handled
by the ticker class. There are 2 folds of the task: 1) the Locator
class handles the positioning of the ticks, and 2) the Formatter
class provides formatted strings as tick labels.
There are a number of different Formatter
s, including the
FixedFormatter
, which is the default one, and the FuncFormatter
, that
accepts a user-defined custom formatting function.
Normally, the locating of the ticks and formatting of the labels are
all done behind the scene, when one draws the plot using plt.show()
or figure.show()
. We are now re-doing these inside the
changeLabels()
function.
Firstly, get the tick values:
ticks = axis.get_majorticklocs()
This is a numpy.ndarray
, with values:
[-5.0e-07 0.0e+00 5.0e-07 1.0e-06 1.5e-06 2.0e-06 2.5e-06 3.0e-06 3.5e-06 4.0e-06 4.5e-06]
At this point, it might be tempting to get the string representation by simply using:
labels = [str(ii) for ii in ticks]
Don’t do this. Unless you have a nice and simple integer array of ticks, this is what you would get from an array of floats:
['-5e-07', '0.0', '5e-07', '1.0000000000000002e-06', '1.5e-06', '2e-06', '2.5e-06', '3e-06', '3.5e-06', '4e-06', '4.499999999999999e-06']
This is exactly what the Formatter
class is designed to do. We query
the default one from the input axis
object, which in this case is
an XAxis
:
formatter = axis.get_major_formatter()
And use it to format the ticks:
labels = formatter.format_ticks(ticks)
This labels
is a Python list
, with values:
['−0.5', '0.0', '0.5', '1.0', '1.5', '2.0', '2.5', '3.0', '3.5', '4.0', '4.5']
This time we get nicely formatted string representations of the
floats. Except that the order of magnitude has been set to 1
. This
is because these values are to be scaled by the offset
, which is
1e-6
.
To get the offset
:
offset = formatter.get_offset()
NOTE that this has to be done before we assign the modified tick labels using the following:
for pii, lii in zip(pos, newlabels): labels[pii] = lii axis.set_ticklabels(labels)
This is because the axis.set_ticklabels()
function will create a new
FixedFormatter
object internally, and assign it to the axis
object. This new FixedFormatter
has no offset
attribute with
it. This is why the stackoverflow solution as shown in Figure 2 is
wrong, and is also the reason why we need to record the original
offset
before calling set_ticklabels()
.
Lastly, after updating the labels, the original offset
is
re-assigned:
axis.get_major_formatter().set_offset_string(offset)