Rickson
Rickson

Reputation: 1090

Why is the legend of my matplotlib plot always cropped?

I am searching for a programmatic way to add a legend to the outside of my matplotlib plot such that it will not be cropped when saving the figure to an svg file. In the following example, you will see that the x-axis label as well as the legend are cropped in the resulting svg picture:

import matplotlib
import matplotlib.backends.backend_svg

fig = matplotlib.figure.Figure(figsize=(8,2))
subplot = fig.add_axes([0.1, 0.2, 0.8, 0.75], xlabel='x_label')

Sig_1, = subplot.plot([1,2,3], [1,2,3])
Sig_2, = subplot.plot([1,2,3], [4,5,6])

legend = subplot.legend([Sig_1, Sig_2], ['y_label_1', 'y_label_2'],loc='upper right',borderpad=0.06,handletextpad=0.1,handlelength=1.5,bbox_to_anchor=(1.0, 1.235),frameon=False,columnspacing=1.0,ncol=2)

fig.set_canvas(matplotlib.backends.backend_svg.FigureCanvasSVG(fig))
fig.savefig('C:\plot.svg')
fig.clear()

enter image description here

Ideally, I would like to create the plot and then extend the canvas by some method without cropping existing whitespace in order to make the plot more compact. Only those regions of the canvas shall be extended which would otherwise lead to cropped elements like outside legends or axis labels. The only restriction that I have is that I cannot make use of pyplot.

Of course, I could tweak the legend properties until I find a configuration which may work for exactly that plot. But I hope that there is a generic way to solve that for any kind of plot.

Any answer is highly appreciated.

Upvotes: 2

Views: 1245

Answers (2)

ImportanceOfBeingErnest
ImportanceOfBeingErnest

Reputation: 339220

If I understand correctly you want to limit the "tight" option to only expand the figure, not crop it. Since there is no such predefined option, you would need to calculate the tight box first and use only those values from it that are smaller/larger than the figure extents.

import matplotlib.figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.transforms import Bbox

fig = matplotlib.figure.Figure(figsize=(8,2))
subplot = fig.add_axes([0.1, 0.2, 0.8, 0.75], xlabel='x_label')

Sig_1, = subplot.plot([1,2,3], [1,2,3])
Sig_2, = subplot.plot([1,2,3], [4,5,6])

legend = subplot.legend([Sig_1, Sig_2], ['y_label_1', 'y_label_2'],
                        loc='upper right',borderpad=0.06,handletextpad=0.1,
                        handlelength=1.5,bbox_to_anchor=(1.0, 1.235),
                        frameon=False,columnspacing=1.0,ncol=2)

canvas = FigureCanvasAgg(fig)

fig.canvas.draw()
renderer = fig._cachedRenderer
tightbox = fig.get_tightbbox(renderer)
w,h = fig.get_size_inches()
bbox = Bbox.from_extents(min(tightbox.x0,0), min(tightbox.y0,0),
                         max(tightbox.x1,w), max(tightbox.y1,h))

fig.savefig('cropplot.png', bbox_inches=bbox, facecolor="#fff9e3")

enter image description here

Here I made the figure background colorful to see the boundaries well. Also note that I replaced the svg canvas by the usual agg canvas, because otherwise there is no renderer available.

The following code should work for older versions of matplotlib. It will leave the figure width untouched and only expand the figure in vertical direction.

import matplotlib.figure
from matplotlib.backends.backend_agg import FigureCanvasAgg

def CreateTightBbox(fig):
    from matplotlib.transforms import Affine2D, Bbox, TransformedBbox
    from matplotlib import rcParams

    w,h = fig.get_size_inches()
    renderer = fig._cachedRenderer
    bbox_artists = fig.get_default_bbox_extra_artists()
    bbox_filtered = []

    for a in bbox_artists:
        bbox = a.get_window_extent(renderer)
        if a.get_clip_on():
            clip_box = a.get_clip_box()
            if clip_box is not None:
                bbox = Bbox.intersection(bbox, clip_box)
            clip_path = a.get_clip_path()
            if clip_path is not None and bbox is not None:
                clip_path = clip_path.get_fully_transformed_path()
                bbox = Bbox.intersection(bbox, clip_path.get_extents())
        if bbox is not None and (bbox.width != 0 or bbox.height != 0):
            bbox_filtered.append(bbox)

    if bbox_filtered:
        _bbox = Bbox.union(bbox_filtered)
        trans = Affine2D().scale(1.0 / fig.dpi)
        bbox_extra = TransformedBbox(_bbox, trans)
        bbox_inches = Bbox.union([fig.bbox_inches, bbox_extra])

    pad = rcParams['savefig.pad_inches']
    bbox_inches = bbox_inches.padded(pad)
    bbox = Bbox.from_extents(0, min(bbox_inches.y0,0), w, max(bbox_inches.y1,h))

    return bbox


#create the figure
fig = matplotlib.figure.Figure(figsize=(8,2))
subplot = fig.add_axes([0.1, 0.2, 0.8, 0.75], xlabel='x_label')

Sig_1, = subplot.plot([1,2,3], [1,2,3])
Sig_2, = subplot.plot([1,2,3], [4,5,6])

legend = subplot.legend([Sig_1, Sig_2], ['y_label_1', 'y_label_2'],
                        loc='upper right',borderpad=0.06,handletextpad=0.1,
                        handlelength=1.5,bbox_to_anchor=(1.0, 1.235),
                        frameon=False,columnspacing=1.0,ncol=2)

#set the canvas
canvas = FigureCanvasAgg(fig)
fig.canvas.draw()
w,h = fig.get_size_inches()

#create tight bbox
bbox = CreateTightBbox(fig)

#print bbox
fig.savefig('cropplot.png', bbox_inches=bbox, facecolor="#fff9e3")

Upvotes: 1

killian95
killian95

Reputation: 813

Does passing bbox_inches='tight' to savefig() not work?

import matplotlib
import matplotlib.backends.backend_svg

fig = matplotlib.figure.Figure(figsize=(8,2))
subplot = fig.add_axes([0.1, 0.2, 0.8, 0.75], xlabel='x_label')

Sig_1, = subplot.plot([1,2,3], [1,2,3])
Sig_2, = subplot.plot([1,2,3], [4,5,6])

legend = subplot.legend([Sig_1, Sig_2], ['y_label_1', 'y_label_2'],loc='upper right',borderpad=0.06,handletextpad=0.1,handlelength=1.5,bbox_to_anchor=(1.0, 1.235),frameon=False,columnspacing=1.0,ncol=2)

fig.set_canvas(matplotlib.backends.backend_svg.FigureCanvasSVG(fig))
fig.savefig('C:\plot.jpg', bbox_inches='tight')
fig.clear()

Upvotes: 2

Related Questions