298

I'm familiar with the following questions:

Matplotlib savefig with a legend outside the plot

How to put the legend out of the plot

It seems that the answers in these questions have the luxury of being able to fiddle with the exact shrinking of the axis so that the legend fits.

Shrinking the axes, however, is not an ideal solution because it makes the data smaller making it actually more difficult to interpret; particularly when its complex and there are lots of things going on ... hence needing a large legend

The example of a complex legend in the documentation demonstrates the need for this because the legend in their plot actually completely obscures multiple data points.

http://matplotlib.sourceforge.net/users/legend_guide.html#legend-of-complex-plots

What I would like to be able to do is dynamically expand the size of the figure box to accommodate the expanding figure legend.

import matplotlib.pyplot as plt
import numpy as np

x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))
ax.grid('on')

Notice how the final label 'Inverse tan' is actually outside the figure box (and looks badly cutoff - not publication quality!) enter image description here

Finally, I've been told that this is normal behaviour in R and LaTeX, so I'm a little confused why this is so difficult in python... Is there a historical reason? Is Matlab equally poor on this matter?

I have the (only slightly) longer version of this code on pastebin http://pastebin.com/grVjc007

3
  • 10
    As far as the why's it's because matplotlib is geared towards interactive plots, while R, etc, aren't. (And yes, Matlab is "equally poor" in this particular case.) To do it properly, you need to worry about resizing the axes every time the figure is resized, zoomed, or the legend's position is updated. (Effectively, this means checking every time the plot is drawn, which leads to slowdowns.) Ggplot, etc, are static, so that's why they tend to do this by default, whereas matplotlib and matlab don't. That having been said, tight_layout() should be changed to take legends into account. Commented Apr 11, 2012 at 16:03
  • 3
    I'm also discussing this question on the matplotlib users mailing list. So I have the suggestions of adjusting the savefig line to: fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox='tight') Commented Apr 13, 2012 at 6:23
  • 7
    I know matplotlib likes to tout that everything is under the control of the user, but this entire thing with the legends is too much of a good thing. If I put the legend outside, I obviously want it to still be visible. The window should just scale itself to fit instead of creating this huge rescaling hassle. At the very least there should be a default True option to control this autoscaling behavior. Forcing users to go through a ridiculous number of re-renders to try and get the scale numbers right in the name of control accomplishes the opposite.
    – Elliot
    Commented Jan 2, 2013 at 21:50

7 Answers 7

401

Sorry EMS, but I actually just got another response from the matplotlib mailling list (Thanks goes out to Benjamin Root).

The code I am looking for is adjusting the savefig call to:

fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox_inches='tight')
#Note that the bbox_extra_artists must be an iterable

This is apparently similar to calling tight_layout, but instead you allow savefig to consider extra artists in the calculation. This did in fact resize the figure box as desired.

import matplotlib.pyplot as plt
import numpy as np

plt.gcf().clear()
x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
handles, labels = ax.get_legend_handles_labels()
lgd = ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5,-0.1))
text = ax.text(-0.2,1.05, "Aribitrary text", transform=ax.transAxes)
ax.set_title("Trigonometry")
ax.grid('on')
fig.savefig('samplefigure', bbox_extra_artists=(lgd,text), bbox_inches='tight')

This produces:

[edit] The intent of this question was to completely avoid the use of arbitrary coordinate placements of arbitrary text as was the traditional solution to these problems. Despite this, numerous edits recently have insisted on putting these in, often in ways that led to the code raising an error. I have now fixed the issues and tidied the arbitrary text to show how these are also considered within the bbox_extra_artists algorithm.

[edit] Some of the comments below note that since 2019, the command has been simplified. plt.savefig('x.png', bbox_inches='tight') was sufficient. Thanks for sharing. – mateuszb Jun 27, 2019

