6

I'm trying to place a rather extensive legend outside my plot in matplotlib. The legend has quite a few entries, and each entry can be quite long (but I don't know exactly how long).

Obviously, that's quite easy using

legendHandle = plt.legend(loc = "center left", bbox_to_anchor = (1, 0.5))

but the problem is that the legend is cut off by the edge of the window. I've spent quite a while searching for solutions for this. The best thing I could find so far was this:

box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
plt.legend(loc = "center left", bbox_to_anchor = (1, 0.5))

Unfortunately, this doesn't really solve my problem. Because of the explicit factor 0.8 applied to the box width, this only works with one specific combination of figure and legend width. If I resize the figure window, or if my legend entries have a different length, it doesn't work.

I just don't understand how placing a legend outside the figure can be so difficult. I'm used to Matlab, and there it's as simple as

legend('Location', 'eastoutside')

Is there something similar in Python that I'm missing?

Serenity
  • 35,289
  • 20
  • 120
  • 115
  • I'm not aware of anything similar to Matlab. How important is the total figsize to you? If it's not important, try this: http://stackoverflow.com/a/10154763 If it is important, try to create two subplots: one for the plot and an invisible one for the legend. Then you can create dummy artists (basically setting the same line/marker style for a non-existing drawing) in the empty subplot and show those dummy artists in the legend: http://stackoverflow.com/a/13319427/4085591 – StefanS May 19 '16 at 14:27
  • Thanks for your reply! Total figure size isn't really that relevant. Obviously I want it to fill whatever window size there is... so if my window is full screen, the figure should be larger, if my window covers only half the screen, the figure should be scaled down appropriately to accommodate the legend, and so on. –  May 19 '16 at 15:37
  • (continued) The problem with the first solution you suggested is that it only works once saving the figure. To give a bit more background, I have a (separate) tkinter window where I'm controlling which lines are shown and so on. That's also the reason why I don't know what size the legend will be. –  May 19 '16 at 15:42
  • (continued) I'll have a look at the second solution. I was thinking about this before, but it will be extremely tedious because (a) there is a lot of lines and (b) the lines change depending on the settings in the tkinter window, so I can't just hardcode the styles. This is really quite a nuisance... do you/anybody else know whether it would make sense to suggest this feature to the matplotlib team? It seems like something so fundamental to me that I'm really surprised it's missing. –  May 19 '16 at 15:46
  • Having a legend outside is actually really important for something like a scatterplot, where your legend items look like actual data if they are inside the chart. – jlansey Mar 01 '18 at 00:24

3 Answers3

2

After trying around a lot, this is the best I can come up with:

from matplotlib.lines import Line2D
from matplotlib.gridspec import GridSpec
from enum import Enum

class Location(Enum):
    EastOutside = 1
    WestOutside = 2
    NorthOutside = 3
    SouthOutside = 4

