13
votes

I would like to make a complex legend in matplotlib. I made the following code

import matplotlib.pylab as plt
import numpy as np

N = 25
y = np.random.randn(N)
x = np.arange(N)

y2 = np.random.randn(25)

# serie A
p1a, = plt.plot(x, y,       "ro", ms=10, mfc="r", mew=2, mec="r")
p1b, = plt.plot(x[:5], y[:5] ,  "w+", ms=10, mec="w", mew=2) 
p1c, = plt.plot(x[5:10], y[5:10], "w*", ms=10, mec="w", mew=2) 

# serie B
p2a, = plt.plot(x, y2,       "bo", ms=10, mfc="b", mew=2, mec="b")
p2b, = plt.plot(x[15:20], y2[15:20] ,  "w+", ms=10, mec="w", mew=2) 
p2c, = plt.plot(x[10:15], y2[10:15], "w*", ms=10, mec="w", mew=2) 


plt.legend([p1a, p2a, (p1a, p1b), (p2a,p2b), (p1a, p1c), (p2a,p2c)], 
 ["No prop", "No prop", "Prop +", "Prop +", "Prop *", "Prop *"], ncol=3, numpoints=1)

plt.show()

It produces plot like that: enter image description here

But I would like to plot complex legend like here:

enter image description here

I also tried to do the legend with table function but I can not put a patch object into the table to a proper position of a cell.

3
I'm not positive but I believe there is an example doing exactly that in the accepted answer to this question. Or it may at least point you in the right direction? - Ajean
No, in that example every marker has its own label. - Serenity
Right, but you can put empty strings there. I was actually looking for a different example I've seen here before (somebody wrote a legend that was a thing of beauty) but I couldn't track it down. Just a thought, because I think that one used empty strings. Sorry I can't find it... - Ajean
Yep, I tried it too. Result is very ugly: i.imgur.com/JHbNt3z.png - Serenity
There is not a top-level configurable way to do that. The tools to do this are there (handler creation tools, layout boxes hpackers / vpackers etc) but it would be fun (and would be a good thing to end up at least in our docs) as an alternative legend implementation. - tacaswell

3 Answers

6
votes

Is this solution close enough to your liking? It is slighty inspired by Ricardo's answer, but I only used one legend-object for each column, and then utilised the title-keyword to set the title of each individual column. To put the markers in the center of each column I used handletextpad with a negative value to push it backward. There are no legends to individual lines. I also had to insert some spaces into the title-strings to make them look equally big when drawn on screen.

I also noticed now when the figure was saved that additional tweaks to the exact position of the legend-boxes and are needed, but since I guess you might want to tweak more stuff in the code anyway I leave it for you. You might also need to play yourself with the handletextpad to make them "perfectly" aligned.

import matplotlib.pylab as plt
import numpy as np
plt.close('all')

N = 25
y = np.random.randn(N)
x = np.arange(N)

y2 = np.random.randn(25)

# serie A
p1a, = plt.plot(x, y,       "ro", ms=10, mfc="r", mew=2, mec="r")
p1b, = plt.plot(x[:5], y[:5] ,  "w+", ms=10, mec="w", mew=2) 
p1c, = plt.plot(x[5:10], y[5:10], "w*", ms=10, mec="w", mew=2) 

# serie B
p2a, = plt.plot(x, y2,       "bo", ms=10, mfc="b", mew=2, mec="b")
p2b, = plt.plot(x[15:20], y2[15:20] ,  "w+", ms=10, mec="w", mew=2) 
p2c, = plt.plot(x[10:15], y2[10:15], "w*", ms=10, mec="w", mew=2) 

line_columns = [
                p1a, p2a,
                (p1a, p1b), (p2a, p2b),
                (p1a, p1c), (p2a, p2c)
                ]


leg1 = plt.legend(line_columns[0:2], ['', ''], ncol=1, numpoints=1, 
                  title='No prop', handletextpad=-0.4, 
                  bbox_to_anchor=[0.738, 1.])
leg2 = plt.legend(line_columns[2:4], ['', ''], ncol=1, numpoints=1, 
                  title=' Prop  + ', handletextpad=-0.4,
                  bbox_to_anchor=[0.87, 1.])
leg3 = plt.legend(line_columns[4:6], ['', ''], ncol=1, numpoints=1, 
                  title=' Prop  * ', handletextpad=-0.4, 
                  bbox_to_anchor=[0.99, 1.])

plt.gca().add_artist(leg1)
plt.gca().add_artist(leg2)
plt.gca().add_artist(leg3)

plt.gcf().show()

enter image description here

Edit

Maybe this will work better. You still have to tweak a few stuff, but the alignment-problem of the bboxes are away.

leg = plt.legend(line_columns, ['']*len(line_columns), 
             title='No Prop    Prop +    Prop *',  
             ncol=3, numpoints=1, handletextpad=-0.5)

