如何使用 matplotlib 使两个标记在图例中共享相同的标签?

How to make two markers share the same label in the legend using matplotlib?

我想要的是这样的:

我得到的是:

那么如何将标记合并为一个标签呢? 同样对于线条,对于线条,当然,你可以通过在使用相同的线型时不为第二条线分配标签来实现它,但是对于标记,你不能,因为它们具有不同的形状。

这是一个新的解决方案,可以绘制具有相同标签的任何标记集合。我还没有弄清楚如何让它与线图中的标记一起使用,但如果需要,您可以在线图之上绘制散点图。

from matplotlib import pyplot as plt
import matplotlib.collections as mcol
import matplotlib.transforms as mtransforms
import numpy as np
from matplotlib.legend_handler import HandlerPathCollection
from matplotlib import cm


class HandlerMultiPathCollection(HandlerPathCollection):
    """
    Handler for PathCollections, which are used by scatter
    """
    def create_collection(self, orig_handle, sizes, offsets, transOffset):
        p = type(orig_handle)(orig_handle.get_paths(), sizes=sizes,
                              offsets=offsets,
                              transOffset=transOffset,
                              )
        return p

fig, ax = plt.subplots()
#make some data to plot
x = np.arange(0, 100, 10)
models = [.05 * x, 8 * np.exp(- .1 * x), np.log(x + 1), .01 * x]
tests = [model + np.random.rand(len(model)) - .5 for model in models]
#make colors and markers
colors = cm.brg(np.linspace(0, 1, len(models)))
markers = ['o', 'D', '*', 's']
markersize = 50
plots = []
#plot points and lines
for i in xrange(len(models)):
    line, = plt.plot(x, models[i], linestyle = 'dashed', color = 'black', label = 'Model')
    plot = plt.scatter(x, tests[i], c = colors[i], s = markersize, marker = markers[i])
    plots.append(plot)

#get attributes
paths = []
sizes = []
facecolors = []
edgecolors = []
for plot in plots:
    paths.append(plot.get_paths()[0])
    sizes.append(plot.get_sizes()[0])
    edgecolors.append(plot.get_edgecolors()[0])
    facecolors.append(plot.get_facecolors()[0])

#make proxy artist out of a collection of markers
PC = mcol.PathCollection(paths, sizes, transOffset = ax.transData, facecolors = colors, edgecolors = edgecolors)
PC.set_transform(mtransforms.IdentityTransform())
plt.legend([PC, line], ['Test', 'Model'], handler_map = {type(PC) : HandlerMultiPathCollection()}, scatterpoints = len(paths), scatteryoffsets = [.5], handlelength = len(paths))
plt.show()

如果您愿意使用所有圆圈作为标记并仅按颜色区分,我有一个解决方案。您可以使用圆形集合来表示标记,然后为整个集合添加图例标签。

示例代码:

import matplotlib.pyplot as plt
import matplotlib.collections as collections
from matplotlib import cm
import numpy as np

#make some data to plot
x = np.arange(0, 100, 10)
models = [.05 * x, 8 * np.exp(- .1 * x), np.log(x + 1), .01 * x]
tests = [model + np.random.rand(len(model)) - .5 for model in models]
#make colors
colors = cm.brg(np.linspace(0, 1, len(models)))
markersize = 50
#plot points and lines
for i in xrange(len(models)):
    line, = plt.plot(x, models[i], linestyle = 'dashed', color = 'black', label = 'Model')
    plt.scatter(x, tests[i], c = colors[i], s = markersize)
#create collection of circles corresponding to markers
circles = collections.CircleCollection([markersize] * len(models), facecolor = colors)
#make the legend -- scatterpoints needs to be the same as the number 
#of markers so that all the markers show up in the legend
plt.legend([circles, line], ['Test', 'Model'], scatterpoints = len(models), scatteryoffsets = [.5], handlelength = len(models))
plt.show()

我认为最好使用完整的图例 - 否则,您的读者将如何知道两个模型或两个数据集之间的区别?我会这样做:

但是,如果您真的想按照自己的方式来做,您可以使用自定义图例,如此 guide 所示。您需要像他们一样创建自己的 class,定义 legend_artist 方法,然后根据需要添加正方形和圆形。这是生成的图和用于生成它的代码:

#!/usr/bin/env python
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np


# ==================================
# Define the form of the function
# ==================================
def model(x, A=190, k=1):
    return A * np.exp(-k*x/50)

# ==================================
# How many data points are generated
# ==================================
num_samples = 15

# ==================================
# Create data for plots
# ==================================
x_model = np.linspace(0, 130, 200)

x_data1 = np.random.rand(num_samples) * 130
x_data1.sort()

x_data2 = np.random.rand(num_samples) * 130
x_data2.sort()

data1 = model(x_data1, k=1) * (1 + np.random.randn(num_samples) * 0.2)
data2 = model(x_data2, k=2) * (1 + np.random.randn(num_samples) * 0.15)

model1 = model(x_model, k=1)
model2 = model(x_model, k=2)

# ==================================
# Plot everything normally
# ==================================
fig = plt.figure()
ax = fig.add_subplot('111')
ax.plot(x_data1, data1, 'ok', markerfacecolor='none', label='Data (k=1)')
ax.plot(x_data2, data2, 'sk', markeredgecolor='0.5', markerfacecolor='0.5', label='Data (k=2)')
ax.plot(x_model, model1, '-k', label='Model (k=1)')
ax.plot(x_model, model2, '--k', label='Model (k=2)')

