Figure legend with constrained_layout

Hello, I’m trying to add a figure legend on the top of the figure that is using costrained_layout. However, matplotlib puts it inside the axis. The code:

import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(constrained_layout=True)
ax = fig.subplots()

x = np.linspace(0, 2*np.pi, 200)
for n in [1, 2, 3, 4]:
    ax.plot(x, np.sin(n*x), label=f"sin({n}x)")

fig.legend(loc="upper center", ncol=4, mode="expand")
plt.savefig("four_plots.png", pad_inches=0.0, bbox_inches="tight")

I managed to move it out of the axis by using

fig.legend(bbox_to_anchor=(0., 1., 1., 0.01), loc="lower left", ncol=4, mode="expand")

instead. However, now the box around the legend is clipped of at the top:

How would I do this correctly?

Use ax.legend instead so the axes is resized to make room for the legend.

If I use ax.legend(), it also puts the legend inside the axes, which is not what I wanted.

Also, and I should have mentioned that, I have some plots with subplots that should share the legend, i.e.:

import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(constrained_layout=True)
axs = fig.subplots(ncols=2)

x = np.linspace(0, 2*np.pi, 200)
for n in [1, 2, 3, 4]:
    axs[0].plot(x, np.sin(n*x), label=f"n = {n}")
    axs[1].plot(x, np.cos(n*x))

fig.legend(loc="upper center", ncol=4, mode="expand")
plt.savefig("legend_placement.png", pad_inches=0.0, bbox_inches="tight")

Right - that requires a bit of mucking around:


import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(constrained_layout=True)
axs = fig.subplots(ncols=2)

x = np.linspace(0, 2*np.pi, 200)
for n in [1, 2, 3, 4]:
    axs[0].plot(x, np.sin(n*x), label=f"n = {n}")
    axs[1].plot(x, np.cos(n*x))

axs[0].legend(loc="upper center", ncol=4, mode="expand", 
              bbox_to_anchor=(0, 0, 2, 1.1), 
             bbox_transform=axs[0].transAxes)
plt.show()

Thanks! It behaves a bit strangely when changing the width in bbox_to_anchor, i.e. pulls the two plots apart when I use for example 3 instead of 2, but I guess I’ll have to figure that out relative to my figure width.

The 2 is relative to the axes width, so if you make it 3, there had better be 3 subplots, or it will make extra room.

I’m not claiming this is particularly ideal. But its hard to think about other approaches that would work with how CL works.

You could make a third axes to hold the legend, and maybe that is the most automatic. Note that the natural height of this axis is essentially 0:

import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(constrained_layout=True)
axs = fig.subplot_mosaic([['legend', 'legend'],['left', 'right']], 
                          gridspec_kw={'height_ratios':[0.001, 1]})

x = np.linspace(0, 2*np.pi, 200)
for n in [1, 2, 3, 4]:
    axs['left'].plot(x, np.sin(n*x), label=f"n = {n}")
    axs['right'].plot(x, np.cos(n*x))

axs['legend'].axis('off')
handles, labels = axs['left'].get_legend_handles_labels()
axs['legend'].legend(handles, labels, loc="upper center", ncol=4, mode="expand")

plt.show()

Legend

BTW there is a PR for this https://github.com/matplotlib/matplotlib/pull/13072 But I’ve not come back to it for quite a while.

Thanks again for the detailed response. Very helpful.

Thanks for the reminder about this. ENH: allow fig.legend in layout by jklymak · Pull Request #19743 · matplotlib/matplotlib · GitHub is a much simpler PR. Pretty sure it will be in for 3.5

This is great, thanks a lot @jklymak! Your second solution, using fig.subplot_mosaic(), is the only one that I have found to work when displaying a legend spanning more than one axis, and using both constrained_layout and savefig. With all the other solutions I’ve tried, savefig would mess up the layout, and the axes would be collapsed in the saved figure. This happened in Matplotlib 3.6, but it did not use to happen in Matplotlib 3.5, not sure why.

If there was a reversion, might be good to document it with a reproducible example? I’m not 100% sure what could have changed.