class Legend:
    def __init__(self, figure, plotAxes, location: Location):
        self.figure = figure
        self.plotAxes = plotAxes
        self.location = location

        # Create a separate subplot for the legend. Actual location doesn't matter - will be modified anyway.
        self.legendAxes = figure.add_subplot(1, 2, 1)
        self.legendAxes.clear() # remove old lines
        self.legendAxes.set_axis_off()

        # Add all lines from the plot to the legend subplot
        for line in plotAxes.get_lines():
            legendLine = Line2D([], [])
            legendLine.update_from(line)
            self.legendAxes.add_line(legendLine)

        if self.location == Location.EastOutside:
            self.legend = self.legendAxes.legend(loc = "center left")
        elif self.location == Location.WestOutside:
            self.legend = self.legendAxes.legend(loc = "center right")
        elif self.location == Location.NorthOutside:
            self.legend = self.legendAxes.legend(loc = "lower center")
        elif self.location == Location.SouthOutside:
            self.legend = self.legendAxes.legend(loc = "upper center")
        else:
            raise Exception("Unknown legend location.")

        self.UpdateSize()

        # Recalculate legend size if the size changes
        figure.canvas.mpl_connect('resize_event', lambda event: self.UpdateSize())

    def UpdateSize(self):
        self.figure.canvas.draw() # draw everything once in order to get correct legend size

        # Extract legend size in percentage of the figure width
        legendSize = self.legend.get_window_extent().inverse_transformed(self.figure.transFigure)
        legendWidth = legendSize.width
        legendHeight = legendSize.height

        # Update subplot such that it is only as large as the legend
        if self.location == Location.EastOutside:
            gridspec = GridSpec(1, 2, width_ratios = [1 - legendWidth, legendWidth])
            legendLocation = 1
            plotLocation = 0
        elif self.location == Location.WestOutside:
            gridspec = GridSpec(1, 2, width_ratios = [legendWidth, 1 - legendWidth])
            legendLocation = 0
            plotLocation = 1
        elif self.location == Location.NorthOutside:
            gridspec = GridSpec(2, 1, height_ratios = [legendHeight, 1 - legendHeight])
            legendLocation = 0
            plotLocation = 1
        elif self.location == Location.SouthOutside:
            gridspec = GridSpec(2, 1, height_ratios = [1 - legendHeight, legendHeight])
            legendLocation = 1
            plotLocation = 0
        else:
            raise Exception("Unknown legend location.")

        self.legendAxes.set_position(gridspec[legendLocation].get_position(self.figure))
        self.legendAxes.set_subplotspec(gridspec[legendLocation]) # to make figure.tight_layout() work if that's desired

        self.plotAxes.set_position(gridspec[plotLocation].get_position(self.figure))
        self.plotAxes.set_subplotspec(gridspec[plotLocation]) # to make figure.tight_layout() work if that's desired

This places the legend more or less alright in the cases I have tested so far. Usage is e.g.

import matplotlib.pyplot as plt

plt.ion()

figure = plt.figure()

plotAxes = figure.gca()

plotAxes.plot([1, 2, 3], [4, 5, 6], "b-", label = "Testaaaaaaaaaaaaaa 1")
plotAxes.plot([1, 2, 3], [6, 5, 4], "r-", label = "Test 2")

legend = Legend(figure, plotAxes, Location.EastOutside)

Let me ask the question I posted in a comment already... how would I go about suggesting this as an additional feature to the matplotlib developers? (Not my hack, but a native way of having the legend outside the figure)

1

Just stumbled across this question, I think you can use the figure's legend method:

import matplotlib.pyplot as plt

# sample data
x1 = [1,2,3,4,5,6]
y1 = [0,1,0,1,0,1]
x2 = [1,2,3,4,5,6]
y2 = [9,8,8,7,8,6]

fig = plt.figure(figsize=(4, 2), dpi=100)
ax = fig.add_subplot(111)

ax.plot(x1,y1,x2,y2)

fig.legend(loc=1, mode='expand', numpoints=1, ncol=4, fancybox = True,
           fontsize='small', labels=['d1', 'd2'])
# loc=1 means at the top-right of the figure

plt.show()

Although the legend is placed outside the axis, it might still overlap with the axis, if the legend is too big.

Pyplot legend outside of axis

Snow bunting
  • 1,120
  • 8
  • 28
0

The placement of a legend can be controlled by using the parameters loc and bbox_to_anchor. Here is a code example:

import matplotlib.pyplot as plt

#sample data
import numpy as np
x = np.linspace(0, 2, 101)

#create figure and its axes
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])

#plot 3 lines and define their labels
ax.plot(x, x**2, label="square")
ax.plot(x, x**3, label="cubic")
ax.plot(x, np.sin(x), label="sinus")

#place the legend
ax.legend(loc='lower center', bbox_to_anchor=(0.5, 1.0), ncol=3)
  # The center of the lower edge of the rectangle containing the legend
  # is placed at coordinates (x,y)=(0.5,1.0) of ax. 
  # Thus, figure and legend should not overlap.

plt.show()

You should now see the following figure with a legend outside:

1

Mario
  • 1,631
  • 2
  • 21
  • 51
BMSL
  • 1
  • 1
  • 1