I am plotting the same type of information, but for different countries, with multiple subplots with matplotlib. That is, I have 9 plots on a 3×3 grid, all with the same for lines (of course, different values per line).
However, I have not figured out how to put a single legend (since all 9 subplots have the same lines) on the figure just once.
There is also a nice function get_legend_handles_labels() you can call on the last axis (if you iterate over them) that would collect everything you need from label= arguments:
For the automatic positioning of a single legend in a figure with many axes, like those obtained with subplots(), the following solution works really well:
With bbox_to_anchor and bbox_transform=plt.gcf().transFigure you are defining a new bounding box of the size of your figureto be a reference for loc. Using (0,-0.1,1,1) moves this bouding box slightly downwards to prevent the legend to be placed over other artists.
OBS: use this solution AFTER you use fig.set_size_inches() and BEFORE you use fig.tight_layout()
回答 3
您只需要在循环外询问一次图例。
例如,在这种情况下,我有4个子图,它们具有相同的行和一个图例。
from matplotlib.pyplot import*
ficheiros =['120318.nc','120319.nc','120320.nc','120321.nc']
fig = figure()
fig.suptitle('concentration profile analysis')for a in range(len(ficheiros)):# dados is here defined
level = dados.variables['level'][:]
ax = fig.add_subplot(2,2,a+1)
xticks(range(8),['0h','3h','6h','9h','12h','15h','18h','21h'])
ax.set_xlabel('time (hours)')
ax.set_ylabel('CONC ($\mu g. m^{-3}$)')for index in range(len(level)):
conc = dados.variables['CONC'][4:12,index]*1e9
ax.plot(conc,label=str(level[index])+'m')
dados.close()
ax.legend(bbox_to_anchor=(1.05,0), loc='lower left', borderaxespad=0.)# it will place the legend on the outer right-hand side of the last axes
show()
You just have to ask for the legend once, outside of your loop.
For example, in this case I have 4 subplots, with the same lines, and a single legend.
from matplotlib.pyplot import *
ficheiros = ['120318.nc', '120319.nc', '120320.nc', '120321.nc']
fig = figure()
fig.suptitle('concentration profile analysis')
for a in range(len(ficheiros)):
# dados is here defined
level = dados.variables['level'][:]
ax = fig.add_subplot(2,2,a+1)
xticks(range(8), ['0h','3h','6h','9h','12h','15h','18h','21h'])
ax.set_xlabel('time (hours)')
ax.set_ylabel('CONC ($\mu g. m^{-3}$)')
for index in range(len(level)):
conc = dados.variables['CONC'][4:12,index] * 1e9
ax.plot(conc,label=str(level[index])+'m')
dados.close()
ax.legend(bbox_to_anchor=(1.05, 0), loc='lower left', borderaxespad=0.)
# it will place the legend on the outer right-hand side of the last axes
show()
from numpy import linspace
import matplotlib.pyplot as plt
# Calling the axes.prop_cycle returns an itertoools.cycle
color_cycle = plt.rcParams['axes.prop_cycle']()# I need some curves to plot
x = linspace(0,1,51)
f1 = x*(1-x); lab1 ='x - x x'
f2 =0.25-f1 ; lab2 ='1/4 - x + x x'
f3 = x*x*(1-x); lab3 ='x x - x x x'
f4 =0.25-f3 ; lab4 ='1/4 - x x + x x x'# let's plot our curves (note the use of color cycle, otherwise the curves colors in# the two subplots will be repeated and a single legend becomes difficult to read)
fig,(a13, a24)= plt.subplots(2)
a13.plot(x, f1, label=lab1,**next(color_cycle))
a13.plot(x, f3, label=lab3,**next(color_cycle))
a24.plot(x, f2, label=lab2,**next(color_cycle))
a24.plot(x, f4, label=lab4,**next(color_cycle))# so far so good, now the trick
lines_labels =[ax.get_legend_handles_labels()for ax in fig.axes]
lines, labels =[sum(lol,[])for lol in zip(*lines_labels)]# finally we invoke the legend (that you probably would like to customize...)
fig.legend(lines, labels)
plt.show()
两条线
lines_labels =[ax.get_legend_handles_labels()for ax in fig.axes]
lines, labels =[sum(lol,[])for lol in zip(*lines_labels)]
值得解释-为此,我将棘手的部分封装在一个函数中,仅用4行代码,但注释严重
def fig_legend(fig,**kwdargs):# generate a sequence of tuples, each contains# - a list of handles (lohand) and# - a list of labels (lolbl)
tuples_lohand_lolbl =(ax.get_legend_handles_labels()for ax in fig.axes)# e.g. a figure with two axes, ax0 with two curves, ax1 with one curve# yields: ([ax0h0, ax0h1], [ax0l0, ax0l1]) and ([ax1h0], [ax1l0])# legend needs a list of handles and a list of labels, # so our first step is to transpose our data,# generating two tuples of lists of homogeneous stuff(tolohs), i.e# we yield ([ax0h0, ax0h1], [ax1h0]) and ([ax0l0, ax0l1], [ax1l0])
tolohs = zip(*tuples_lohand_lolbl)# finally we need to concatenate the individual lists in the two# lists of lists: [ax0h0, ax0h1, ax1h0] and [ax0l0, ax0l1, ax1l0]# a possible solution is to sum the sublists - we use unpacking
handles, labels =(sum(list_of_lists,[])for list_of_lists in tolohs)# call fig.legend with the keyword arguments, return the legend objectreturn fig.legend(handles, labels,**kwdargs)
I have noticed that no answer display an image with a single legend referencing many curves in different subplots, so I have to show you one… to make you curious…
Now, you want to look at the code, don’t you?
from numpy import linspace
import matplotlib.pyplot as plt
# Calling the axes.prop_cycle returns an itertoools.cycle
color_cycle = plt.rcParams['axes.prop_cycle']()
# I need some curves to plot
x = linspace(0, 1, 51)
f1 = x*(1-x) ; lab1 = 'x - x x'
f2 = 0.25-f1 ; lab2 = '1/4 - x + x x'
f3 = x*x*(1-x) ; lab3 = 'x x - x x x'
f4 = 0.25-f3 ; lab4 = '1/4 - x x + x x x'
# let's plot our curves (note the use of color cycle, otherwise the curves colors in
# the two subplots will be repeated and a single legend becomes difficult to read)
fig, (a13, a24) = plt.subplots(2)
a13.plot(x, f1, label=lab1, **next(color_cycle))
a13.plot(x, f3, label=lab3, **next(color_cycle))
a24.plot(x, f2, label=lab2, **next(color_cycle))
a24.plot(x, f4, label=lab4, **next(color_cycle))
# so far so good, now the trick
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
# finally we invoke the legend (that you probably would like to customize...)
fig.legend(lines, labels)
plt.show()
The two lines
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
deserve an explanation — to this aim I have encapsulated the tricky part in a function, just 4 lines of code but heavily commented
def fig_legend(fig, **kwdargs):
# generate a sequence of tuples, each contains
# - a list of handles (lohand) and
# - a list of labels (lolbl)
tuples_lohand_lolbl = (ax.get_legend_handles_labels() for ax in fig.axes)
# e.g. a figure with two axes, ax0 with two curves, ax1 with one curve
# yields: ([ax0h0, ax0h1], [ax0l0, ax0l1]) and ([ax1h0], [ax1l0])
# legend needs a list of handles and a list of labels,
# so our first step is to transpose our data,
# generating two tuples of lists of homogeneous stuff(tolohs), i.e
# we yield ([ax0h0, ax0h1], [ax1h0]) and ([ax0l0, ax0l1], [ax1l0])
tolohs = zip(*tuples_lohand_lolbl)
# finally we need to concatenate the individual lists in the two
# lists of lists: [ax0h0, ax0h1, ax1h0] and [ax0l0, ax0l1, ax1l0]
# a possible solution is to sum the sublists - we use unpacking
handles, labels = (sum(list_of_lists, []) for list_of_lists in tolohs)
# call fig.legend with the keyword arguments, return the legend object
return fig.legend(handles, labels, **kwdargs)
PS I recognize that sum(list_of_lists, []) is a really inefficient method to flatten a list of lists but ① I love its compactness, ② usually is a few curves in a few subplots and ③ Matplotlib and efficiency? ;-)
Important Update
If you want to stick with the official Matplotlib API my answer above is perfect, really.
On the other hand, if you don’t mind using a private method of the matplotlib.legend module … it’s really much much much easier
from matplotlib.legend import _get_legend_handles_labels
...
fig.legend(*_get_legend_handles_and_labels(fig.axes), ...)
A complete explanation can be found in the source code of Axes.get_legend_handles_labels in .../matplotlib/axes/_axes.py
While rather late to the game, I’ll give another solution here as this is still one of the first links to show up on google. Using matplotlib 2.2.2, this can be achieved using the gridspec feature. In the example below the aim is to have four subplots arranged in a 2×2 fashion with the legend shown at the bottom. A ‘faux’ axis is created at the bottom to place the legend in a fixed spot. The ‘faux’ axis is then turned off so only the legend shows. Result: https://i.stack.imgur.com/5LUWM.png.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
#Gridspec demo
fig = plt.figure()
fig.set_size_inches(8,9)
fig.set_dpi(100)
rows = 17 #the larger the number here, the smaller the spacing around the legend
start1 = 0
end1 = int((rows-1)/2)
start2 = end1
end2 = int(rows-1)
gspec = gridspec.GridSpec(ncols=4, nrows=rows)
axes = []
axes.append(fig.add_subplot(gspec[start1:end1,0:2]))
axes.append(fig.add_subplot(gspec[start2:end2,0:2]))
axes.append(fig.add_subplot(gspec[start1:end1,2:4]))
axes.append(fig.add_subplot(gspec[start2:end2,2:4]))
axes.append(fig.add_subplot(gspec[end2,0:4]))
line, = axes[0].plot([0,1],[0,1],'b') #add some data
axes[-1].legend((line,),('Test',),loc='center') #create legend on bottommost axis
axes[-1].set_axis_off() #don't show bottommost axis
fig.tight_layout()
plt.show()
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
labels =['Red Bar','Magenta Bar','Cyan Bar','Black Bar']###################################### insert code for the subplots here ####################################### now, create an artist for each color
red_patch = mpatches.Patch(facecolor='r', edgecolor='#000000')#this will create a red bar with black borders, you can leave out edgecolor if you do not want the borders
black_patch = mpatches.Patch(facecolor='k', edgecolor='#000000')
magenta_patch = mpatches.Patch(facecolor='m', edgecolor='#000000')
cyan_patch = mpatches.Patch(facecolor='c', edgecolor='#000000')
fig.legend(handles =[red_patch, magenta_patch, cyan_patch, black_patch],labels=labels,
loc="center right",
borderaxespad=0.1)
plt.subplots_adjust(right=0.85)#adjust the subplot to the right for the legend
if you are using subplots with bar charts, with different colour for each bar. it may be faster to create the artefacts yourself using mpatches
Say you have four bars with different colours as rmck you can set the legend as follows
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
labels = ['Red Bar', 'Magenta Bar', 'Cyan Bar', 'Black Bar']
#####################################
# insert code for the subplots here #
#####################################
# now, create an artist for each color
red_patch = mpatches.Patch(facecolor='r', edgecolor='#000000') #this will create a red bar with black borders, you can leave out edgecolor if you do not want the borders
black_patch = mpatches.Patch(facecolor='k', edgecolor='#000000')
magenta_patch = mpatches.Patch(facecolor='m', edgecolor='#000000')
cyan_patch = mpatches.Patch(facecolor='c', edgecolor='#000000')
fig.legend(handles = [red_patch, magenta_patch, cyan_patch, black_patch],labels=labels,
loc="center right",
borderaxespad=0.1)
plt.subplots_adjust(right=0.85) #adjust the subplot to the right for the legend
This answer is a complement to @Evert’s on the legend position.
My first try on @Evert’s solution failed due to overlaps of the legend and the subplot’s title.
In fact, the overlaps are caused by fig.tight_layout(), which changes the subplots’ layout without considering the figure legend. However, fig.tight_layout() is necessary.
In order to avoid the overlaps, we can tell fig.tight_layout() to leave spaces for the figure’s legend by fig.tight_layout(rect=(0,0,1,0.9)).
labels_handles ={
label: handle for ax in fig.axes for handle, label in zip(*ax.get_legend_handles_labels())}
fig.legend(
labels_handles.values(),
labels_handles.keys(),
loc="upper center",
bbox_to_anchor=(0.5,0),
bbox_transform=plt.gcf().transFigure,)