Multiple staggered axes: inset_axes and constrained layout

Hello all, complex vertical plots with a shared x-axis are very common in my field, but I’m having a bit of trouble with the figure layout and saving to PDF. In the following sample I have a host axis that contains four staggered and offset axes. This is a bit different scenario from normal and isn’t a standard twinx since it doesn’t take up the full vertical figure. In these kind of plots there could be many (2-8) time series slightly offset from each other (and overlapping in some cases) but all using the same x-axis (i.e. time; think ice cores or tree ring data on a common time axis). I have this mostly working well with inset_axes(), but it seems like the axis labels are not included in the constrained layout calculation. If I try to save to a 3" by 6" figure for a journal, only the host axis seems to be centered in the PDF. The sub axes labels are totally off the plot. If I include savefig(…, bbox_inches=‘tight’) the sub axes labels are now included, but the figure width has been extended, and the PDF is no longer 3" wide. This is a problem for scientific journals where the figure sizes are tightly controlled.

Two part question, A) is there a way to get the inset_axes() labels properly included in the constrained layout, and B) is there a better way to stack up a bunch of time series on the same x-axis while having multiple staggered y-axes? As in, is inset_axes() the best way to create this type of figure?

Thanks

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from matplotlib._layoutgrid import plot_children

y = np.random.rand(100)

def get_right_staggered_axis(host_ax, y_anchor, height):
    ax = inset_axes(host_ax, width="100%", height="100%",borderpad=0, axes_kwargs=dict(facecolor='none'),
                   bbox_to_anchor=(.0, y_anchor, 1, height),
                   bbox_transform=host_ax.transAxes)
    ax.sharex(host_ax)
    ax.spines[['left', 'top','bottom']].set_visible(False)
    ax.spines[['right']].set_visible(True)
    ax.get_yaxis().tick_right()
    ax.get_yaxis().set_label_position("right")
    ax.get_xaxis().set_tick_params(labelbottom=False, bottom=False)
    ax.set_in_layout(True)
    return ax

def get_left_staggered_axis(host_ax, y_anchor, height):
    ax = inset_axes(host_ax, width="100%", height="100%",borderpad=0, axes_kwargs=dict(facecolor='none'),
                   bbox_to_anchor=(.0, y_anchor, 1, height),
                   bbox_transform=host_ax.transAxes)
    ax.sharex(host_ax)
    ax.spines[['right', 'top','bottom']].set_visible(False)
    ax.spines[['left']].set_visible(True)
    ax.get_xaxis().set_tick_params(labelbottom=False, bottom=False)
    ax.set_in_layout(True)
    return ax


fig, host_ax = plt.subplots(nrows=1, ncols=1, layout='constrained', figsize=(3, 6))

host_ax.set_xlabel("Time",fontsize="small")
host_ax.spines[['right', 'top','left']].set_visible(False)
host_ax.get_yaxis().set_tick_params(labelbottom=False, left=False)
host_ax.spines['bottom'].set_position(('outward', 5))

ax = get_left_staggered_axis(host_ax, 0.75, 0.25)
ax.plot(x,np.random.rand(100)*10)
ax.set_ylabel("Axis 1")

ax = get_right_staggered_axis(host_ax, 0.5, 0.25)
ax.plot(x,np.random.rand(100)*10000)
ax.set_ylabel("Axis 2")

ax = get_left_staggered_axis(host_ax, 0.25, 0.25)
ax.plot(x,np.random.rand(100))
ax.set_ylabel("Axis 3\nlong label")

ax = get_right_staggered_axis(host_ax, 0.0, 0.25)
ax.plot(x,np.random.rand(100))
ax.set_ylabel("Axis 4\nreally\nlong label")

plot_children(fig)

fig.savefig("staggered_axis_test.pdf")
fig.savefig("staggered_axis_tight_test.pdf", bbox_inches='tight') 

Matplotlib has two different inset_axes functions. The newer one is a method on the host axes. If I replace the first line in each of your functions with

ax = host_ax.inset_axes([0, y_anchor, 1, height], facecolor='none')

I get

Thank you, I found the same while my post was pending moderation! This solution does work and is cleaner.