How is order of items in matplotlib legend determined?

Question:

I am having to reorder items in a legend, when I don’t think I should have to. I try:

from pylab import *
clf()
ax=gca()
ht=ax.add_patch(Rectangle((1,1),1,1,color='r',label='Top',alpha=.1))
h1=ax.bar(1,2,label='Middle')
hb=ax.add_patch(Rectangle((1,1),1,1,color='k',label='Bottom',alpha=.11))
legend()
show()

and end up with Bottom above Middle. How can I get the right order? Is it not determined by creation order?

Code results in wrong legend item order

Update: The following can be used to force the order. I think this may be the simplest way to do it, and that seems awkward. The question is what determines the original order?

hh=[ht,h1,hb]
legend([ht,h1.patches[0],hb],[H.get_label() for H in hh])
Asked By: CPBL

||

Answers:

The order is deterministic, but part of the private guts so can be changed at any time, see the code here which goes to here and eventually here. The children are the artists that have been added, hence the handle list is sorted by order they were added (this is a change in behavior with mpl34 or mpl35).

If you want to explicitly control the order of the elements in your legend then assemble a list of handlers and labels like you did in the your edit.

Answered By: tacaswell

Here’s a quick snippet to sort the entries in a legend. It assumes that you’ve already added your plot elements with a label, for example, something like

ax.plot(..., label='label1')
ax.plot(..., label='label2')

and then the main bit:

handles, labels = ax.get_legend_handles_labels()
# sort both labels and handles by labels
labels, handles = zip(*sorted(zip(labels, handles), key=lambda t: t[0]))
ax.legend(handles, labels)

This is just a simple adaptation from the code listed at http://matplotlib.org/users/legend_guide.html

Answered By: kevin

The following function makes control of legend order easy and readable.

You can specify the order you want by label. It will find the legend handles and labels, drop duplicate labels, and sort or partially sort them according to your given list (order). So you use it like this:

reorderLegend(ax,['Top', 'Middle', 'Bottom'])

Details are below.

#  Returns tuple of handles, labels for axis ax, after reordering them to conform to the label order `order`, and if unique is True, after removing entries with duplicate labels.
def reorderLegend(ax=None,order=None,unique=False):
    if ax is None: ax=plt.gca()
    handles, labels = ax.get_legend_handles_labels()
    labels, handles = zip(*sorted(zip(labels, handles), key=lambda t: t[0])) # sort both labels and handles by labels
    if order is not None: # Sort according to a given list (not necessarily complete)
        keys=dict(zip(order,range(len(order))))
        labels, handles = zip(*sorted(zip(labels, handles), key=lambda t,keys=keys: keys.get(t[0],np.inf)))
    if unique:  labels, handles= zip(*unique_everseen(zip(labels,handles), key = labels)) # Keep only the first of each handle
    ax.legend(handles, labels)
    return(handles, labels)


def unique_everseen(seq, key=None):
    seen = set()
    seen_add = seen.add
    return [x for x,k in zip(seq,key) if not (k in seen or seen_add(k))]
 

The function in updated form is in cpblUtilities.mathgraph at https://gitlab.com/cpbl/cpblUtilities/blob/master/mathgraph.py

Usage is thus like this:

fig, ax = plt.subplots(1)
ax.add_patch(Rectangle((1,1),1,1,color='r',label='Top',alpha=.1))
ax.bar(1,2,label='Middle')
ax.add_patch(Rectangle((.8,.5),1,1,color='k',label='Bottom',alpha=.1))
legend()


reorderLegend(ax,['Top', 'Middle', 'Bottom'])
show()

The optional unique argument makes sure to drop duplicate plot objects which have the same label.

Figure after re-ordering labels

Answered By: CPBL

A slight variation on some other aswers. The list order should have the same length as the number of legend items, and specifies the new order manually.

handles, labels = plt.gca().get_legend_handles_labels()
order = [0,2,1]
plt.legend([handles[idx] for idx in order],[labels[idx] for idx in order])
Answered By: Ian Hincks

