How to make two markers share the same label in the legend
Asked Answered
R

5

35

What I want is like this: enter image description here

What I get is this: enter image description here

So how to merge the markers into one label? also for the lines, for the lines, of course, u can realize it by not assigning label to the second line while using the same linetype, but for the markers, you can not, since they are of different shapes.

Root answered 17/7, 2015 at 14:22 Comment(0)
L
10

I think it's best to use a full legend - otherwise, how will your readers know the difference between the two models, or the two datasets? I would do it this way:

enter image description here

But, if you really want to do it your way, you can use a custom legend as shown in this guide. You'll need to create your own class, like they do, that defines the legend_artist method, which then adds squares and circles as appropriate. Here is the plot generated and the code used to generate it:

enter image description here

#!/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($10^{-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($10^{-2}$ m)')
ax.set_ylabel('Temperature ($^\circ$C)')
ax.set_xlim((0, 130))
ax.set_title('Custom legend')
fig.tight_layout()

plt.show()
Lewis answered 21/7, 2015 at 4:28 Comment(8)
This looks nice, but when I ran your sample code, it displayed "TypeError: 'data_handler' object is not callable"...Root
What version of Python are you running? It looks like a problem interpreting data_handler as a class, are you running <=Python 2.1?Lewis
Well... I don't know what to tell you, that error doesn't make sense to me because data_handler is a class, and not a function, so I don't know why your interpreter is trying to call it. It works for me. Maybe try upgrading your python install? You might have to google around a bit too.Lewis
In my python version, it is also a class. Object is an instance of class. I have some confusions: 1.AnyObject: data_handler()? Is there any relationship between these two objects? 2. In your class data_handler(object), why you only return patch_sq? 3.AnyObject: data_handler()? Does it need any parameter input? Thank you, Joel~Root
It's set in the argument dict handle_map in the legend method of ax. My understanding is that, for object type AnyObject the custom legend handler is used to create a legend entry.Lewis
It works as-is. I even copy-paste the code to a new file. I return patch_sq because it works. I'm not sure why the guide says you have to return an artist that represents the entire legend, but I don't have to. Maybe your problem is the version of matplotlib that you're using?Lewis
Ah hah, you are really an expert even in terms of your intuition, I install new matplotlib, then everything is ok.Root
@Root Glad to be of service.Lewis
P
22

Note that in recent versions of matplotlib you can achieve this using class matplotlib.legend_handler.HandlerTuple as illustrated in this answer 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")

Example script output

Pastrami answered 4/3, 2019 at 9:51 Comment(1)
Clear and simple solutionCholinesterase
L
10

I think it's best to use a full legend - otherwise, how will your readers know the difference between the two models, or the two datasets? I would do it this way:

enter image description here

But, if you really want to do it your way, you can use a custom legend as shown in this guide. You'll need to create your own class, like they do, that defines the legend_artist method, which then adds squares and circles as appropriate. Here is the plot generated and the code used to generate it:

enter image description here

#!/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($10^{-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($10^{-2}$ m)')
ax.set_ylabel('Temperature ($^\circ$C)')
ax.set_xlim((0, 130))
ax.set_title('Custom legend')
fig.tight_layout()

plt.show()
Lewis answered 21/7, 2015 at 4:28 Comment(8)
This looks nice, but when I ran your sample code, it displayed "TypeError: 'data_handler' object is not callable"...Root
What version of Python are you running? It looks like a problem interpreting data_handler as a class, are you running <=Python 2.1?Lewis
Well... I don't know what to tell you, that error doesn't make sense to me because data_handler is a class, and not a function, so I don't know why your interpreter is trying to call it. It works for me. Maybe try upgrading your python install? You might have to google around a bit too.Lewis
In my python version, it is also a class. Object is an instance of class. I have some confusions: 1.AnyObject: data_handler()? Is there any relationship between these two objects? 2. In your class data_handler(object), why you only return patch_sq? 3.AnyObject: data_handler()? Does it need any parameter input? Thank you, Joel~Root
It's set in the argument dict handle_map in the legend method of ax. My understanding is that, for object type AnyObject the custom legend handler is used to create a legend entry.Lewis
It works as-is. I even copy-paste the code to a new file. I return patch_sq because it works. I'm not sure why the guide says you have to return an artist that represents the entire legend, but I don't have to. Maybe your problem is the version of matplotlib that you're using?Lewis
Ah hah, you are really an expert even in terms of your intuition, I install new matplotlib, then everything is ok.Root
@Root Glad to be of service.Lewis
M
7

You can do this by plotting data without any label and then adding the label separately:

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()

enter image description here

Morphophoneme answered 21/4, 2016 at 7:44 Comment(0)
P
7

I also found this link very useful (code below), it's an easier way to handle this issue. It's basically using a list of legend handles to make one of the markers of the first handle invisible and overplot it with the marker of the second handle. This way, you have both markers next to each other with one label.

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)

enter image description here

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

enter image description here

Paronomasia answered 6/7, 2016 at 19:53 Comment(0)
A
5

Here is a new solution that will plot any collection of markers with the same label. I have not figured out how to make it work with markers from a line plot, but you can probably do a scatter plot on top of a line plot if you need to.

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()

Plot with markers sharing a label

I have a solution for you if you're willing to use all circles for markers and differentiate by color only. You can use a circle collection to represent the markers, and then have a legend label for the collection as a whole.

Example code:

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()

Scatter and Line plot with merged legend labels

Absentee answered 20/7, 2015 at 23:24 Comment(1)
This is a useful alternative, however, when printed in black and white, it is still difficult to differentiate each other. : )Root

© 2022 - 2024 — McMap. All rights reserved.