35

I have data that results in multiple lines being plotted, I want to give these lines a single label in my legend. I think this can be better demonstrated using the example below,

a = np.array([[ 3.57,  1.76,  7.42,  6.52],
              [ 1.57,  1.2 ,  3.02,  6.88],
              [ 2.23,  4.86,  5.12,  2.81],
              [ 4.48,  1.38,  2.14,  0.86],
              [ 6.68,  1.72,  8.56,  3.23]])

plt.plot(a[:,::2].T, a[:, 1::2].T, 'r', label='data_a')

plt.legend(loc='best')

As you can see at Out[23] the plot resulted in 5 distinct lines. The resulting plot looks like this legend of multiple line plot

Is there any way that I can tell the plot method to avoid multiple labels? I don't want to use custom legend (where you specify the label and the line shape all at once) as much as I can.

Community
  • 1
  • 1
hashmuke
  • 3,075
  • 2
  • 18
  • 29
  • 1
    You need newscast to create a custom legend. Can't paste a link because my phone is being crap. Google "matplotlib manually create legend". There is an SO answer which has everything you need a couple of hits down. – will Oct 13 '14 at 10:38

9 Answers9

24

I'd make a small helper function personally, if i planned on doing it often;

from matplotlib import pyplot
import numpy


a = numpy.array([[ 3.57,  1.76,  7.42,  6.52],
                 [ 1.57,  1.2 ,  3.02,  6.88],
                 [ 2.23,  4.86,  5.12,  2.81],
                 [ 4.48,  1.38,  2.14,  0.86],
                 [ 6.68,  1.72,  8.56,  3.23]])


def plotCollection(ax, xs, ys, *args, **kwargs):

  ax.plot(xs,ys, *args, **kwargs)

  if "label" in kwargs.keys():

    #remove duplicates
    handles, labels = pyplot.gca().get_legend_handles_labels()
    newLabels, newHandles = [], []
    for handle, label in zip(handles, labels):
      if label not in newLabels:
        newLabels.append(label)
        newHandles.append(handle)

    pyplot.legend(newHandles, newLabels)

ax = pyplot.subplot(1,1,1)  
plotCollection(ax, a[:,::2].T, a[:, 1::2].T, 'r', label='data_a')
plotCollection(ax, a[:,1::2].T, a[:, ::2].T, 'b', label='data_b')
pyplot.show()

An easier (and IMO clearer) way to remove duplicates (than what you have) from the handles and labels of the legend is this:

handles, labels = pyplot.gca().get_legend_handles_labels()
newLabels, newHandles = [], []
for handle, label in zip(handles, labels):
  if label not in newLabels:
    newLabels.append(label)
    newHandles.append(handle)
pyplot.legend(newHandles, newLabels)
will
  • 10,260
  • 6
  • 46
  • 69
  • If you use a `set` for `newLabels` you'll avoid iterating over the entire thing when checking for membership. – zfj3ub94rf576hc4eegm Jul 19 '21 at 01:36
  • @zfj3ub94rf576hc4eegm `set` doesn't preserve the elements in the order in which they were added, so your labels could appear in the wrong order in the legend. Also, when dealing with so few elements, it's not necessarily faster to use a set. Also, I think it's unnecessary to optimise plotting code :D – Pig May 13 '22 at 11:07
  • @zfj3ub94rf576hc4eegm the point of using set is not to optimize it, it's to prevent the legend entries being duplicated. if you want to preserve order, then just use an ordered set. – will May 13 '22 at 23:34
17

Numpy solution based on will's response above.

import numpy as np
import matplotlib.pylab as plt
a = np.array([[3.57, 1.76, 7.42, 6.52],
              [1.57, 1.20, 3.02, 6.88],
              [2.23, 4.86, 5.12, 2.81],
              [4.48, 1.38, 2.14, 0.86],
              [6.68, 1.72, 8.56, 3.23]])

plt.plot(a[:,::2].T, a[:, 1::2].T, 'r', label='data_a')
handles, labels = plt.gca().get_legend_handles_labels()

Assuming that equal labels have equal handles, get unique labels and their respective indices, which correspond to handle indices.

labels, ids = np.unique(labels, return_index=True)
handles = [handles[i] for i in ids]
plt.legend(handles, labels, loc='best')
plt.show()
rafaelvalle
  • 6,683
  • 3
  • 34
  • 36
  • another very short solution: `legs = ax.get_legend_handles_labels(); list(zip(*[[legs[0][i], legs[1][i]] for i in [legs[1].index(l) for l in set(legs[1])]]))` – Night Train Jul 22 '20 at 13:11
14

Matplotlib gives you a nice interface to collections of lines, LineCollection. The code is straight forward

import numpy
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection

a = numpy.array([[ 3.57,  1.76,  7.42,  6.52],
                 [ 1.57,  1.2 ,  3.02,  6.88],
                 [ 2.23,  4.86,  5.12,  2.81],
                 [ 4.48,  1.38,  2.14,  0.86],
                 [ 6.68,  1.72,  8.56,  3.23]])

