3

I am trying to plot several data which, in some cases, occupies the entire plot.

The default option, from version 2, should be 'best', which tries to find the best position to place the legend inside the plot. Is there a way to extend the option to be able to place the legend outside the plot if the space is insufficient?

Otherwise, is there an option for matplotlib (without taking the max of all the series and add a manual padding) to automatically add an ylim padding and give space to the legend and be placed inside the plot?

The main idea is to avoid manual tweaking of the plots, having several plots to be created automatically.

A simple MWE is in the following:

%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import scipy as sc
import matplotlib.pyplot as plt
plt.close('all')

x = sc.linspace(0, 1, 50)
y = sc.array([sc.ones(50)*0.5, x, x**2, (1-x), (1-x**2)]).T
fig = plt.figure('Fig')
ax = fig.add_subplot(111)
lines = ax.plot(x, y)
leg = ax.legend([lines[0], lines[1], lines[2], lines[3], lines[4]],
                [r'$\mathrm{line} = 0.5$', r'$\mathrm{line} = x$', r'$\mathrm{line} = x^2$',
                 r'$\mathrm{line} = 1-x$',r'$\mathrm{line} = 1-x^2$'], ncol=2)
fig.tight_layout()

Plot

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Alex Pacini
  • 314
  • 4
  • 14

1 Answers1

2

There is no automatic way to place the legend at "the best" position outside the axes.

inside the plot

You may decide to always leave enough space inside the axes, such that the legend doesn't overlap with anything. To this end you can use ax.margins. e.g.

ax.margins(y=0.25)

will produce 25% margin on both ends of the y axis, enough space to host the legend if it has 3 columns.

enter image description here

You may then decide to always use the same location, e.g. loc="upper center" for a consistent result among all plots. The drawback of this is that it depends on figure size and that it adds a (potentially undesired) margin at the other end of the axis as well. If you can live with that margin, a way to automatically determine the needed margin would be the following:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms

x = np.linspace(0, 1, 50)
y = np.array([np.ones(50)*0.5, x, x**2, (1-x), (1-x**2)]).T
fig = plt.figure('Fig')
ax = fig.add_subplot(111)
lines = ax.plot(x, y)

def legend_adjust(legend, ax=None ):
    if ax == None: ax  =plt.gca()
    ax.figure.canvas.draw()
    bbox = legend.get_window_extent().transformed(ax.transAxes.inverted() )
    print bbox.height
    ax.margins(y = 2.*bbox.height)
    
leg = plt.legend(handles=[lines[0], lines[1], lines[2], lines[3], lines[4]],
       labels= [r'$\mathrm{line} = 0.5$', r'$\mathrm{line} = x$', r'$\mathrm{line} = x^2$',
                 r'$\mathrm{line} = 1-x$',r'$\mathrm{line} = 1-x^2$'], loc="upper center", 
                  ncol=2)
legend_adjust(leg)
plt.show()

If setting the limits is fine with you, you may also adapt the limits themselves:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms

x = np.linspace(0, 1, 50)
y = np.array([np.ones(50)*0.5, x, x**2, (1-x), (1-x**2)]).T
fig = plt.figure('Fig')
ax = fig.add_subplot(111)
lines = ax.plot(x, y)

def legend_adjust(legend, ax=None, pad=0.05 ):
    if ax == None: ax  =plt.gca()
    ax.figure.canvas.draw()
    bbox = legend.get_window_extent().transformed(ax.transAxes.inverted() )
    ymin, ymax = ax.get_ylim()
    ax.set_ylim(ymin, ymax+(ymax-ymin)*(1.+pad-bbox.y0))
    

    
leg = plt.legend(handles=[lines[0], lines[1], lines[2], lines[3], lines[4]],
       labels= [r'$\mathrm{line} = 0.5$', r'$\mathrm{line} = x$', r'$\mathrm{line} = x^2$',
                 r'$\mathrm{line} = 1-x$',r'$\mathrm{line} = 1-x^2$'], loc="upper center", 
                  ncol=2)
legend_adjust(leg)
plt.show()

enter image description here

out of the plot

Otherwise you may decide to always put the legend out of the plot. Some techniques are collected in this answer.

Of special interest may be to place the legend outside the figure without changing the figuresize, as detailed in this question: Creating figure with exact size and no padding (and legend outside the axes)

Adapting it to this case would look like:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms

x = np.linspace(0, 1, 50)
y = np.array([np.ones(50)*0.5, x, x**2, (1-x), (1-x**2)]).T
fig = plt.figure('Fig')
ax = fig.add_subplot(111)
lines = ax.plot(x, y)

def legend(ax=None, x0=1,y0=1, direction = "v", padpoints = 3,**kwargs):
    if ax == None: ax  =plt.gca()
    otrans = ax.figure.transFigure
    t = ax.legend(bbox_to_anchor=(x0,y0), loc=1, bbox_transform=otrans,**kwargs)
    plt.tight_layout()
    ax.figure.canvas.draw()
    plt.tight_layout()
    ppar = [0,-padpoints/72.] if direction == "v" else [-padpoints/72.,0] 
    trans2=matplotlib.transforms.ScaledTranslation(ppar[0],ppar[1],fig.dpi_scale_trans)+\
             ax.figure.transFigure.inverted() 
    tbox = t.get_window_extent().transformed(trans2 )
    bbox = ax.get_position()
    if direction=="v":
        ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox.y0-bbox.y0]) 
    else:
        ax.set_position([bbox.x0, bbox.y0,tbox.x0-bbox.x0, bbox.height]) 

legend(handles=[lines[0], lines[1], lines[2], lines[3], lines[4]],
       labels= [r'$\mathrm{line} = 0.5$', r'$\mathrm{line} = x$', r'$\mathrm{line} = x^2$',
                 r'$\mathrm{line} = 1-x$',r'$\mathrm{line} = 1-x^2$'], 
                 y0=0.8, direction="h", borderaxespad=0.2)

plt.show()

enter image description here

Community
  • 1
  • 1
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Is there a way to automatically compute a size for ax.margins(y=0.25) given the plot size and the legend size and to add it only at the top? – Alex Pacini Apr 28 '17 at 14:46
  • As I said the margin is always added on both sides. I updated the answer for an automatic determination of the margin. – ImportanceOfBeingErnest Apr 28 '17 at 15:04
  • What if I change the last part of your inside the plot with: `axlim_in = ax.get_ylim(); legend_adjust(leg); ax.set_ylim(axlim_in[0], ax.get_ylim()[1])`? – Alex Pacini Apr 28 '17 at 15:17
  • I was under the impression that you did not want to set the limits (this is how I understood your question). I updated the answer with a method for that one as well. – ImportanceOfBeingErnest Apr 28 '17 at 16:09