Legend outside the plot in Python – matplotlib

Question:

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?

Asked By: user6356258

||

Answers:

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)

Answered By: user6356258

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

Answered By: Snow bunting

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

Answered By: BMSL
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.