如何使用 matplotlib 为许多子图制作一个图例?

Posted

技术标签:

【中文标题】如何使用 matplotlib 为许多子图制作一个图例?【英文标题】:how do I make a single legend for many subplots with matplotlib? 【发布时间】:2012-04-07 17:35:13 【问题描述】:

我正在绘制相同类型的信息,但针对不同的国家,使用 matplotlib 绘制多个子图。也就是说,我在 3x3 网格上有 9 个图,所有的线都相同(当然,每条线的值不同)。

但是,我还没有想出如何将一个图例(因为所有 9 个子图都有相同的线)放在图上一次。

我该怎么做?

【问题讨论】:

【参考方案1】:

还有一个不错的函数get_legend_handles_labels(),您可以在最后一个轴上调用(如果您遍历它们),它将从label= 参数中收集您需要的所有内容:

handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')

【讨论】:

这应该是最佳答案。 如何删除子图的图例? 只是为了添加到这个很好的答案。如果您的地块上有辅助 y 轴并且需要合并它们,请使用:handles, labels = [(a + b) for a, b in zip(ax1.get_legend_handles_labels(), ax2.get_legend_handles_labels())] plt.gca().get_legend_handles_labels() 为我工作。 对于熊猫绘图员,请在绘图函数中传递 legend=0 以隐藏子绘图中的图例。【参考方案2】:

figlegend 可能是你要找的东西:http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.figlegend

此处示例:http://matplotlib.org/examples/pylab_examples/figlegend_demo.html

另一个例子:

plt.figlegend( lines, labels, loc = 'lower center', ncol=5, labelspacing=0. )

或:

fig.legend( lines, labels, loc = (0.5, 0), ncol=5 )

【讨论】:

我知道要放入图例中的行,但是如何让 lines 变量放入 legend 的参数中? @patapouf_ai lines 是从axes.plot() 返回的结果列表(即,每个axes.plot 或类似例程都返回一个“行”)。另请参阅链接示例。【参考方案3】:

TL;DR

lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
fig.legend(lines, labels)

我注意到没有答案会显示带有单个图例的图像,该图例引用了不同子图中的许多曲线,所以我必须向您展示一个...让您好奇...

现在,您想要查看代码,不是吗?

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 object

    return fig.legend(handles, labels, **kwdargs)

PS 我承认sum(list_of_lists, []) 是一种非常低效的方法来展平列表列表,但①我喜欢它的紧凑性,②通常是几个子图中的几条曲线,③ Matplotlib 和效率? ;-)


重要更新

如果你想坚持使用官方的 Matplotlib API,我上面的回答是完美的,真的。

另一方面,如果您不介意使用 matplotlib.legend 模块的私有方法...确实要容易得多

from matplotlib.legend import _get_legend_handles_labels
...

fig.legend(*_get_legend_handles_and_labels(fig.axes), ...)

完整的解释可以在Axes.get_legend_handles_labels的源代码中找到.../matplotlib/axes/_axes.py

【讨论】:

带有sum(lol, ...) 的行给了我一个TypeError: 'list' object cannot be interpreted as an integer(使用matplotlib 3.3.4 版) @duff18 看起来您忘记了sum 的可选参数,即空列表[]。请参阅sum documentation 了解说明。 不,我只是复制粘贴了您的代码。为了更清楚,给出错误的行是lines, labels = [sum(lol, []) for lol in zip(*lines_labels)] @duff18 鉴于所提供信息的稀缺性,我没有立即解释。我只能建议您在一个新问题中提供所有相关的上下文(一个可重现的示例和一个完整的回溯)。如果您决定提出新问题,请给我留言。 @duff18 我刚刚检查了 Matplotlib 3.3.4,我惊讶地发现一切都很好,就像 2019 年 8 月我写答案时一样。我不知道您的情况出了什么问题,我可以更新我的建议,请发布一个新问题,详细说明您的情况。如果您 p​​ing 我,我很乐意为您提供帮助。【参考方案4】:

对于在具有多个轴的 figure 中自动定位单个图例,例如使用 subplots() 获得的图例,以下解决方案非常有效:

plt.legend( lines, labels, loc = 'lower center', bbox_to_anchor = (0,-0.1,1,1),
            bbox_transform = plt.gcf().transFigure )