# ==================================
# Format plot
# ==================================
ax.set_xlabel('Distance from heated face(^{-2}$ m)')
ax.set_ylabel('Temperature ($^\circ$C)')
ax.set_xlim((0, 130))
ax.set_title('Normal way to plot')
ax.legend()
fig.tight_layout()

plt.show()


# ==================================
# ==================================
# Do it again, but with custom
# legend
# ==================================
# ==================================
class AnyObject(object):
    pass


class data_handler(object):
    def legend_artist(self, legend, orig_handle, fontsize, handlebox):
        scale = fontsize / 22
        x0, y0 = handlebox.xdescent, handlebox.ydescent
        width, height = handlebox.width, handlebox.height
        patch_sq = mpatches.Rectangle([x0, y0 + height/2 * (1 - scale) ], height * scale, height * scale, facecolor='0.5',
                edgecolor='0.5', transform=handlebox.get_transform())
        patch_circ = mpatches.Circle([x0 + width - height/2, y0 + height/2], height/2 * scale, facecolor='none',
                edgecolor='black', transform=handlebox.get_transform())

        handlebox.add_artist(patch_sq)
        handlebox.add_artist(patch_circ)
        return patch_sq

# ==================================
# Plot everything
# ==================================
fig = plt.figure()
ax = fig.add_subplot('111')
d1 = ax.plot(x_data1, data1, 'ok', markerfacecolor='none', label='Data (k=2)')
d2 = ax.plot(x_data2, data2, 'sk', markeredgecolor='0.5', markerfacecolor='0.5', label='Data (k=1)')
m1 = ax.plot(x_model, model1, '-k', label='Model (k=1)')
m2 = ax.plot(x_model, model2, '-k', label='Model (k=2)')

# ax.legend([d1], handler_map={ax.plot: data_handler()})
ax.legend([AnyObject(), m1[0]], ['Data', 'Model'], handler_map={AnyObject: data_handler()})

# ==================================
# Format plot
# ==================================
ax.set_xlabel('Distance from heated face(^{-2}$ m)')
ax.set_ylabel('Temperature ($^\circ$C)')
ax.set_xlim((0, 130))
ax.set_title('Custom legend')
fig.tight_layout()

plt.show()

您可以通过绘制没有任何标签的数据然后单独添加标签来做到这一点:

from matplotlib import pyplot as plt
from numpy import random

xs = range(10)
data = random.rand(10, 2)    
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
kwargs = {'color': 'r', 'linewidth': 2, 'linestyle': '--'}

ax.plot(xs, data, **kwargs)
ax.plot([], [], label='Model', **kwargs)
ax.legend()
plt.show()

我还发现 this link 非常有用(下面的代码),这是处理此问题的更简单方法。它基本上是使用图例句柄列表使第一个句柄的标记之一不可见,并将其与第二个句柄的标记重叠。这样,您就可以将两个标记并排放置一个标签。

fig, ax = plt.subplots()
p1 = ax.scatter([0.1],[0.5],c='r',marker='s')
p2 = ax.scatter([0.3],[0.2],c='b',marker='o')
l = ax.legend([(p1,p2)],['points'],scatterpoints=2)

With the above code, a TupleHandler is used to create legend handles which simply overplot two handles (there are red squares behind the blue circles if you look carefylly. What you want to do is make the second marker of first handle and the first marker of the second handle invisible. Unfortunately, the TupleHandler is a rather recent addition and you need a special function to get all the handles. Otherwise, you can use the Legend.legendHandles attribute (it only show the first handle for the TupleHandler).

def get_handle_lists(l):
    """returns a list of lists of handles.
    """
    tree = l._legend_box.get_children()[1]

    for column in tree.get_children():
        for row in column.get_children():
            yield row.get_children()[0].get_children()
handles_list = list(get_handle_lists(l))
handles = handles_list[0] # handles is a list of two PathCollection.
                          # The first one is for red squares, and the second
                          # is for blue circles.
handles[0].set_facecolors(["r", "none"]) # for the fist
                   # PathCollection, make the
                   # second marker invisible by
                   # setting their facecolor and
                   # edgecolor to "none."
handles[0].set_edgecolors(["k", "none"])
handles[1].set_facecolors(["none", "b"])
handles[1].set_edgecolors(["none", "k"])
fig

请注意,在最新版本的 matplotlib 中,您可以使用 class matplotlib.legend_handler.HandlerTuple as illustrated in this and also in this guide:

实现此目的
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerTuple
fig, ax1 = plt.subplots(1, 1)

# First plot: two legend keys for a single entry
p2, = ax1.plot([3, 4], [2, 3], 'o', mfc="white", mec="k")
p1, = ax1.plot([1, 2], [5, 6], 's', mfc="gray", mec="gray")
# `plot` returns a list, but we want the handle - thus the comma on the left
p3, = ax1.plot([1, 5], [4, 4], "-k")
p4, = ax1.plot([2, 6], [3, 2], "-k")

# Assign two of the handles to the same legend entry by putting them in a tuple
# and using a generic handler map (which would be used for any additional
# tuples of handles like (p1, p3)).
l = ax1.legend([(p1, p2), p3], ['data', 'models'],
               handler_map={tuple: HandlerTuple(ndivide=None)})

plt.savefig("demo.png")