Matplotlib provides better off-plot positioning too

I am trying to plot some data which in some cases takes up the whole plot.

The default setting since version 2 should be "best", which tries to find the best place to put the legend within the plot. Is there a way to expand the option so that the legend can be placed outside the graph if there is not enough space?

Otherwise, is there an option for matplotlib (not taking the maximum length of the entire series and adding a manual padding) to automatically add padding to the em and give space to the legend and fit inside the plot?

The main idea is to avoid manually setting up plots by creating multiple graphs that will be generated automatically.

A simple MWE is 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

+3


source to share


1 answer


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

inside the schedule

You can always leave enough space inside the axes so that the legend does not intersect with anything. For this purpose, you can use ax.margins

. eg

ax.margins(y=0.25)

      

will give out 25% margin at both ends of the y-axis, enough room to place the legend if it has 3 columns.

enter image description here

Then you can decide to always use the same location, for example. loc="upper center"

for a consistent result across all graphs. The downside to this is that it depends on the size of the shape and that it adds a (potentially unwanted) edge at the other end of the axis. If you can live with this field, the way to automatically determine the required stock would be as follows:

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 you set limits with you, you can also adapt the boundaries yourself:



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

from schedule

Otherwise, you might decide that you always get the legend out of the plot . Some methods are collected in this answer .

Of particular interest might be to place the legend outside the shape without changing the dimension, as detailed in this question: Creating a shape with an exact size and no padding (and the legend outside the axes)

An adaptation for this case would look like this:

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

+2


source







All Articles