【发布时间】:2010-12-26 16:09:53
【问题描述】:
我正在开发一个 Web 应用程序,并希望在页面的不同位置显示一个图形及其图例。这意味着我需要将图例保存为单独的 png 文件。这在 Matplotlib 中是否可能以一种或多或少直接的方式实现?
【问题讨论】:
标签: python matplotlib
我正在开发一个 Web 应用程序,并希望在页面的不同位置显示一个图形及其图例。这意味着我需要将图例保存为单独的 png 文件。这在 Matplotlib 中是否可能以一种或多或少直接的方式实现?
【问题讨论】:
标签: python matplotlib
您可以使用bbox_inches 到fig.savefig 的参数将图形的保存区域限制在图例的边界框内。下面是函数的版本,您可以使用要保存为参数的图例简单地调用该函数。您可以在此处使用原始图中创建的图例(然后将其删除,legend.remove()),也可以为图例创建一个新图并直接使用该函数。
如果要保存完整的图例,提供给bbox_inches 参数的边界框将只是图例的转换边界框。如果图例周围没有边框,这很有效。
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=False)
def export_legend(legend, filename="legend.png"):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
如果图例周围有边框,则上述解决方案可能不是最理想的。在这种情况下,将边界框扩展一些像素以将边框完全包含在内是有意义的。
import numpy as np
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=True)
def export_legend(legend, filename="legend.png", expand=[-5,-5,5,5]):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent()
bbox = bbox.from_extents(*(bbox.extents + np.array(expand)))
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
【讨论】:
可以使用axes.get_legend_handles_labels 从一个axes 对象中获取图例句柄和标签,并使用它们将它们添加到不同图形中的轴。
# create a figure with one subplot
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1,2,3,4,5], [1,2,3,4,5], 'r', label='test')
# save it *without* adding a legend
fig.savefig('image.png')
# then create a new image
# adjust the figure size as necessary
figsize = (3, 3)
fig_leg = plt.figure(figsize=figsize)
ax_leg = fig_leg.add_subplot(111)
# add the legend from the previous axes
ax_leg.legend(*ax.get_legend_handles_labels(), loc='center')
# hide the axes frame and the x/y labels
ax_leg.axis('off')
fig_leg.savefig('legend.png')
如果出于某种原因您只想隐藏坐标轴标签,您可以使用:
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
或者,如果出于某种奇怪的原因,您想隐藏坐标轴框架,但不隐藏您可以使用的坐标轴标签:
ax.set_frame_on(False)
ps:这个答案改编自我对duplicate question的回答
【讨论】:
ax.fill_between 失败。例如,如果我在第一个 fig.save 调用之前插入行 ax.fill_between([1,2,3,4,5], [2,3,4,5,6], y2=[1,2,3,4,5], label='test2'),那么 ax_leg.legend 会抛出以下错误:RuntimeError: Can not put single artist in more than one figure 你能想到这种情况的解决方案吗?
灵感来自 Maxim 和 ImportanceOfBeingErnest 的答案,
def export_legend(ax, filename="legend.pdf"):
fig2 = plt.figure()
ax2 = fig2.add_subplot()
ax2.axis('off')
legend = ax2.legend(*ax.get_legend_handles_labels(), frameon=False, loc='lower center', ncol=10,)
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
这允许我将图例水平保存在单独的文件中。举个例子
【讨论】:
这可以工作:
import pylab
fig = pylab.figure()
figlegend = pylab.figure(figsize=(3,2))
ax = fig.add_subplot(111)
lines = ax.plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
figlegend.legend(lines, ('one', 'two'), 'center')
fig.show()
figlegend.show()
figlegend.savefig('legend.png')
【讨论】:
figlegend.legend(ax.get_legend_handles_labels()[0], ax[get_legend_handles_labels()[1]) 例如如果在 for 循环中将多个绘图添加到 ax。
在 2020 年 11 月,我几乎尝试了这篇文章中的所有内容,但没有一个对我有用。经过一段时间的挣扎,我找到了一个我想要的解决方案。
假设您想分别绘制如下图所示的图形和图例(显然我没有足够的声誉在帖子中嵌入图片;点击链接查看图片)。
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots()
ax.plot([1, 2, 3], label="test1")
ax.plot([3, 2, 1], label="test2")
ax.legend()
您可以将图形和图例分隔在两个不同的斧头对象中:
fig, [ax1, ax2] = plt.subplots(1, 2)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
您可以轻松控制图例的位置:
fig, [ax1, ax2] = plt.subplots(2, 1)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
【讨论】:
我发现最简单的方法就是创建你的图例,然后用plt.gca().set_axis_off() 关闭axis:
# Create a color palette
palette = dict(zip(['one', 'two'], ['b', 'g']))
# Create legend handles manually
handles = [mpl.patches.Patch(color=palette[x], label=x) for x in palette.keys()]
# Create legend
plt.legend(handles=handles)
# Get current axes object and turn off axis
plt.gca().set_axis_off()
plt.show()
【讨论】:
我无法在现有答案中准确找到我想要的,所以我实现了它。它想生成一个独立的图例,没有任何附加的图形,也没有视觉上的“故障”。我想出了这个:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
palette = dict(zip(['one', 'two', 'tree', 'four'], ['b', 'g', 'r', 'k']))
def export_legend(palette, dpi="figure", filename="legend.png"):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi=dpi, transparent=True, bbox_inches=bbox)
# Delete the legend along with its temporary figure
plt.close(fig)
export_legend(palette, dpi=400)
请注意,周围的背景是透明的,因此在图形顶部添加图例不应在角落出现白色“毛刺”,也不应出现裁剪边框的问题。
如果你不想保存磁盘映像,这里有窍门!
DPI = 400
def export_legend(palette):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox_inches = bbox.transformed(fig.dpi_scale_trans.inverted())
bbox_inches = bbox.from_extents(np.round(bbox_inches.extents * 400) / 400)
io_buf = io.BytesIO()
fig.savefig(io_buf, format='rgba', dpi=DPI, transparent=True, bbox_inches=bbox_inches)
io_buf.seek(0)
img_raw = io_buf.getvalue()
img_size = (np.asarray(bbox_inches.bounds)[2:] * DPI).astype(int)
# Delete the legend along with its temporary figure
plt.close(fig)
return img_raw, img_size
可以使用PIL 或任何处理原始缓冲区的方法直接读取原始缓冲区。
【讨论】:
使用pylab.figlegend(..) 和get_legend_handles_labels(..):
import pylab, numpy
x = numpy.arange(10)
# create a figure for the data
figData = pylab.figure()
ax = pylab.gca()
for i in xrange(3):
pylab.plot(x, x * (i+1), label='line %d' % i)
# create a second figure for the legend
figLegend = pylab.figure(figsize = (1.5,1.3))
# produce a legend for the objects in the other figure
pylab.figlegend(*ax.get_legend_handles_labels(), loc = 'upper left')
# save the two figures to files
figData.savefig("plot.png")
figLegend.savefig("legend.png")
虽然以自动方式正确获取图例的大小可能会很棘手。
【讨论】:
我想为自定义图例的特定情况添加一点贡献,例如:https://matplotlib.org/3.1.1/gallery/text_labels_and_annotations/custom_legends.html
在这种情况下,您可能必须采用不同的方法。我遇到过这个问题,上面的答案对我不起作用。
下面的代码设置图例。
import cmocean
import matplotlib
from matplotlib.lines import Line2D
lightcmap = cmocean.tools.lighten(cmo.solar, 0.7)
custom_legend = []
custom_legend_strings=['no impact - high confidence', 'no impact - low confidence', 'impact - low confidence', 'impact - high confidence']
for nbre_classes in range(len(custom_legend_strings)):
custom_legend.append(Line2D([0], [0], color=lightcmap(nbre_classes/len(custom_legend_strings)), lw=4))
我认为因为这种传说是附在轴上的,所以需要一个小技巧:
以大字体居中图例,使其占用大部分可用空间,并且不擦除但将轴设置为关闭:
fig,ax = plt.subplots(figsize=(10,10))
ax.legend(custom_legend,custom_legend_strings, loc = 10, fontsize=30)
plt.axis('off')
fig.savefig('legend.png', bbox_inches='tight')
结果是:
【讨论】:
这会自动计算图例的大小。如果mode == 1,代码类似于Steve Tjoa 的答案,而mode == 2 类似于Andre Holzner 的答案。
loc 参数必须设置为 'center' 以使其工作(但我不知道为什么这是必要的)。
mode = 1
#mode = 2
import pylab
fig = pylab.figure()
if mode == 1:
lines = fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
legend_fig = pylab.figure(figsize=(3,2))
legend = legend_fig.legend(lines, ('one', 'two'), 'center')
if mode == 2:
fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10), label='asd')
legend_fig = pylab.figure()
legend = pylab.figlegend(*fig.gca().get_legend_handles_labels(), loc = 'center')
legend.get_frame().set_color('0.70')
legend_fig.canvas.draw()
legend_fig.savefig('legend_cropped.png',
bbox_inches=legend.get_window_extent().transformed(legend_fig.dpi_scale_trans.inverted()))
legend_fig.savefig('legend_original.png')
原始(未裁剪)图例:
裁剪图例:
【讨论】:
bbox = legend.get_window_extent().transformed(legend_fig.dpi_scale_trans.inverted()); ll, ur = bbox.get_points(); x0, y0 = ll; x1, y1 = ur; w, h = x1 - x0, y1 - y0; x1, y1 = x0 + w * 1.1, y0 + h * 1.1; bbox = matplotlib.transforms.Bbox(np.array(((x0, y0),(x1, y1)))); legend_fig.savefig('filename', bbox_inches=bbox)
所以我在玩这个想法,我发现最简单的事情是这个(适用于多个轴):
def export_legend(filename=legend.png, fig=fig):
legend = fig.legend(framealpha=1)
fig2 = legend.figure
fig2.canvas.draw()
bbox = legend.get_window_extent().transformed(fig2.dpi_scale_trans.inverted())
fig2.savefig(filename, dpi="figure", bbox_inches=bbox, facecolor="w")
legend.remove() # removes legend from showing on plot
export_legend()
函数的输出(我用方框隐藏了标签):
fig 来自fig, ax = plt.subplots()
如果您希望图例仍显示在图上,您可以使用(例如):
fig.legend(loc="upper right", bbox_to_anchor=(1, 1), bbox_transform=ax.transAxes)
【讨论】: