Legend outside the plot in Python - matplotlib

2024/11/14 20:04:07

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?

Answer

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)

https://en.xdnf.cn/q/72138.html

Related Q&A

Filter items that only occurs once in a very large list

I have a large list(over 1,000,000 items), which contains english words:tokens = ["today", "good", "computer", "people", "good", ... ]Id like to get al…

Get Data JSON in Flask

Even following many example here & there, i cant get my API work in POST Method. Here the code about it :from flask import Flask, jsonify, request@app.route(/api/v1/lists, methods=[POST]) def add_e…

Commands working on windows command line but not in Git Bash terminal

I am trying to run certain commands in Git Bash but they continue to hang and not display anything. When I run them in the Windows command prompt they work.For example, in my windows command prompt the…

RBF interpolation: LinAlgError: singular matrix

The following call:rbf = Rbf(points[0], points[1], values,epsilon=2)results in an error:LinAlgError: singular matrixwith the following values:In [3]: points Out[3]: (array([71, 50, 48, 84, 71, 74, 89,…

What does `\x1b(B` do?

Im a Blessed user, and recently, when I tried to find out the contents of the term.bold() function, I got this output: \x1b[1m\x1b(B\x1b[mI understand what \x1b[1m and \x1b[m do, but what does \x1b(B d…

Not clicking all tabs and not looping once issues

I am trying to click the tabs on the webpage as seen below. Unfortunately, it only seems to click some of the tabs despite correct correct xpath in inspect Chrome. I can only assume it’s not clickin…

Opencv stream from a camera connected to a remote machine

I am developing a wx application in python for streaming and displaying video from two different webcams. This works fine, but now I need to do this in a different scenario in which the two cameras are…

Is there a callable equivalent to f-string syntax?

Everybody loves Python 3.6s new f-strings:In [33]: foo = {blah: bang}In [34]: bar = blahIn [35]: f{foo[bar]} Out[35]: bangHowever, while functionally very similar, they dont have the exact same semanti…

Python: list comprehension based on previous value? [duplicate]

This question already has answers here:Python list comprehension - access last created element(9 answers)Closed 10 months ago.Say I want to create a list using list comprehension like:l = [100., 50., 2…

How to run a coroutine inside a context?

In the Python docs about Context Vars a Context::run method is described to enable executing a callable inside a context so changes that the callable perform to the context are contained inside the cop…