Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Matplotlib customize the legend to show squares instead of rectangles

Here is my attempt to change the legend of a barplot from rectangle to square:

import matplotlib.patches as patches

rect1 = patches.Rectangle((0,0),1,1,facecolor='#FF605E')
rect2 = patches.Rectangle((0,0),1,1,facecolor='#64B2DF')
plt.legend((rect1, rect2), ('2016', '2015'))

But when I plot this, I still see rectangles instead of squares:

enter image description here

Any suggestions on how can I do this?


I tried both solutions provided by @ImportanceOfBeingErnest and @furas, here are the results:

@ImportanceOfBeingErnest's solution is the easiest to do:

plt.rcParams['legend.handlelength'] = 1
plt.rcParams['legend.handleheight'] = 1.125

Here is the result:

enter image description here

My final code looks like this:

plt.legend((df.columns[1], df.columns[0]), handlelength=1, handleheight=1) # the df.columns = the legend text

@furas's solution produces this, I don't know why the texts are further away from the rectangles, but I am sure the gap can be changed somehow:

enter image description here

like image 455
Cheng Avatar asked Nov 18 '16 08:11

Cheng


2 Answers

Matplotlib provides the rcParams

legend.handlelength  : 2.    # the length of the legend lines in fraction of fontsize
legend.handleheight  : 0.7   # the height of the legend handle in fraction of fontsize

You can set those within the call to plt.legend()

plt.legend(handlelength=1, handleheight=1)

or using the rcParams at the beginning of your script

import matplotlib
matplotlib.rcParams['legend.handlelength'] = 1
matplotlib.rcParams['legend.handleheight'] = 1

Unfortunately providing equal handlelength=1, handleheight=1 will not give a perfect rectange. It seems handlelength=1, handleheight=1.125 will do the job, but this may depend on the font being used.


An alternative, if you want to use proxy artists may be to use the square markers from the plot/scatter methods.

bar1 = plt.plot([], marker="s", markersize=15, linestyle="", label="2015")

and supply it to the legend, legend(handles=[bar1]). Using this approach needs to have set matplotlib.rcParams['legend.numpoints'] = 1, otherwise two markers would appear in the legend.


Here is a full example of both methods
import matplotlib.pyplot as plt
plt.rcParams['legend.handlelength'] = 1
plt.rcParams['legend.handleheight'] = 1.125
plt.rcParams['legend.numpoints'] = 1


fig, ax = plt.subplots(ncols=2, figsize=(5,2.5))

# Method 1: Set the handlesizes already in the rcParams
ax[0].set_title("Setting handlesize")
ax[0].bar([0,2], [6,3], width=0.7, color="#a30e73", label="2015", align="center")
ax[0].bar([1,3], [3,2], width=0.7, color="#0943a8", label="2016", align="center" )
ax[0].legend()

# Method 2: use proxy markers. (Needs legend.numpoints to be 1)
ax[1].set_title("Proxy markers")
ax[1].bar([0,2], [6,3], width=0.7, color="#a30e73", align="center" )
ax[1].bar([1,3], [3,2], width=0.7, color="#0943a8", align="center" )
b1, =ax[1].plot([], marker="s", markersize=15, linestyle="", color="#a30e73",  label="2015")
b2, =ax[1].plot([], marker="s", markersize=15, linestyle="", color="#0943a8",  label="2016")
ax[1].legend(handles=[b1, b2])

[a.set_xticks([0,1,2,3]) for a in ax]
plt.show()

producing

enter image description here

like image 160
ImportanceOfBeingErnest Avatar answered Oct 28 '22 13:10

ImportanceOfBeingErnest


It seems they change it long time ago - and now some elements can't be used directly in legend.

Now it needs handler: Implementing a custom legend handler

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.legend_handler import HandlerPatch

# --- handlers ---

class HandlerRect(HandlerPatch):

    def create_artists(self, legend, orig_handle,
                       xdescent, ydescent, width, height,
                       fontsize, trans):

        x = width//2
        y = 0
        w = h = 10

        # create
        p = patches.Rectangle(xy=(x, y), width=w, height=h)

        # update with data from oryginal object
        self.update_prop(p, orig_handle, legend)

        # move xy to legend
        p.set_transform(trans)

        return [p]


class HandlerCircle(HandlerPatch):

    def create_artists(self, legend, orig_handle,
                       xdescent, ydescent, width, height,
                       fontsize, trans):

        r = 5
        x = r + width//2
        y = height//2

        # create 
        p = patches.Circle(xy=(x, y), radius=r)

        # update with data from oryginal object
        self.update_prop(p, orig_handle, legend)

        # move xy to legend
        p.set_transform(trans)

        return [p]

# --- main ---

rect = patches.Rectangle((0,0), 1, 1, facecolor='#FF605E')
circ = patches.Circle((0,0), 1, facecolor='#64B2DF')

plt.legend((rect, circ), ('2016', '2015'),
            handler_map={
               patches.Rectangle: HandlerRect(),
               patches.Circle: HandlerCircle(),
            })

plt.show()

Legend reserves place for rectangle and this method doesn't change it so there is so many empty space.

enter image description here

like image 41
furas Avatar answered Oct 28 '22 14:10

furas