285

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

Community
  • 1
  • 1
jonathanbsyd
  • 8,031
  • 6
  • 24
  • 26
  • 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. – Joe Kington Apr 11 '12 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') – jonathanbsyd Apr 13 '12 at 06: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 Jan 02 '13 at 21:50

5 Answers5

386

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.

jonathanbsyd
  • 8,031
  • 6
  • 24
  • 26
  • 1
    /!\ Seems to work only since matplotlib >= 1.0 (Debian squeeze have 0.99 and this does not work) – Julien Palard Dec 04 '12 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. – Caleb Stanford Mar 15 '15 at 06:39
  • 16
    Ah! I just needed to use bbox_inches = "tight" as you did. Thanks! – Caleb Stanford Mar 15 '15 at 06:44
  • 10
    This is nice, but I still get my figure cut when I try to `plt.show()` it. Any fix for that? – Agostino Mar 26 '15 at 17:51
  • I know this is old, but is there any way to make this work for version 0.99? – TomCho May 07 '15 at 10:37
  • What does `ax.grid('on')` do? – tommy.carstensen Jul 05 '15 at 21:43
  • Does the print_xxx variants have this option? i.e. is there a way to get this image without having to write to a file? – Har Jun 16 '17 at 15:27
  • @Agostino try `plt.tight_layout(pad=7)` – bonanza Nov 30 '17 at 15:07
  • 5
    [It does not work if use `fig.legend()` method](https://stackoverflow.com/questions/48128546/legend-is-not-present-in-the-generated-image-if-i-use-tight-for-bbox-inches) , really weird. – jdhao Jan 07 '18 at 16:29
  • The margins exclude a title. E.g. if I use fig.suptitle('title') and then tight bbox margins, I will not see a title. – thedoctar Jan 02 '19 at 16:20
  • @thedoctar In future, do not edit my answer to include an error. Your code is wrong. You should use ax.set_title("title") and it works fine. – jonathanbsyd Jan 03 '19 at 23:22
  • The margins exclude text. E.g. if I use ax.text(-2,2, "test", transform=ax.transAxes) – thedoctar Jan 07 '19 at 16:28
  • This works great for any other object too, such as lgd = plt.xlabel('x'). Thanks! – The Doctor Apr 26 '19 at 15:04
  • 1
    `plt.savefig('x.png', bbox_inches='tight')` was sufficient. Thanks for sharing. – mateuszb Jun 27 '19 at 13:04
  • I'm getting the error `TypeError: 'Legend' object is not iterable`. Any ideas? Thanks – Danny Nov 09 '20 at 22:31
  • I got it. I was only passing `lgd` as a single argument because I didn't want to use arbitrary text, and assumed I didn't need to have in a list or a tuple. I simply enclosed `lgd` in square brackets and everything is working. – Danny Nov 10 '20 at 17:31
  • This worked for me fig = plt.figure(dpi=100) #plotting code skipped lgd = fig.legend(loc='lower center', bbox_to_anchor=(0.5, -0.2),fancybox=True, shadow=False, frameon=False, ncol=2) fig.savefig('roc_curve.svg', bbox_extra_artists=(lgd,), bbox_inches='tight') – alluppercase Mar 23 '21 at 16:33
  • Unfortunatly this great solution is now deprecated as per version 3.3: `MatplotlibDeprecationWarning: savefig() got unexpected keyword argument "extra_artists" which is no longer supported as of 3.3 and will become an error two minor releases later`. Solution based on `subplots_adjust` method is still valid. – jlandercy May 12 '22 at 09:55
  • Has there been any answer for how to make the figure.show() method work without cropping? Even when saving the figure correctly produces the chart with the full legend, figure.show() will still crop the legend - tight_layout will squish the main body of the chart, so is also undesirable. – Faraz Masroor Feb 01 '23 at 22:28
  • @FarazMasroor try emailing the matplotlib mailing list. Then write a new question and self-answer it here on SO. Come back and link to your question in a new comment right here – jonathanbsyd Feb 05 '23 at 20:27
34

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.

ely
  • 74,674
  • 34
  • 147
  • 228
  • 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. – jonathanbsyd Apr 14 '12 at 15:11
  • also works for me to get a big enough picture to fit the xlabels previously being cut off – Frederick Nord Feb 20 '14 at 17:57
  • 1
    [here](http://matplotlib.org/examples/pylab_examples/subplots_adjust.html) is the documentation with example code from matplotlib.org – Yojimbo Feb 18 '15 at 17:26
17

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()
#--------------------------------------------------
#==================================================
gebbissimo
  • 2,137
  • 2
  • 25
  • 35
  • 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 Mar 09 '16 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 Mar 09 '16 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

Ann
  • 103
  • 2
  • 4
-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.

  • This is the top answer already, from 2019 – Vincent Rupp May 16 '23 at 19:13
  • This does not really answer the question. If you have a different question, you can ask it by clicking [Ask Question](https://stackoverflow.com/questions/ask). To get notified when this question gets new answers, you can [follow this question](https://meta.stackexchange.com/q/345661). Once you have enough [reputation](https://stackoverflow.com/help/whats-reputation), you can also [add a bounty](https://stackoverflow.com/help/privileges/set-bounties) to draw more attention to this question. - [From Review](/review/late-answers/34397136) – dmeister May 19 '23 at 21:04