强基初中数学&学Python——第259课 数字和数学第三方模块Matplotlib之七:封装端-图例(Legend)指南

  在Matplotlib中灵活生成图例。

  本图例指南是legend():https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html#matplotlib.pyplot.legend

上提供的文档的扩展,请确保在继续阅读本指南之前熟悉该文档的内容。

  本指南使用了一些常用术语,为清晰起见,此处记录了这些术语:

图例条目

  图例由一个或多个图例条目组成。条目由一个键和一个标签组成。

图例键

  每个图例标签左侧的彩色/图案标记。

图例标签

  文本描述由键表示的句柄。

图例句柄

  用于在图例中生成适当条目的原始对象。

控制图例条目

  在没有参数的情况下调用legend()会自动获取图例句柄及其相关标签。此功能等同于:

· 

· 

handles, labels = ax.get_legend_handles_labels()ax.legend(handles, labels)

  get_legend_handles_labels()函数返回图表域(Axes)上存在的句柄/艺术家列表,可用于生成图例的所有条目——但值得注意的是,并非所有艺术家都可以添加到图例中,此时必须创建一个“代理”(有关详细信息,请参阅创建专门用于添加到图例的艺术家(又名代理艺术家))。

  备注:标签为空字符串或标签以下划线“_”开头的艺术家将被忽略。

  为了完全控制添加到图例中的内容,通常将适当的句柄直接传递给legend():

· 

· 

· 

· 

fig, ax = plt.subplots()line_up, = ax.plot([1, 2, 3], label='Line 2')line_down, = ax.plot([3, 2, 1], label='Line 1')ax.legend(handles=[line_up, line_down])

  完整代码:

· 

· 

· 

· 

· 

· 

· 

ipython%matplotlib tkimport matplotlib.pyplot as pltfig, ax = plt.subplots()line_up, = ax.plot([1, 2, 3], label='Line 2')line_down, = ax.plot([3, 2, 1], label='Line 1')ax.legend(handles=[line_up, line_down])

 

  在某些情况下,无法设置句柄的标签,因此可以将标签列表传递给legend():

· 

· 

· 

· 

fig, ax = plt.subplots()line_up, = ax.plot([1, 2, 3], label='Line 2')line_down, = ax.plot([3, 2, 1], label='Line 1')ax.legend([line_up, line_down], ['Line Up', 'Line Down'])

  完整代码:

· 

· 

· 

· 

· 

· 

· 

ipython%matplotlib tkimport matplotlib.pyplot as pltfig, ax = plt.subplots()line_up, = ax.plot([1, 2, 3], label='Line 2')line_down, = ax.plot([3, 2, 1], label='Line 1')ax.legend([line_up, line_down], ['Line Up', 'Line Down'])

 

get_legend_handles_labels():

https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.get_legend_handles_labels.html#matplotlib.axes.Axes.get_legend_handles_labels

创建专门用于添加到图例的艺术家(又名代理艺术家):

https://matplotlib.org/stable/tutorials/intermediate/legend_guide.html#proxy-legend-handles

创建专门用于添加到图例的艺术家(又名代理艺术家)

  并非所有句柄都可以自动转换为图例条目,因此通常需要创建一个可以转换的艺术家。创建的图例句柄不必存在于图表(Figure)或图表域(Axes)容器中:

  假设我们想要创建一个图例,其中包含一些数据的条目,这些数据用红色表示:

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.patches as mpatchesimport matplotlib.pyplot as plt
fig, ax = plt.subplots()red_patch = mpatches.Patch(color='red', label='The red data')ax.legend(handles=[red_patch])
plt.show()

 

  有很多受支持的图例句柄。我们可以创建一条带有标记的线,而不是创建一块颜色:

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.lines as mlinesimport matplotlib.pyplot as plt
fig, ax = plt.subplots()blue_line = mlines.Line2D([], [], color='blue', marker='*',                          markersize=15, label='Blue stars')ax.legend(handles=[blue_line])
plt.show()

 

图例位置

  图例的位置可以由关键字参数loc指定。有关详细信息,请参阅legend()上的文档。

  bbox_to_anchor关键字为图例手动放置提供了很大程度的控制。例如,如果希望图表域(axes)图例位于图表(Figure)的右上角而不是图表域(axes)的角,只需指定图例的哪个角和该角所处的位置的坐标系:

· 

· 

