'Centering matplotlib legend entries within incomplete/unfilled rows?

Say I'm making a plot with five items, and only have room to create a legend with 3 columns (more columns than this would be too wide), e.g.

import matplotlib.pyplot as plt
f, a = plt.subplots()
for i in range(5):
    a.plot(np.arange(10),np.random.rand(10),label='Item #%d'%i)
a.legend(ncol=3)

The trailing two entries in the bottom row are left-aligned, leaving a big empty space on the right that isn't very aesthetically pleasing. This becomes especially problematic when you have to label very large numbers of lines.

Is there any way to center the entries within the unfilled row?



Solution 1:[1]

The matplotlib legend is column-based. You cannot have legend entries spanning multiple columns. That said, it is of course possible to "center" an odd number of entries in a legend with an odd number of rows and similarly "center" an even number of entries in a legend with an even number of rows. This would be done by using empty artists at the outer positions.

enter image description hereenter image description here

import matplotlib.pyplot as plt
import numpy as np

ncols = 3  # or 4
nlines = 7 # or 10

x = np.linspace(0,19)
f = lambda x,p: np.sin(x*p)
h = [plt.plot(x,f(x,i/10.), label="Line {}".format(i))[0] for i in range(nlines)]
h.insert(nlines//ncols, plt.plot([],[],color=(0,0,0,0), label=" ")[0])
plt.legend(handles=h, ncol=ncols, framealpha=1)

plt.show()

If the number of columns is odd and the number of legend entries is even or vice versa the above is not possible. An option may be to use two different legends and position them below one another such that it looks like the entries of the last line are centered.

enter image description here

import matplotlib.pyplot as plt
import numpy as np

ncols = 3
nlines = 8

x = np.linspace(0,19)
f = lambda x,p: np.sin(x*p)
h = [plt.plot(x,f(x,i/10.), label="Line {}".format(i))[0] for i in range(nlines)]

kw = dict(framealpha=1, borderaxespad=0, bbox_to_anchor=(0.5,0.2), edgecolor="w")
leg1 = plt.legend(handles=h[:nlines//ncols*ncols], ncol=ncols, loc="lower center", **kw)
plt.gca().add_artist(leg1)
leg2 = plt.legend(handles=h[nlines//ncols*ncols:], ncol=nlines-nlines//ncols*ncols, 
                   loc="upper center", **kw)

plt.show()

The drawback here is that the legend has no border and both legends need to be positionned individually. To overcome this one would need to dig a little deeper and use some private attributed of the legend (caution, those may change from version to version without notice).
The idea can then be to remove the second legend once created and get its _legend_handle_box to place into the first legend.

import matplotlib.pyplot as plt
import numpy as np

ncols = 3
nlines = 8

x = np.linspace(0,19)
f = lambda x,p: np.sin(x*p)
h = [plt.plot(x,f(x,i/10.), label="Line {}".format(i))[0] for i in range(nlines)]

kw = dict(framealpha=1, bbox_to_anchor=(0.5,0.2))
leg1 = plt.legend(handles=h[:nlines//ncols*ncols], ncol=ncols, loc="lower center", **kw)
plt.gca().add_artist(leg1)
leg2 = plt.legend(handles=h[nlines//ncols*ncols:], ncol=nlines-nlines//ncols*ncols)

leg2.remove()
leg1._legend_box._children.append(leg2._legend_handle_box)
leg1._legend_box.stale = True

plt.show()

enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1