21
  • 1
    /!\ Seems to work only since matplotlib >= 1.0 (Debian squeeze have 0.99 and this does not work) Commented Dec 4, 2012 at 15:26
  • 1
    Can't get this to work :( I pass in lgd to savefig but it still doesn't resize. The problem may be I'm not using a subplot. Commented Mar 15, 2015 at 6:39
  • 17
    Ah! I just needed to use bbox_inches = "tight" as you did. Thanks! Commented Mar 15, 2015 at 6:44
  • 12
    This is nice, but I still get my figure cut when I try to plt.show() it. Any fix for that?
    – Agostino
    Commented Mar 26, 2015 at 17:51
  • 6
    It does not work if use fig.legend() method , really weird.
    – jdhao
    Commented Jan 7, 2018 at 16:29
35

Added: I found something that should do the trick right away, but the rest of the code below also offers an alternative.

Use the subplots_adjust() function to move the bottom of the subplot up:

fig.subplots_adjust(bottom=0.2) # <-- Change the 0.02 to work for your plot.

Then play with the offset in the legend bbox_to_anchor part of the legend command, to get the legend box where you want it. Some combination of setting the figsize and using the subplots_adjust(bottom=...) should produce a quality plot for you.

Alternative: I simply changed the line:

fig = plt.figure(1)

to:

fig = plt.figure(num=1, figsize=(13, 13), dpi=80, facecolor='w', edgecolor='k')

and changed

lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))

to

lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,-0.02))

and it shows up fine on my screen (a 24-inch CRT monitor).

Here figsize=(M,N) sets the figure window to be M inches by N inches. Just play with this until it looks right for you. Convert it to a more scalable image format and use GIMP to edit if necessary, or just crop with the LaTeX viewport option when including graphics.

3
  • 1
    It would seem that this is the best solution at the current time, even though it still requires 'playing until it looks good' which is not a good solution for a autoreport generator. I actually already use this solution, the real problem is that matplotlib doesn't dynamically compensate for the legend being outside the bbox of the axis. As @Joe said, tight_layout should take into account more features than just axis, titles and lables. I might add this as a feature request on the matplotlib. Commented Apr 14, 2012 at 15:11
  • also works for me to get a big enough picture to fit the xlabels previously being cut off Commented Feb 20, 2014 at 17:57
  • 1
    here is the documentation with example code from matplotlib.org
    – Yojimbo
    Commented Feb 18, 2015 at 17:26
18

Here is another, very manual solution. You can define the size of the axis and paddings are considered accordingly (including legend and tickmarks). Hope it is of use to somebody.

Example (axes size are the same!):

enter image description here

Code:

#==================================================
# Plot table

colmap = [(0,0,1) #blue
         ,(1,0,0) #red
         ,(0,1,0) #green
         ,(1,1,0) #yellow
         ,(1,0,1) #magenta
         ,(1,0.5,0.5) #pink
         ,(0.5,0.5,0.5) #gray
         ,(0.5,0,0) #brown
         ,(1,0.5,0) #orange
         ]


import matplotlib.pyplot as plt
import numpy as np

import collections
df = collections.OrderedDict()
df['labels']        = ['GWP100a\n[kgCO2eq]\n\nasedf\nasdf\nadfs','human\n[pts]','ressource\n[pts]'] 
df['all-petroleum long name'] = [3,5,2]
df['all-electric']  = [5.5, 1, 3]
df['HEV']           = [3.5, 2, 1]
df['PHEV']          = [3.5, 2, 1]

numLabels = len(df.values()[0])
numItems = len(df)-1
posX = np.arange(numLabels)+1
width = 1.0/(numItems+1)

fig = plt.figure(figsize=(2,2))
ax = fig.add_subplot(111)
for iiItem in range(1,numItems+1):
  ax.bar(posX+(iiItem-1)*width, df.values()[iiItem], width, color=colmap[iiItem-1], label=df.keys()[iiItem])
ax.set(xticks=posX+width*(0.5*numItems), xticklabels=df['labels'])

#--------------------------------------------------
# Change padding and margins, insert legend

