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?
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 Enumclass Location(Enum):EastOutside = 1WestOutside = 2NorthOutside = 3SouthOutside = 4class Legend:def __init__(self, figure, plotAxes, location: Location):self.figure = figureself.plotAxes = plotAxesself.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 linesself.legendAxes.set_axis_off()# Add all lines from the plot to the legend subplotfor 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 changesfigure.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 widthlegendSize = self.legend.get_window_extent().inverse_transformed(self.figure.transFigure)legendWidth = legendSize.widthlegendHeight = legendSize.height# Update subplot such that it is only as large as the legendif self.location == Location.EastOutside:gridspec = GridSpec(1, 2, width_ratios = [1 - legendWidth, legendWidth])legendLocation = 1plotLocation = 0elif self.location == Location.WestOutside:gridspec = GridSpec(1, 2, width_ratios = [legendWidth, 1 - legendWidth])legendLocation = 0plotLocation = 1elif self.location == Location.NorthOutside:gridspec = GridSpec(2, 1, height_ratios = [legendHeight, 1 - legendHeight])legendLocation = 0plotLocation = 1elif self.location == Location.SouthOutside:gridspec = GridSpec(2, 1, height_ratios = [1 - legendHeight, legendHeight])legendLocation = 1plotLocation = 0else: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 desiredself.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 pltplt.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)