xs = a[:,::2]
ys = a[:, 1::2]
lines = LineCollection([list(zip(x,y)) for x,y in zip(xs, ys)], label='data_a')
f, ax = plt.subplots(1, 1)
ax.add_collection(lines)
ax.legend()
ax.set_xlim([xs.min(), xs.max()]) # have to set manually
ax.set_ylim([ys.min(), ys.max()])
plt.show()

This results in the output below: A plot of the lines collected under a single legend entry.

televator
  • 1,133
  • 1
  • 9
  • 8
10

A low tech solution is to make two plot calls. One that plots your data and a second one that plots nothing but carries the handle:

a = np.array([[ 3.57,  1.76,  7.42,  6.52],
              [ 1.57,  1.2 ,  3.02,  6.88],
              [ 2.23,  4.86,  5.12,  2.81],
              [ 4.48,  1.38,  2.14,  0.86],
              [ 6.68,  1.72,  8.56,  3.23]])

plt.plot(a[:,::2].T, a[:, 1::2].T, 'r')
plt.plot([],[], 'r', label='data_a')

plt.legend(loc='best')

Here's the result:

result

clemens
  • 16,716
  • 11
  • 50
  • 65
Arthur Bauville
  • 190
  • 2
  • 5
9

So using will's suggestion and another question here, I am leaving my remedy here

handles, labels = plt.gca().get_legend_handles_labels()
i =1
while i<len(labels):
    if labels[i] in labels[:i]:
        del(labels[i])
        del(handles[i])
    else:
        i +=1

plt.legend(handles, labels)

And the new plot looks like, modified multiple line plot legend

Community
  • 1
  • 1
hashmuke
  • 3,075
  • 2
  • 18
  • 29
4

The easiest and most pythonic way to remove duplicates is to use the keys of a dict which are guaranteed to be unique. This also ensures that we only iterate over each of the (handle, label) pairs once.

handles, labels = plt.gca().get_legend_handles_labels()

# labels will be the keys of the dict, handles will be values
temp = {k:v for k,v in zip(labels, handles)}

plt.legend(temp.values(), temp.keys(), loc='best')
3

I would do this trick:

for i in range(len(a)):
  plt.plot(a[i,::2].T, a[i, 1::2].T, 'r', label='data_a' if i==0 else None)
Chien Nguyen
  • 109
  • 1
  • 6
  • This did not work when I tried it. Throws `No handles with labels found to put in legend. ` when calling `plt.legend()` – Philipp Jun 13 '20 at 15:01
  • The complete code is: ```import numpy as np import matplotlib.pyplot as plt a = np.array([[ 3.57, 1.76, 7.42, 6.52], [ 1.57, 1.2 , 3.02, 6.88], [ 2.23, 4.86, 5.12, 2.81], [ 4.48, 1.38, 2.14, 0.86], [ 6.68, 1.72, 8.56, 3.23]]) for i in range(len(a)): plt.plot(a[i,::2].T, a[i, 1::2].T, 'r', label='data_a' if i==0 else None) plt.legend(loc='best') plt.savefig('test_plot_legend.png')``` – Chien Nguyen Jun 27 '20 at 10:50
1

I found short way to solve this:

a = np.array([[ 3.57,  1.76,  7.42,  6.52],
              [ 1.57,  1.2 ,  3.02,  6.88],
              [ 2.23,  4.86,  5.12,  2.81],
              [ 4.48,  1.38,  2.14,  0.86],
              [ 6.68,  1.72,  8.56,  3.23]])

p1=plt.plot(a[:,::2].T, a[:, 1::2].T, color='r')
plt.legend([p1[0]],['data_a'],loc='best')
0

If 'None' is provided as label, no label is printed. So just do this:

a = np.array([[ 3.57,  1.76,  7.42,  6.52],
              [ 1.57,  1.2 ,  3.02,  6.88],
              [ 2.23,  4.86,  5.12,  2.81],
              [ 4.48,  1.38,  2.14,  0.86],
              [ 6.68,  1.72,  8.56,  3.23]])

plt.plot(a[:,::2].T, a[:, 1::2].T, 'r', label=['data_a', None, None, None, None])

plt.legend(loc='best')

Plot From The Code Above

A more lazy alternative:

a = np.array([[ 3.57,  1.76,  7.42,  6.52],
              [ 1.57,  1.2 ,  3.02,  6.88],
              [ 2.23,  4.86,  5.12,  2.81],
              [ 4.48,  1.38,  2.14,  0.86],
              [ 6.68,  1.72,  8.56,  3.23]])

n_labels = 5
labels = [None for _ in range(n_labels)]
labels[0] = 'data_a'
plt.plot(a[:,::2].T, a[:, 1::2].T, 'r', label=labels)

plt.legend(loc='best')