54

Reading the following article, I managed to put a legend outside plot.

code:

import matplotlib.pyplot as pyplot

x = [0, 1, 2, 3, 4]
y = [xx*xx for xx in x]

fig = pyplot.figure()
ax  = fig.add_subplot(111)

box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width*0.8, box.height])

ax.plot(x, y)
leg = ax.legend(['abc'], loc = 'center left', bbox_to_anchor = (1.0, 0.5))
#pyplot.show()

fig.savefig('aaa.png', bbox_inches='tight')

pyplot.show() displays the correct plot with a legend outside it. But when I save it as a file with fig.savefig(), the legend is truncated.

Some googling shows me workarounds such as adding bbox_extra_artists=[leg.legendPatch] or bbox_extra_artists=[leg] to savefig(), but neither worked.

What is the correct way to do it? Matplotlib version is 0.99.3.

Thanks.

Community
  • 1
  • 1
niboshi
  • 1,448
  • 3
  • 12
  • 20
  • 3
    (I see this is old thread, but it is first in google) There is a better solution by including actors to savefig: http://stackoverflow.com/questions/10101700/moving-matplotlib-legend-outside-of-the-axis-makes-it-cutoff-by-the-figure-box – Alleo May 16 '15 at 14:25
  • 1
    another answer https://stackoverflow.com/a/44649558/805588 – dparkar Nov 20 '20 at 04:00
  • 5
    Using `fig.savefig('aaa.png', bbox_inches='tight', bbox_inches="tight")` like @MPa suggested at the question @dparker pointed to (https://stackoverflow.com/questions/44642082/text-or-legend-cut-from-matplotlib-figure-on-savefig/44649558#44649558) worked for me just now – a11 Jan 31 '21 at 23:28
  • 1
    Adding the `bbox_inches='tight'` in `plt.savefig(path_output, bbox_inches='tight')` work for me. I set `fig.legend(lines, labels, bbox_to_anchor=(0, 1, 1, 0), loc="lower left", mode="expand", ncol=4); plt.tight_layout();` before saving. So the legend is at the top of plots. – FisNaN Jan 17 '22 at 10:45

4 Answers4

28

The problem is that when you plot dynamically, matplotlib determines the borders automatically to fit all your objects. When you save a file, things are not being done automatically, so you need to specify the size of your figure, and then the bounding box of your axes object. Here is how to correct your code:

import matplotlib.pyplot as pyplot

x = [0, 1, 2, 3, 4]
y = [xx*xx for xx in x]

fig = pyplot.figure(figsize=(3,3))
ax  = fig.add_subplot(111)

#box = ax.get_position()
#ax.set_position([0.3, 0.4, box.width*0.3, box.height])
# you can set the position manually, with setting left,buttom, witdh, hight of the axis
# object
ax.set_position([0.1,0.1,0.5,0.8])
ax.plot(x, y)
leg = ax.legend(['abc'], loc = 'center left', bbox_to_anchor = (1.0, 0.5))

fig.savefig('aaa.png')
Løiten
  • 3,185
  • 4
  • 24
  • 36
oz123
  • 27,559
  • 27
  • 125
  • 187
  • Thank you, it worked. I hope future versions of savefig() will support border calculation similar to pyplot.show(). – niboshi Jan 23 '12 at 15:10
  • Mmm, you're right. I'll consider upgrading it to more recent one. – niboshi Jan 23 '12 at 16:41
  • 67
    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:49
  • There's a workaround (provided in the comments on top and my answer below) that works for most cases. Just to note, the matplotlib devs are aware of the issue, but apparently it's hard to fix. – tim-oh Mar 30 '22 at 10:47
19

Although this method works with legend, it seems not to be working well with figlegend when there are multiple subplots and we want a single overall legend. figlegend still get cropped when savefig. I just pasted my temporary solution below in case someone faces such a case.

import matplotlib.pyplot as plt

para = {
    ## this parameter will indicate the position of
    ## subplot within figure, but will not be shown
    ## if using bbox_inches='tight' when saving
    'figure.subplot.top': 0.5
}
#plt.rcParams.update(para)

fig = plt.figure()

ax=fig.add_subplot(221)
## only needed when what to manually control
## subplot ration
#ax.set_position([0.1,0.6,0.5, 0.4])
ax.plot([1,1,1])


ax=fig.add_subplot(222)
#ax.set_position([0.7,0.6,0.5, 0.4])
ax.plot([2,2,2])

ax=fig.add_subplot(223)
#ax.set_position([0.1,0.1,0.5, 0.4])
ax.plot([3,3,3])


ax=fig.add_subplot(224)
#ax.set_position([0.7,0.1,0.5, 0.4])
p1, = ax.plot([4,4,4])
p2, = ax.plot([2,3,2])

## figlegend does not work fine with tight bbox
## the legend always get cropped by this option
## even add bbox extra will not help
## had to use legend, and manually adjust it to
## arbitary position such as (0.3, 2.5)

## http://matplotlib.org/users/tight_layout_guide.html
## according to this link, tight layout is only
## an experimental feature, might not support figlegend

#lgd = plt.figlend(
lgd = plt.legend(
    [p1,p2],
    ['a', 'b'],
    ## by default, legend anchor to axis, but can
    ## also be anchored to arbitary position
    ## positions within [1,1] would be within the figure
    ## all numbers are ratio by default

    bbox_to_anchor=(-0.1, 2.5),

    ## loc indicates the position within the figure
    ## it is defined consistent to the same Matlab function 
    loc='center',

    ncol=2
    #mode="expand",
    #borderaxespad=0.
    )



#plt.show()

plt.savefig('temp.png', bbox_inches='tight')#, bbox_extra_artist=[lgd])
Ning
  • 461
  • 4
  • 9
  • Thanks for this. Have you submitted a bug report for this (bbox_extra_artists?)? I have the same problem as you, with a multi-axis figure and a out-of-axes figlegend. I cannot apply your workaround to my situation. – CPBL Mar 08 '14 at 15:45
  • I haven't fired any bug report. I'm not sure this is a bug or it is designed like this. – Ning Mar 09 '14 at 16:47
9

For most cases, a simple fix cases is to amend the plt.savefig() command:

plt.savefig('your_figure.png', bbox_inches='tight')

This solution has already been suggested in the comments below the question, but I overlooked those and found it in the matplotlib issues on GitHub. Keeping this answer for others who didn't read the comments on top.

tim-oh
  • 688
  • 8
  • 13
0

If all else fails, I use Inkscape's bounding-box features to deal with what I would call persistent bugs in matplotlib's output. If you're running GNU/Linux, just save whatever Matplotlib gives you as a pdf, and then send it to the following

def tightBoundingBoxInkscape(pdffile,use_xvfb=True):
    """Makes POSIX-specific OS calls. Preferably, have xvfb installed, to avoid any GUI popping up in the background. If it fails anyway, could always resort to use_xvfb=False, which will allow some GUIs to show as they carry out the task 
      pdffile: the path for a PDF file, without its extension
    """
    usexvfb='xvfb-run '*use_xvfb
    import os
    assert not pdffile.endswith('.pdf')
    os.system("""
       inkscape -f %(FN)s.pdf -l %(FN)s_tmp.svg
       inkscape -f %(FN)s_tmp.svg --verb=FitCanvasToDrawing \
                                   --verb=FileSave \
                                   --verb=FileQuit
      inkscape -f %(FN)s_tmp.svg -A %(FN)s-tightbb.pdf
"""%{'FN':pdffile}
Jānis Erdmanis
  • 389
  • 3
  • 13
CPBL
  • 3,783
  • 4
  • 34
  • 44