ax.legend(bbox_to_anchor=(1, 1),          bbox_transform=fig.transFigure)

  完整代码:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.lines as mlinesimport matplotlib.pyplot as pltfig, ax = plt.subplots()blue_line = mlines.Line2D([], [], color='blue', marker='*',                          markersize=15, label='Blue stars')ax.legend(handles=[blue_line],          bbox_to_anchor=(1, 1),          bbox_transform=fig.transFigure)
plt.show()

 

  自定义图例放置的更多示例:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltfig, ax_dict = plt.subplot_mosaic([['top', 'top'], ['bottom', 'BLANK']],                                  empty_sentinel="BLANK")ax_dict['top'].plot([1, 2, 3], label="test1")ax_dict['top'].plot([3, 2, 1], label="test2")# Place a legend above this subplot, expanding itself to# fully use the given bounding box.ax_dict['top'].legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left',                      ncol=2, mode="expand", borderaxespad=0.)
ax_dict['bottom'].plot([1, 2, 3], label="test1")ax_dict['bottom'].plot([3, 2, 1], label="test2")# Place a legend to the right of this smaller subplot.ax_dict['bottom'].legend(bbox_to_anchor=(1.05, 1),                         loc='upper left', borderaxespad=0.)
plt.show()

 

同一图表域(Axes)上的多个图例

  有时,将图例条目拆分为多个图例更为清晰。虽然这样做的直觉方法可能是多次调用legend()函数,但你会发现图表域(Axes)上还是只存在一个图例。这样就可以重复调用legend()来将图例更新为图表域(Axes)上的最新句柄图例。要保留旧图例实例,我们必须手动将它们添加到图表域(Axes)中:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltfig, ax = plt.subplots()line1, = ax.plot([1, 2, 3], label="Line 1", linestyle='--')line2, = ax.plot([3, 2, 1], label="Line 2", linewidth=4)
# Create a legend for the first line.first_legend = ax.legend(handles=[line1], loc='upper right')
# Add the legend manually to the Axes.ax.add_artist(first_legend)
# Create another legend for the second line.ax.legend(handles=[line2], loc='lower right')
plt.show()

 

图例处理者

  为了创建图例条目,句柄作为参数提供给适当的HandlerBase子类。处理者子类的选择由以下规则决定:

  1、使用handler_map关键字中的值更新get_legend_handler_map()

  2、检查句柄是否在新创建的handler_map中。

  3、检查句柄类型是否在新创建的handler_map中。

  4、检查句柄的mro中是否有任何类型在新创建的handler_map中。

  为了完整起见,这个逻辑主要在get_legend_handler()中实现。

  所有这些灵活性,意味着我们有必要用钩子(hooks)来为自己类型的图例键实现自定义处理者。

  使用自定义处理者的最简单示例是实例化一个现有的legend_handler.HandlerBase子类。为了简单起见,就选择legend_handler.HandlerLine2D,它接受numpoints参数(为了方便,numpoints也是legend()函数上的关键字)。然后,我们可以将实例到处理者的映射作为关键字传递给图例。

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltfrom matplotlib.legend_handler import HandlerLine2D
fig, ax = plt.subplots()line1, = ax.plot([3, 2, 1], marker='o', label='Line 1')line2, = ax.plot([1, 2, 3], marker='o', label='Line 2')
ax.legend(handler_map={line1: HandlerLine2D(numpoints=4)})

 

  如上图,“Line 1”有4个标记点,其中“Line 2”有1个(默认值)。尝试以上代码,只需将地图的键从line1更改为type(line1)。即发现,两个Line2D实例都获得了4个标记。

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltfrom matplotlib.legend_handler import HandlerLine2D
fig, ax = plt.subplots()line1, = ax.plot([3, 2, 1], marker='o', label='Line 1')line2, = ax.plot([1, 2, 3], marker='o', label='Line 2')
ax.legend(handler_map={type(line1): HandlerLine2D(numpoints=4)})

 

  除了用于复杂绘图类型(如误差条形图、针头图和直方图)的处理者之外,默认的handler_map还有一个特殊的元组处理者(legend_handler.HandlerTuple),它按给定元组中的每个句柄的顺序,一个接一个地绘制在前一个的上面而成。以下示例演示了将两个图例键叠加在一起:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltfrom numpy.random import randnnp.random.seed(19680801)  # seed the random number generator.z = randn(10)
fig, ax = plt.subplots()red_dot, = ax.plot(z, "ro", markersize=15)# Put a white cross over some of the data.white_cross, = ax.plot(z[:5], "w+", markeredgewidth=3, markersize=15)
ax.legend([red_dot, (red_dot, white_cross)], ["Attr A", "Attr A+B"])

 

  legend_handler.HandlerTuple类也可用于将多个图例键分配给同一条目:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltfrom matplotlib.legend_handler import HandlerTuple