enter image description here

3
votes

It seems there is no standard approach for this rather than some few tricks available over here.

It is worth mentioning that you should check the size bbox factor that fits you the most.

The best I could find so far, an perhaps can lead you to a better solution:

N = 25
y = np.random.randn(N)
x = np.arange(N)

y2 = np.random.randn(25)

# Get current size
fig_size = list(plt.rcParams["figure.figsize"])

# Set figure width to 12 and height to 9
fig_size[0] = 12
fig_size[1] = 12
plt.rcParams["figure.figsize"] = fig_size

# serie A
p1a, = plt.plot(x, y,       "ro", ms=10, mfc="r", mew=2, mec="r")
p1b, = plt.plot(x[:5], y[:5] ,  "w+", ms=10, mec="w", mew=2) 
p1c, = plt.plot(x[5:10], y[5:10], "w*", ms=10, mec="w", mew=2) 

# serie B
p2a, = plt.plot(x, y2,       "bo", ms=10, mfc="b", mew=2, mec="b")
p2b, = plt.plot(x[15:20], y2[15:20] ,  "w+", ms=10, mec="w", mew=2) 
p2c, = plt.plot(x[10:15], y2[10:15], "w*", ms=10, mec="w", mew=2) 

v_factor = 1.
h_factor = 1.

leg1 = plt.legend([(p1a, p1a)], ["No prop"], bbox_to_anchor=[0.78*h_factor, 1.*v_factor])
leg2 = plt.legend([(p2a, p2a)], ["No prop"], bbox_to_anchor=[0.78*h_factor, .966*v_factor])

leg3 = plt.legend([(p2a,p2b)], ["Prop +"], bbox_to_anchor=[0.9*h_factor, 1*v_factor])
leg4 = plt.legend([(p1a, p1b)], ["Prop +"], bbox_to_anchor=[0.9*h_factor, .966*v_factor])

leg5 = plt.legend([(p1a, p1c)], ["Prop *"], bbox_to_anchor=[1.*h_factor, 1.*v_factor])
leg6 = plt.legend([(p2a,p2c)], ["Prop *"], bbox_to_anchor=[1.*h_factor, .966*v_factor])

plt.gca().add_artist(leg1)
plt.gca().add_artist(leg2)
plt.gca().add_artist(leg3)
plt.gca().add_artist(leg4)
plt.gca().add_artist(leg5)
plt.gca().add_artist(leg6)
plt.show()

enter image description here

1
votes

I improve the answer of @pathoren to position legends automatically in a cycle according to bbox coordinate of legends. This code allow to show all desired border lines of a complex legend:

import matplotlib.pylab as plt
import numpy as np
plt.close('all')

# test data
N = 25
y = np.random.randn(N)
x = np.arange(N)
y2 = np.random.randn(25)

# serie A
p1a, = plt.plot(x, y, "ro", ms=10, mfc="r", mew=2, mec="r")
p1b, = plt.plot(x[:5], y[:5], "w+", ms=10, mec="w", mew=2) 
p1c, = plt.plot(x[5:10], y[5:10], "w*", ms=10, mec="w", mew=2) 
# serie B
p2a, = plt.plot(x, y2, "bo", ms=10, mfc="b", mew=2, mec="b")
p2b, = plt.plot(x[15:20], y2[15:20], "w+", ms=10, mec="w", mew=2) 
p2c, = plt.plot(x[10:15], y2[10:15], "w*", ms=10, mec="w", mew=2) 

# legend handlers
columns = [p1a, p2a, 
 (p1a, p1b), (p2a, p2b),
 (p1a, p1c), (p2a, p2c)]

ax = plt.gca()
fig = plt.gcf()
legs = []
# set the first legend in desired position
leg = plt.legend(columns[0:2], ['', ''], ncol=1, numpoints=1, 
 borderaxespad=0., title='No prop.', framealpha=.75,
 facecolor='w', edgecolor='k', loc=2, fancybox=None)
ax.add_artist(leg)
fig.canvas.draw()
plt.pause(1.e-3)

# get bbox postion of 1st legend to calculate
# postion of 2nd and 3rd legends according to loc
for i,si in enumerate(['+','*']):
    bbox = leg.get_window_extent().inverse_transformed(ax.transAxes)
    # next legends
    leg = plt.legend(columns[(i+1)*2:(i+1)*2+2], ['', ''], ncol=1, numpoints=1, 
     title='Prop. '+si, framealpha=.75, borderaxespad=.0,
     bbox_to_anchor=(bbox.x1-bbox.height*.08, bbox.y0, bbox.width, bbox.height),
     facecolor='w', edgecolor='k')
    ax.add_artist(leg)
    fig.canvas.draw()
    plt.pause(1.e-3)

plt.show()

enter image description here