A simple way to sort the labels based on another list goes like this:
after you add all your plots and labels to the axes, do the following steps before displaying the label.

handles,labels = ax.get_legend_handles_labels()
sorted_legends= [x for _,x in sorted(zip(k,labels),reverse=True)] 
#sort the labels based on the list k
#reverse=True sorts it in descending order
sorted_handles=[x for _,x in sorted(zip(k,handles),reverse=True)]
#to sort the colored handles
ax.legend(sorted_handles,sorted_legends,bbox_to_anchor=(1,0.5), loc='center left')
#display the legend on the side of your plot.

Example:

from matplotlib import pyplot as plt
import numpy as np


rollno=np.arange(1,11)
marks_math=np.random.randint(30,100,10)
marks_science=np.random.randint(30,100,10)
marks_english=np.random.randint(30,100,10)
print("Roll No. of the students: ",rollno)
print("Marks in Math: ",marks_math)
print("Marks in Science: ",marks_science)
print("Marks in English: ",marks_english)
average=[np.average(marks_math),np.average(marks_science),np.average(marks_english)] #storing the average of each subject in a list

fig1=plt.figure()
ax=fig1.add_subplot(1,1,1)
ax.set_xlabel("Roll No.")
ax.set_ylabel("Marks")
ax.plot(rollno,marks_math,c="red",label="marks in math, Mean="+str(average[0]))
ax.plot(rollno,marks_science,c="green",label="marks in science, Mean="+str(average[1]))
ax.plot(rollno,marks_english,c="blue",label="marks in english, Mean="+str(average[2]))
#ax.legend() # This would display the legend with red color first, green second and the blue at last

#but we want to sort the legend based on the average marks which must order the labels based on average sorted in decending order
handles,labels=ax.get_legend_handles_labels()
sorted_legends= [x for _,x in sorted(zip(average,labels),reverse=True)] #sort the labels based on the average which is on a list
sorted_handles=[x for _,x in sorted(zip(average,handles),reverse=True)] #sort the handles based on the average which is on a list
ax.legend(sorted_handles,sorted_legends,bbox_to_anchor=(1,0.5), loc='center left') #display the handles and the labels on the side
plt.show()
plt.close()

For a run that had the values like this:

Roll No. of the students:  [ 1  2  3  4  5  6  7  8  9 10]
Marks in Math:  [66 46 44 70 37 72 93 32 81 84]
Marks in Science:  [71 99 99 40 59 80 72 98 91 81]
Marks in English:  [46 64 74 33 86 49 84 92 67 35]
The average in each subject [62.5, 79.0, 63.0]

The labels would have come in the order red, green, and blue as they come in that order in the plot but we want to sort them based on the average which would give us an order green, blue and red.

Check this image

Answered By: Binod

Playing off of Ian Hincks’s answer, the changing of the order of the legend elements can be done in one line with a nested list comprehension. This avoids the need to name intermediate variables and reduces code duplication.

plt.legend(*(
    [ x[i] for i in [2,1,0] ]
    for x in plt.gca().get_legend_handles_labels()
), handletextpad=0.75, loc='best')

I threw in some additional arguments at the end to illustrate that the plt.legend() function does not need to be called separately for formatting and ordering of the elements.

Answered By: SU3

I had several plots with the same theme in a figure. When I tried the answers above based on changing the labels, I found that side-by-side plots would end up with different colors for the same label.

So the simplest solution that worked for me is to sort by the desired label before creating the plot:

# Pandas adds the series in random order, we'll need to sort before plotting below...
pd.plotting.parallel_coordinates(
    df.sort_values(by='tier_label'), 
    ax=ax,
    class_column='tier_label', 
    alpha=0.5, color='#EDBB00 #004D98 #A50044'.split())

This of course requires a sort, so you decide if it is applicable to your situation. Also, if you need the label text to be different than the class_column then you may need to add a bit more code.

Answered By: Leo
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.