fig.tight_layout() #tight margins
leg = ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0)
plt.draw() #to know size of legend

padLeft   = ax.get_position().x0 * fig.get_size_inches()[0]
padBottom = ax.get_position().y0 * fig.get_size_inches()[1]
padTop    = ( 1 - ax.get_position().y0 - ax.get_position().height ) * fig.get_size_inches()[1]
padRight  = ( 1 - ax.get_position().x0 - ax.get_position().width ) * fig.get_size_inches()[0]
dpi       = fig.get_dpi()
padLegend = ax.get_legend().get_frame().get_width() / dpi 

widthAx = 3 #inches
heightAx = 3 #inches
widthTot = widthAx+padLeft+padRight+padLegend
heightTot = heightAx+padTop+padBottom

# resize ipython window (optional)
posScreenX = 1366/2-10 #pixel
posScreenY = 0 #pixel
canvasPadding = 6 #pixel
canvasBottom = 40 #pixel
ipythonWindowSize = '{0}x{1}+{2}+{3}'.format(int(round(widthTot*dpi))+2*canvasPadding
                                            ,int(round(heightTot*dpi))+2*canvasPadding+canvasBottom
                                            ,posScreenX,posScreenY)
fig.canvas._tkcanvas.master.geometry(ipythonWindowSize) 
plt.draw() #to resize ipython window. Has to be done BEFORE figure resizing!

# set figure size and ax position
fig.set_size_inches(widthTot,heightTot)
ax.set_position([padLeft/widthTot, padBottom/heightTot, widthAx/widthTot, heightAx/heightTot])
plt.draw()
plt.show()
#--------------------------------------------------
#==================================================
2
  • This didn't work for me until I changed the first plt.draw() to ax.figure.canvas.draw(). I'm not sure why, but before this change the legend size was not getting updated.
    – ws_e_c421
    Commented Mar 9, 2016 at 23:57
  • If you are trying to use this on a GUI window, you need to change fig.set_size_inches(widthTot,heightTot) to fig.set_size_inches(widthTot,heightTot, forward=True).
    – ws_e_c421
    Commented Mar 9, 2016 at 23:58
2

I tried a very simple way, just make the figure a bit wider:

fig, ax = plt.subplots(1, 1, figsize=(a, b))

adjust a and b to a proper value such that the legend is included in the figure

before

after

1

Since google lead me here for a similar question it's probably worth noting that now you only need to use plt.savefig('myplot.png', bbox_inches='tight') to expand the canvas to fit a legend.

Most of the answers here are obsolete.

2
  • 2
    This is the top answer already, from 2019 Commented May 16, 2023 at 19:13
  • This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. To get notified when this question gets new answers, you can follow this question. Once you have enough reputation, you can also add a bounty to draw more attention to this question. - From Review
    – dmeister
    Commented May 19, 2023 at 21:04
1

i agree with John Thomas, if you are using a plt. based figure then all you probably need is: plt.savefig('myplot.png', bbox_inches='tight')

1

The layout="constrained" option (available since Matplotlib 3.7.0) allows a pretty straightforward implementation:

import matplotlib.pyplot as plt
import numpy as np

if __name__ == "__main__":
    x = np.arange(-2 * np.pi, 2 * np.pi, 0.1)
    fig, ax = plt.subplots(1, 1, layout="constrained")
    ax.plot(x, np.sin(x), label='Sine')
    ax.plot(x, np.cos(x), label='Cosine')
    ax.plot(x, np.arctan(x), label='Inverse tan')
    handles, labels = ax.get_legend_handles_labels()
    ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, -0.1))
    text = ax.text(-0.2, 1.05, "Arbitrary text", transform=ax.transAxes)
    ax.set_title("Trigonometry")
    ax.grid('on')
    fig.savefig('samplefigure.png')
    fig.show()

enter image description here

One of the benefits (compared with the original answer by jonathanbsyd) is that fig.show() works just as well.

Not the answer you're looking for? Browse other questions tagged or ask your own question.