将 matplotlib 图例移到轴外使其被图形框截断

Posted

技术标签:

【中文标题】将 matplotlib 图例移到轴外使其被图形框截断【英文标题】:Moving matplotlib legend outside of the axis makes it cutoff by the figure box 【发布时间】:2012-04-23 12:11:18 【问题描述】:

我熟悉以下问题:

Matplotlib savefig with a legend outside the plot

How to put the legend out of the plot

似乎这些问题的答案能够摆弄轴的精确缩小以使图例适合。

然而,缩小坐标轴并不是一个理想的解决方案,因为它会使数据变得更小,从而实际上更难以解释;特别是当它很复杂并且有很多事情发生时......因此需要一个大的传说

文档中的复杂图例示例说明了这样做的必要性,因为图中的图例实际上完全掩盖了多个数据点。

http://matplotlib.sourceforge.net/users/legend_guide.html#legend-of-complex-plots

我希望能够动态扩展图形框的大小以适应扩展图形图例。

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))
ax.grid('on')

请注意最终标签“反棕褐色”实际上是如何在图形框之外的(并且看起来很严重 - 不是出版质量!)

最后,有人告诉我这是 R 和 LaTeX 中的正常行为,所以我有点困惑为什么这在 python 中如此困难......有历史原因吗? Matlab 在这件事上是否同样糟糕?

我在 pastebin http://pastebin.com/grVjc007 上有这个代码的(只是稍微)更长的版本

【问题讨论】:

至于为什么是因为 matplotlib 面向交互式绘图,而 R 等则不是。 (是的,在这种特殊情况下,Matlab “同样糟糕”。)要正确地做到这一点,您需要担心每次调整图形大小、缩放或更新图例的位置时调整轴的大小。 (实际上,这意味着每次绘制绘图时都要检查,这会导致速度变慢。) Ggplot 等是静态的,所以这就是为什么它们默认情况下倾向于这样做,而 matplotlib 和 matlab 不这样做。话虽如此,tight_layout() 应该被更改以考虑传说。 我也在 matplotlib 用户邮件列表中讨论这个问题。所以我建议将 savefig 行调整为: fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox='tight') 我知道 matplotlib 喜欢吹嘘一切都在用户的控制之下,但是这整个带有传说的东西实在是太好了。如果我把图例放在外面,我显然希望它仍然可见。窗口应该自行缩放以适应,而不是造成这种巨大的重新缩放麻烦。至少应该有一个默认的 True 选项来控制这种自动缩放行为。强迫用户通过大量的重新渲染来尝试以控制的名义获得正确的比例数字会起到相反的作用。 【参考方案1】:

抱歉 EMS,但实际上我刚刚收到了来自 matplotlib 邮件列表的另一个回复(感谢 Benjamin Root)。

我正在寻找的代码正在将 savefig 调用调整为:

fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox_inches='tight')
#Note that the bbox_extra_artists must be an iterable

这显然类似于调用tight_layout,但您允许savefig 在计算中考虑额外的艺术家。实际上,这确实根据需要调整了图形框的大小。

import matplotlib.pyplot as plt
import numpy as np

plt.gcf().clear()
x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
handles, labels = ax.get_legend_handles_labels()
lgd = ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5,-0.1))
text = ax.text(-0.2,1.05, "Aribitrary text", transform=ax.transAxes)
ax.set_title("Trigonometry")
ax.grid('on')
fig.savefig('samplefigure', bbox_extra_artists=(lgd,text), bbox_inches='tight')

这会产生:

[编辑] 这个问题的目的是完全避免使用任意文本的任意坐标位置,因为这是这些问题的传统解决方案。尽管如此,最近的许多编辑都坚持将它们放入,通常以导致代码出错的方式。我现在已经解决了这些问题并整理了任意文本,以展示如何在 bbox_extra_artists 算法中考虑这些问题。

【讨论】:

/!\ 似乎只有在 matplotlib >= 1.0 后才有效(Debian 挤压有 0.99,这不起作用) 无法让它工作:(我将 lgd 传递给 savefig 但它仍然没有调整大小。问题可能是我没有使用子图。 啊!我只需要像你一样使用 bbox_inches = "tight" 。谢谢! 这很好,但当我尝试plt.show() 时,我的身材仍然会被削减。有什么解决办法吗? It does not work if use fig.legend() method ,真奇怪。【参考方案2】:

补充:我发现了一些可以立即解决问题的方法,但下面的其余代码也提供了替代方法。

使用subplots_adjust() 函数将子图的底部向上移动:

fig.subplots_adjust(bottom=0.2) # <-- Change the 0.02 to work for your plot.

然后在 legend 命令的图例bbox_to_anchor 部分中使用偏移量,以获取所需的图例框。设置figsize 和使用subplots_adjust(bottom=...) 的某种组合应该会为您生成高质量的图。

替代方案: 我只是换了一行:

fig = plt.figure(1)

到:

fig = plt.figure(num=1, figsize=(13, 13), dpi=80, facecolor='w', edgecolor='k')

改变了

lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))

lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,-0.02))

它在我的屏幕(24 英寸 CRT 显示器)上显示良好。

这里figsize=(M,N) 将图形窗口设置为 M 英寸乘 N 英寸。只是玩这个,直到它看起来适合你。将其转换为更具可扩展性的图像格式并在必要时使用 GIMP 进行编辑,或者在包含图形时使用 LaTeX viewport 选项进行裁剪。

【讨论】:

这似乎是目前最好的解决方案,尽管它仍然需要“播放直到看起来不错”,这对于自动报告生成器来说不是一个好的解决方案。我实际上已经使用了这个解决方案,真正的问题是 matplotlib 不会动态补偿位于轴的 bbox 之外的图例。正如@Joe 所说,tight_layout 应该考虑更多的功能,而不仅仅是轴、标题和标签。我可能会将其添加为 matplotlib 上的功能请求。 也适用于我获得足够大的图片以适应之前被切断的 xlabels here 是带有来自 matplotlib.org 的示例代码的文档【参考方案3】:

这是另一个非常手动的解决方案。您可以定义轴的大小并相应地考虑填充(包括图例和刻度线)。希望它对某人有用。

示例(轴大小相同!):

代码:

#==================================================
# Plot table

colmap = [(0,0,1) #blue
         ,(1,0,0) #red
         ,(0,1,0) #green
         ,(1,1,0) #yellow
         ,(1,0,1) #magenta
         ,(1,0.5,0.5) #pink
         ,(0.5,0.5,0.5) #gray
         ,(0.5,0,0) #brown
         ,(1,0.5,0) #orange
         ]


import matplotlib.pyplot as plt
import numpy as np

import collections
df = collections.OrderedDict()
df['labels']        = ['GWP100a\n[kgCO2eq]\n\nasedf\nasdf\nadfs','human\n[pts]','ressource\n[pts]'] 
df['all-petroleum long name'] = [3,5,2]
df['all-electric']  = [5.5, 1, 3]
df['HEV']           = [3.5, 2, 1]
df['PHEV']          = [3.5, 2, 1]

numLabels = len(df.values()[0])
numItems = len(df)-1
posX = np.arange(numLabels)+1
width = 1.0/(numItems+1)

fig = plt.figure(figsize=(2,2))
ax = fig.add_subplot(111)
for iiItem in range(1,numItems+1):
  ax.bar(posX+(iiItem-1)*width, df.values()[iiItem], width, color=colmap[iiItem-1], label=df.keys()[iiItem])
ax.set(xticks=posX+width*(0.5*numItems), xticklabels=df['labels'])

#--------------------------------------------------
# Change padding and margins, insert legend

fig.tight_layout() #tight margins
leg = ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0)
plt.draw() #to know size of legend

padLeft   = ax.get_position().x0 * fig.get_size_inches()[0]
padBottom = ax.get_position().y0 * fig.get_size_inches()[1]
padTop    = ( 1 - ax.get_position().y0 - ax.get_position().height ) * fig.get_size_inches()[1]
padRight  = ( 1 - ax.get_position().x0 - ax.get_position().width ) * fig.get_size_inches()[0]
dpi       = fig.get_dpi()
padLegend = ax.get_legend().get_frame().get_width() / dpi 

widthAx = 3 #inches
heightAx = 3 #inches
widthTot = widthAx+padLeft+padRight+padLegend
heightTot = heightAx+padTop+padBottom

# resize ipython window (optional)
posScreenX = 1366/2-10 #pixel
posScreenY = 0 #pixel
canvasPadding = 6 #pixel
canvasBottom = 40 #pixel
ipythonWindowSize = '0x1+2+3'.format(int(round(widthTot*dpi))+2*canvasPadding
                                            ,int(round(heightTot*dpi))+2*canvasPadding+canvasBottom
                                            ,posScreenX,posScreenY)
fig.canvas._tkcanvas.master.geometry(ipythonWindowSize) 
plt.draw() #to resize ipython window. Has to be done BEFORE figure resizing!

# set figure size and ax position
fig.set_size_inches(widthTot,heightTot)
ax.set_position([padLeft/widthTot, padBottom/heightTot, widthAx/widthTot, heightAx/heightTot])
plt.draw()
plt.show()
#--------------------------------------------------
#==================================================

【讨论】:

这对我不起作用,直到我将第一个 plt.draw() 更改为 ax.figure.canvas.draw()。我不知道为什么,但在此更改之前,图例大小没有更新。 如果您尝试在 GUI 窗口中使用它,您需要将 fig.set_size_inches(widthTot,heightTot) 更改为 fig.set_size_inches(widthTot,heightTot, forward=True)

以上是关于将 matplotlib 图例移到轴外使其被图形框截断的主要内容,如果未能解决你的问题,请参考以下文章

在 matplotlib 和 pandas 中组合和重新定位两个图表的图例的困难

Matplotlib:在图例中移动标记位置

如何使用选择图例(matplotlib)自动缩放图形?

来自 pandas 数据框的散点图中的 Matplotlib 图例

Matplotlib 可视化之图例与标签高级应用

如何在直方图的 matplotlib 图例中制作线条而不是框/矩形