使用bbox_to_anchorbbox_transform=plt.gcf().transFigure,您将定义一个与figure 大小相同的新边界框,作为loc 的参考。使用(0,-0.1,1,1) 将这个边界框稍微向下移动,以防止图例被放置在其他艺术家之上。

OBS:在使用fig.set_size_inches() 之后和使用fig.tight_layout() 之前使用此解决方案

【讨论】:

或者简单的loc='upper center', bbox_to_anchor=(0.5, 0), bbox_transform=plt.gcf().transFigure,肯定不会重叠。 我仍然不确定为什么,但 Evert 的解决方案对我不起作用——传说一直被切断。这个解决方案(连同 davor 的评论)工作得非常干净——图例按预期放置并且完全可见。谢谢!【参考方案5】:

您只需要在循环之外询问一次图例。

例如,在这种情况下,我有 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()

【讨论】:

figlegend,正如 Evert 所建议的那样,似乎是一个更好的解决方案;) fig.legend() 的问题是它需要识别所有线(图)...因为,对于每个子图,我使用循环来生成线,这是我想出的唯一解决方案解决这个问题的方法是在第二个循环之前创建一个空列表,然后在创建行时附加这些行...然后我将此列表用作fig.legend() 函数的参数。 类似问题here 那里的dados 是什么? @Shyamkkhadka,在我的原始脚本中 dados 是来自 netCDF4 文件的数据集(对于列表 ficheiros 中定义的每个文件)。在每个循环中,读取一个不同的文件并在图中添加一个子图。【参考方案6】:

如果您使用带有条形图的子图,则每个条形具有不同的颜色。使用mpatches自己创建人工制品可能会更快

假设您有四个不同颜色的条形如r m c k 您可以设置图例如下

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

【讨论】:

+1 最好的!我以这种方式使用它直接添加到plt.legend 为我的所有子图创建一个图例 自动手柄和手工标签结合起来更快:handles, _ = plt.gca().get_legend_handles_labels(),然后fig.legend(handles, labels)【参考方案7】:

这个答案是对@Evert 在图例位置上的补充。

由于图例和子情节的标题重叠,我第一次尝试 @Evert 的解决方案失败了。

实际上,重叠是由fig.tight_layout()引起的,它改变了子图的布局而不考虑图例。但是,fig.tight_layout() 是必需的。

为了避免重叠,我们可以通过fig.tight_layout(rect=(0,0,1,0.9))告诉fig.tight_layout()为图例留出空格。

Description of tight_layout() parameters.

【讨论】:

【参考方案8】:

虽然游戏比较晚,但我将在这里提供另一个解决方案,因为这仍然是最早出现在 google 上的链接之一。使用 matplotlib 2.2.2,这可以使用 gridspec 功能来实现。在下面的示例中,目标是以 2x2 的方式排列四个子图,图例显示在底部。在底部创建一个“人造”轴,以将图例放置在固定位置。然后关闭“人造”轴,因此仅显示图例。结果: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()

【讨论】:

【参考方案9】:

以@gboffi 和 Ben Usman 的回答为基础:

在不同的子图中有不同的线条,颜色和标签相同的情况下,可以按照以下方式做一些事情

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,
)

【讨论】:

【参考方案10】:

以上所有内容都超出了我的想象,在我的编码生涯中,我刚刚添加了另一个称为补丁的 matplotlib 方面:

import matplotlib.patches as mpatches

first_leg = mpatches.Patch(color='red', label='1st plot')
second_leg = mpatches.Patch(color='blue', label='2nd plot')
thrid_leg = mpatches.Patch(color='green', label='3rd plot')
plt.legend(handles=[first_leg ,second_leg ,thrid_leg ])

补丁方面将我需要的所有数据放在我的最终图上(这是一个线图,将 3 个不同的线图组合在 jupyter notebook 的同一个单元格中)

结果(我根据自己的传说改了名字)

【讨论】:

以上是关于如何使用 matplotlib 为许多子图制作一个图例?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 matplotlib 为所有子图设置默认颜色循环?

使用 matplotlib 中的许多子图改进子图大小/间距

如何在 Matplotlib 中为子图添加标题

单击 matplotlib 中的步骤图子图点时如何使标签出现(或可能是 plotly)?

与 matplotlib 子图不一致的刻度标签字体

matplotlib 的大量子图