Axis labels in line with tick labels in matplotlib

Question:

For space reasons, I sometimes make plots in the following style:

fig, ax = plt.subplots(figsize=(3, 3))
ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
ax.set_xlabel("x", labelpad=-8)
ax.set_ylabel("y", labelpad=-18)

Image of the plot produced by the previous code.

Here, I’ve kept just ticks marking the boundaries of the X/Y domains, and I’m manually aligning the xlabel and ylabel using the labelpad keyword argument so that the x and y axis labels visually align with the tick labels.

Note, I’ve had to use different amounts of padding for the different axes, since the length of the y tick labels 1000 and 1001 extends farther away from the axis than the height of the x tick labels 0 and 1, and since the vertical position of the x axis label and the horizontal position of the y axis label are relative to their usual position, which would be just past the extent of the tick labels.

I’m wondering, is there a way to automate this procedure, and to do it exactly rather than visually? For example, if labelpad were relative to the spines, that would be very nice, or if there were a way to determine the extent of the ticks and tick labels away from the spines, that number could be used to automate this as well.

A similar effect can be obtained using ax.yaxis.set_label_coords, but this transforms the position relative to the axes’ transform, and thus depends on the size of the axes, while the ticks are positioned absolutely relative to the spines.

Asked By: mostsquares

||

Answers:

The path you were going down with ax.{x,y}axis.set_label_coords was pretty much there! All you need to do is wrap the transAxes transform in an offset_copy and then provide an offset that is a combination of the current length of the ticks + any space around the tick bbox.

Using Transforms

import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy

fig, ax = plt.subplots(figsize=(3, 3))
fig.set_facecolor('white')

ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])

# Create a transform that vertically offsets the label
#   starting at the edge of the Axes and moving downwards 
#   according to the total length of the bounding box of a major tick
t = offset_copy(
    ax.transAxes, y=-(ax.xaxis.get_tick_padding() + ax.xaxis.majorTicks[0].get_pad()),
    fig=fig, units='dots'
)
ax.xaxis.set_label_coords(.5, 0, transform=t)
ax.set_xlabel('x', va='top')

# Repeat the above, but on the y-axis
t = offset_copy(
    ax.transAxes, x=-(ax.yaxis.get_tick_padding() + ax.yaxis.majorTicks[0].get_pad()),
    fig=fig, units='dots'
)
ax.yaxis.set_label_coords(0, .5, transform=t)
ax.set_ylabel('y', va='bottom')

enter image description here

Test with longer ticks

import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy

fig, ax = plt.subplots(figsize=(3, 3))
fig.set_facecolor('white')

ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
ax.xaxis.set_tick_params(length=10)
ax.yaxis.set_tick_params(length=15)

t = offset_copy(
    ax.transAxes, y=-(ax.xaxis.get_tick_padding() + ax.xaxis.majorTicks[0].get_pad()), 
    fig=fig, units='points'
)
ax.xaxis.set_label_coords(.5, 0, transform=t)
ax.set_xlabel('x', va='top')

t = offset_copy(
    ax.transAxes, x=-(ax.yaxis.get_tick_padding() + ax.yaxis.majorTicks[0].get_pad()),
    fig=fig, units='points'
)
ax.yaxis.set_label_coords(0, .5, transform=t)
ax.set_ylabel('y', va='bottom')

enter image description here

Longer ticks & increased DPI

import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy

fig, ax = plt.subplots(figsize=(3, 3), dpi=150)
fig.set_facecolor('white')

ax.plot([0,1], [1000, 1001])
ax.set_xticks([0, 1])
ax.set_yticks([1000, 1001])
ax.xaxis.set_tick_params(length=10)
ax.yaxis.set_tick_params(length=15)

t = offset_copy(
    ax.transAxes, y=-(ax.xaxis.get_tick_padding() + ax.xaxis.majorTicks[0].get_pad()), 
    fig=fig, units='points'
)
ax.xaxis.set_label_coords(.5, 0, transform=t)
ax.set_xlabel('x', va='top')

t = offset_copy(
    ax.transAxes, x=-(ax.yaxis.get_tick_padding() + ax.yaxis.majorTicks[0].get_pad()), 
    fig=fig, units='points'
)
ax.yaxis.set_label_coords(0, .5, transform=t)
ax.set_ylabel("y", va='bottom')

enter image description here

Answered By: Cameron Riddell
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.