【问题标题】:Sharing the same label for two plots with line and point markers in legend在图例中使用线和点标记为两个绘图共享相同的标签
【发布时间】:2019-02-14 07:42:15
【问题描述】:

我有一个包含两个不同系列曲线的图,我将使用点和线来绘制它们。我想要一个图例,使线和点标记共享相同的标签。

我已经尝试过this 建议,如果我的两个系列的图都具有不同的点类型,而不是线和点,则该建议效果很好。 我目前使用的代码,带有不正确的图例,是

import numpy as np
import matplotlib.pyplot as plt

Vs = np.array([0.5, 1, 1.5, 2])
Xs = np.array([[ 0.5, 0.2,  0.7],
  [ 0.5, 0.3,  0.9],
  [ 0.5, 0.5, 0.4],
  [ 0.5, 0.7, 0.4],
  [ 0.5, 0.9, 0.7],
  [ 1, 0.15,  0.9],
  [ 1, 0.35, 0.6],
  [ 1, 0.45, 0.6],
  [ 1, 0.67, 0.5],
  [ 1, 0.85, 0.9],
  [ 1.5, 0.1,  0.9],
  [ 1.5, 0.3, 0.7],
  [ 1.5, 0.76, 0.3],
  [ 1.5, 0.98, 0.4],
  [ 2, 0.21, 0.5],
  [ 2, 0.46, 0.4],
  [ 2, 0.66, 0.3],
  [ 2, 0.76, 0.5],
  [ 2, 0.88, 0.4],
  [ 2, 0.99, 0.4]])


 f, axs = plt.subplots(1, 1, figsize=(2.5,3))
 #-------------------------------------
 axs.set_xlim(0.38,1.0)
 axs.set_ylim(0.0,4.0)
 colors = plt.cm.gist_ncar(np.linspace(0,1,max(Vs)+3))
 for idx,Val in enumerate(Vs):
     axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2],'s',label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
     axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2]*Val/0.3,'-', label=r"$Y={}$".format(Val), ms=3, color=colors[idx])


axs.set_ylim(0.0,4.0)
axs.set_ylabel(r"$Y$    ", labelpad=2)
axs.set_xlabel(r"$X$    ", labelpad=2)
axs.set_yticks([0,0.5,1.0,1.5,2.0, 2.5, 3.0, 3.5, 4.0])
axs.set_xticks([0,0.5,1.0])

axs.legend(fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,handletextpad=0.2, frameon=False)

f.savefig("tmp.pdf")
plt.show()

你有什么解决这个问题的建议吗?

