Search code examples
python-3.xmatplotliblegendaxes

Matplotlib axes.legend changes image size (image aspect?)


This question is most likely related to a previous question I had that arose during refactoring some code (to re-use the matplotlib axes object) that is linked here. That question described an issue where using imshow would adjust the image.aspect parameter.

I am now encoutering an issue that gives a similar effect but I have been unable to fix it by forcing the image.aspect parameter to auto, which was my first thought as it looks like that is being adjusted somehow. I have also been unable to find anything digging through the underlying documentation found here.

Here are pictures illustrating the issue (Obscured legend entries):

Expected behaviour (limited number of legend entries) enter image description here

Unexpected behaviour (large number of legend entries) enter image description here

A MVCE for the above behaviour (with utter nonsense data) is listed below, where the behaviour can be toggled between expected and unexpected (for me) by toggling the upper limit in the for i in range(1,10): line to 50.

from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg
)
import tkinter as tk
import numpy
from matplotlib import image, figure

class Foo(object):
    @classmethod
    def run(cls):
        root = tk.Tk()
        Foo(root)
        root.mainloop()

    def __init__(self, master):
        self.fig = figure.Figure(figsize=(12,6))
        self.fig.set_tight_layout(True)
        self.axes = self.fig.add_subplot(111)
        self.axes.axis('off')
        self.canvas = FigureCanvasTkAgg(self.fig, master=master)
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=tk.YES)
        self.canvas.draw()
        self.data = []
        # Adjust the upper value to 10 or 50
        for i in range(1,10):
            self.axes.plot(range(0,100),numpy.random.randint(1,100,100), label=str(i))
        self.axes.legend()


if __name__ == '__main__':
    Foo.run()

I would love to get a hint as to what parameter or setting I am struggling with this time.


Solution

  • Here tight_layout is trying to fit every artist drawn, into the figure with as little padding as possible. When the number of legend entries is very large this will give some undesired results.

    In matplotlib versions > 3 an artist has a property set_in_layout (docs for tight_layout) which you can set to either True or False to include or omit it from the calculations done when tight_layout is called. Therefore you can modify your __init__ to look something like:

    def __init__(self, master):
        self.fig = figure.Figure(figsize=(12,6))
        self.fig.set_tight_layout(True)
        self.axes = self.fig.add_subplot(111)
        self.axes.axis('off')
        self.canvas = FigureCanvasTkAgg(self.fig, master=master)
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=tk.YES)
        self.canvas.draw()
        self.data = []
        # Adjust the upper value to 10 or 50
        for i in range(1,50):
            self.axes.plot(range(0,100),numpy.random.randint(1,100,100), label=str(i))
        self.leg = self.axes.legend()
        self.leg.set_in_layout(False)  # set to False so it's not used in tight_layout calculation!
    

    An alternative (but less than ideal) option would be to remove self.fig.set_tight_layout(True) and adjust the whitespace yourself using self.fig.subplots_adjust(), however this may require some manual tweaking of the parameters to get the desired result.