fig, ax = plt.subplots()p1, = ax.plot([1, 2.5, 3], 'r-d')p2, = ax.plot([3, 2, 1], 'k-o')
l = ax.legend([(p1, p2)], ['Two keys'], numpoints=1,              handler_map={tuple: HandlerTuple(ndivide=None)})fig.show()

 

HandlerBase:

https://matplotlib.org/stable/api/legend_handler_api.html#matplotlib.legend_handler.HandlerBase

get_legend_handler_map():

https://matplotlib.org/stable/api/legend_api.html#matplotlib.legend.Legend.get_legend_handler_map

get_legend_handler():

https://matplotlib.org/stable/api/legend_api.html#matplotlib.legend.Legend.get_legend_handler

legend_handler.HandlerBase:

https://matplotlib.org/stable/api/legend_handler_api.html#matplotlib.legend_handler.HandlerBase

legend_handler.HandlerLine2D:

https://matplotlib.org/stable/api/legend_handler_api.html#matplotlib.legend_handler.HandlerLine2D

Line2D:

https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D

legend_handler.HandlerTuple:

https://matplotlib.org/stable/api/legend_handler_api.html#matplotlib.legend_handler.HandlerTuple

实现自定义图例处理者

  可以实现自定义处理者将任何句柄转换为图例键(句柄不一定需要是matplotlib艺术家)。处理者必须实现legend_artist方法,该方法返回一个艺术家供图例使用。legend_artist中讲述了legend_artist方法所需的签名(参数)。

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltimport matplotlib.patches as mpatches
class AnyObject:    pass    class AnyObjectHandler:    def legend_artist(self, legend, orig_handle, fontsize, handlebox):        x0, y0 = handlebox.xdescent, handlebox.ydescent        width, height = handlebox.width, handlebox.height        patch = mpatches.Rectangle([x0, y0], width, height, facecolor='red',                                   edgecolor='black', hatch='xx', lw=3,                                   transform=handlebox.get_transform())        handlebox.add_artist(patch)        return patch
fig, ax = plt.subplots()
ax.legend([AnyObject()], ['My first handler'],          handler_map={AnyObject: AnyObjectHandler()})fig.show()

 

  或者,如果我们想全局接受AnyObject实例,而不需要一直手动设置handler_map关键字,我们可以使用以下方式注册新的处理者:

· 

· 

from matplotlib.legend import LegendLegend.update_default_handler_map({AnyObject: AnyObjectHandler()})

  完整代码:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltimport matplotlib.patches as mpatches
class AnyObject:    pass    class AnyObjectHandler:    def legend_artist(self, legend, orig_handle, fontsize, handlebox):        x0y0 = handlebox.xdescent, handlebox.ydescent        width, height = handlebox.width, handlebox.height        patch = mpatches.Rectangle([x0, y0], width, height, facecolor='red',                                   edgecolor='black', hatch='xx', lw=3,                                   transform=handlebox.get_transform())        handlebox.add_artist(patch)        return patch
fig, ax = plt.subplots()
from matplotlib.legend import LegendLegend.update_default_handler_map({AnyObject: AnyObjectHandler()})
ax.legend([AnyObject()], ['My first default handler'])fig.show()

 

  虽然这里的功能很清楚,但请记住,系统已经实现了很多处理者,你想要实现的功能可能已经可以通过现有的类轻松实现。例如,要生成椭圆图例键,而不是矩形图例键:

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

· 

import matplotlib.pyplot as pltimport matplotlib.patches as mpatchesfrom matplotlib.legend_handler import HandlerPatch
class HandlerEllipse(HandlerPatch):    def create_artists(self, legend, orig_handle,                       xdescent, ydescent, width, height, fontsize, trans):        center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent        p = mpatches.Ellipse(xy=center, width=width + xdescent,                             height=height + ydescent)        self.update_prop(p, orig_handle, legend)        p.set_transform(trans)        return [p]
c = mpatches.Circle((0.5, 0.5), 0.25, facecolor="green",                    edgecolor="red", linewidth=3)
fig, ax = plt.subplots()
ax.add_patch(c)ax.legend([c], ["An ellipse, not a rectangle"],          handler_map={mpatches.Circle: HandlerEllipse()})fig.show()

 

legend_artist:

https://matplotlib.org/stable/api/legend_handler_api.html#matplotlib.legend_handler.HandlerBase.legend_artist