【问题讨论】:

    标签: python matplotlib legend


    【解决方案1】:

    在这种情况下将我的答案应用于How to create two legend objects for a single plot instance?

    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib
    
    Vs = np.array([0.5, 1, 1.5, 2])
    Xs = np.array([[ 0.5, 0.2,  0.7], [ 0.5, 0.3,  0.9], [ 0.5, 0.5, 0.4],
                   [ 0.5, 0.7, 0.4],[ 0.5, 0.9, 0.7], [ 1, 0.15,  0.9],
                   [ 1, 0.35, 0.6], [ 1, 0.45, 0.6], [ 1, 0.67, 0.5],
                   [ 1, 0.85, 0.9], [ 1.5, 0.1,  0.9], [ 1.5, 0.3, 0.7],
                   [ 1.5, 0.76, 0.3], [ 1.5, 0.98, 0.4], [ 2, 0.21, 0.5], 
                   [ 2, 0.66, 0.3], [ 2, 0.76, 0.5], [ 2, 0.88, 0.4],
                   [ 2, 0.99, 0.4]])
    
    
    f, axs = plt.subplots(1, 1, figsize=(2.5,3))
    
    axs.set_xlim(0.38,1.0)
    axs.set_ylim(0.0,4.0)
    colors = plt.cm.gist_ncar(np.linspace(0,1,max(Vs)+3))
    for idx,Val in enumerate(Vs):
        axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2],'s',label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
        axs.plot(Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2]*Val/0.3,'-', label=r"$Y={}$".format(Val), ms=3, color=colors[idx])
    
    
    axs.set_ylim(0.0,4.0)
    axs.set_ylabel(r"$Y$    ", labelpad=2)
    axs.set_xlabel(r"$X$    ", labelpad=2)
    axs.set_yticks([0,0.5,1.0,1.5,2.0, 2.5, 3.0, 3.5, 4.0])
    axs.set_xticks([0,0.5,1.0])
    
    h, l = axs.get_legend_handles_labels()
    axs.legend(handles=zip(h[::2], h[1::2]), labels=l[::2], 
               handler_map = {tuple: matplotlib.legend_handler.HandlerTuple(None)})
    
    
    plt.show()
    

    【讨论】:

    • 这确实容易多了。在我自己的解决方案的所有尝试和错误之后,这几乎很烦人:D
    【解决方案2】:

    我会创建自定义线条以显示在您的图例中。您可以通过保存每个绘图命令的输出来解决此问题(线图返回一个 matplotlib.lines.Line2D 对象,该对象存储线型、标记样式、颜色等)。然后,您可以遍历保存的行并创建新的Line2D 对象,将两条线的属性与相同的颜色组合在一起。将这些新的Line2D 对象保存在一个列表中,比如handles,然后您可以将该列表传递给ax.legend() 调用:

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.lines import Line2D
    
    Vs = np.array([0.5, 1, 1.5, 2])
    Xs = np.array([[ 0.5, 0.2,  0.7],
      [ 0.5, 0.3,  0.9],
      [ 0.5, 0.5, 0.4],
      [ 0.5, 0.7, 0.4],
      [ 0.5, 0.9, 0.7],
      [ 1, 0.15,  0.9],
      [ 1, 0.35, 0.6],
      [ 1, 0.45, 0.6],
      [ 1, 0.67, 0.5],
      [ 1, 0.85, 0.9],
      [ 1.5, 0.1,  0.9],
      [ 1.5, 0.3, 0.7],
      [ 1.5, 0.76, 0.3],
      [ 1.5, 0.98, 0.4],
      [ 2, 0.21, 0.5],
      [ 2, 0.46, 0.4],
      [ 2, 0.66, 0.3],
      [ 2, 0.76, 0.5],
      [ 2, 0.88, 0.4],
      [ 2, 0.99, 0.4]])
    
    
    f, axs = plt.subplots(1, 1, figsize=(2.5,3))
    #-------------------------------------
    axs.set_xlim(0.38,1.0)
    axs.set_ylim(0.0,4.0)
    colors = plt.cm.gist_ncar(np.linspace(0,1,max(Vs)+3))
    
    
    ##saving the Line2D objects:
    lines = []
    points = []
    for idx,Val in enumerate(Vs):    
        point, = axs.plot(
            Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2],'s',
            label=r"$Y={}$".format(Val), ms=3, color=colors[idx]
        )
        line, = axs.plot(
            Xs[Xs[:,0] == Val ,1], Xs[Xs[:,0] == Val ,2]*Val/0.3,'-',
            label=r"$Y={}$".format(Val), ms=3, color=colors[idx]
        )
        points.append(point)
        lines.append(line)
    
    axs.set_ylim(0.0,4.0)
    axs.set_ylabel(r"$Y$    ", labelpad=2)
    axs.set_xlabel(r"$X$    ", labelpad=2)
    axs.set_yticks([0,0.5,1.0,1.5,2.0, 2.5, 3.0, 3.5, 4.0])
    axs.set_xticks([0,0.5,1.0])
    
    #axs.legend(fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,handletextpad=0.2, frameon=False)
    
    #f.savefig("tmp.pdf")
    ##generating the legend handles, with linestyle, markerstyle, color, and label
    ##copied from the plotted lines:
    handles = [
        Line2D(
            [],[], marker=point.get_marker(), linestyle=line.get_linestyle(),
            color = line.get_color(), 
            label = line.get_label(),
        ) for line, point in zip(lines,points)
    ]
    
    ##passing handles as argument to the `legend()` call:
    axs.legend(
        handles=handles,
        fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,
        handletextpad=0.2, frameon=False,
        )
    plt.show()
    

    生成的图片如下所示:

    编辑

    按照问题中链接的示例,可以设计一个处理程序对象来生成所需的图例句柄。将上述代码的最后一部分替换为以下内容:

    ##a dedicated class that holds the lines to be included in the legend entry
    class LineContainer:
        def __init__(self, *args):
            args = [line for line in args if isinstance(line,Line2D)]
            if len(args) < 0:
                raise ValueError('At least one line must be passed')
            self._lines = list(args)
    
        def get_lines(self):
            return self._lines
    
        def get_label(self):
            ##assuming here that all lines have the same label
            return self._lines[0].get_label()
    
    
    ##adapted from https://stackoverflow.com/a/31530393/2454357
    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
    
            ##use these two lines to control the lengths of the individual line
            ##segments and the spacing between them:
    
            ##width for individual artists
            l = 0.7*width/len(orig_handle.get_lines()) 
            ##distance between individual artists
            l0 = 0.3*width/len(orig_handle.get_lines()) 
    
            result = []
            for i, line in enumerate(orig_handle.get_lines()):
    
                new_line = Line2D([],[])
                new_line.update_from(line)
    
                ##if no linestyle is defined, plot only the marker:
                if new_line.get_linestyle() in ['None', None]:
                    new_line.set_data(
                        [x0+l*(i+0.5)], [y0+height/2]
                    )
    
                ##else plot markers and lines:
                else:
                    new_line.set_data(
                        [x0+l*i+l0/2, x0+l*(i+1)-l0/2],
                        [y0+height/2, y0+height/2]
                    )
                new_line.set_transform(handlebox.get_transform())
                handlebox.add_artist(new_line)
                result.append(new_line)
    
            return result
    
    
    ##generating the handles
    handles = [
        LineContainer(line, point) for line, point in zip(lines, points)
    ]
    
    axs.legend(
        handles = handles,
        handler_map={LineContainer: data_handler()},
        fontsize=6, loc=2, numpoints = 1, labelspacing=0.2,
        handletextpad=0.2, frameon=False,
        )
    plt.show()
    

    给出以下图像:

    【讨论】:

    • 生成handles 是一个非常好的主意。原则上,我希望将标记分开,以便一个与另一个相邻而没有任何重叠。对于普通图例,“bbox_to_anchor”会有所帮助,但我该如何在这里使用它?
    • Matplotlib 为这种情况提供了一个通用的HandlerTuple
    猜你喜欢
    • 1970-01-01
    • 2015-07-21
    • 2020-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-24
    • 2018-02-18
    相关资源
    最近更新 更多