将 matplotlib 图例移出轴会使其被图形框截断
- 2025-02-10 08:57:00
- admin 原创
- 59
问题描述:
我熟悉以下问题:
Matplotlib savefig 在图表外带有图例
如何将传说从故事情节中剔除
这些问题的答案似乎能够通过摆弄轴的精确收缩来适应图例。
然而,缩小轴并不是一个理想的解决方案,因为它会使数据变小,从而实际上更难以解释;特别是当它很复杂,有很多事情发生时……因此需要一个大的图例
文档中复杂图例的示例证明了这种需要,因为其图中的图例实际上完全遮挡了多个数据点。
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
解决方案 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 算法中考虑这些。
[编辑] 下面的一些评论指出,自 2019 年以来,该命令已简化。
plt.savefig('x.png', bbox_inches='tight') 就足够了。 谢谢分享。 – mateuszb 2019 年 6 月 27 日
解决方案 2:
补充:我发现了一些可以立即解决问题的东西,但是下面的其余代码也提供了另一种选择。
使用subplots_adjust()
函数将子图的底部向上移动:
fig.subplots_adjust(bottom=0.2) # <-- Change the 0.02 to work for your plot.
然后使用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 进行编辑,或者viewport
在包含图形时使用 LaTeX 选项进行裁剪。
解决方案 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
[kgCO2eq]
asedf
asdf
adfs','human
[pts]','ressource
[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 = '{0}x{1}+{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()
#--------------------------------------------------
#==================================================
解决方案 4:
我尝试了一个非常简单的方法,只是让图形稍微宽一点:
fig, ax = plt.subplots(1, 1, figsize=(a, b))
调整a
为b
适当的值,以便图例包含在图中
解决方案 5:
由于谷歌引导我到这里回答类似的问题,所以值得注意的是,现在你只需要使用plt.savefig('myplot.png', bbox_inches='tight')
扩展画布来适应图例。
这里的大多数答案都已过时。
解决方案 6:
我同意 John Thomas 的观点,如果你使用基于 plt 的图形,那么你可能需要的是:plt.savefig('myplot.png', bbox_inches='tight')
解决方案 7:
该layout="constrained"
选项(自 Matplotlib 3.7.0 起可用)允许相当直接的实现:
import matplotlib.pyplot as plt
import numpy as np
if __name__ == "__main__":
x = np.arange(-2 * np.pi, 2 * np.pi, 0.1)
fig, ax = plt.subplots(1, 1, layout="constrained")
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()
ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, -0.1))
text = ax.text(-0.2, 1.05, "Arbitrary text", transform=ax.transAxes)
ax.set_title("Trigonometry")
ax.grid('on')
fig.savefig('samplefigure.png')
fig.show()
其中一个好处(与jonathanbsyd的原始答案相比)是fig.show()
效果同样好。