2

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.

Rickson
  • 1,040
  • 2
  • 16
  • 40

2 Answers2

2

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()
killian95
  • 803
  • 6
  • 11
  • I played around with those tight () options as well but they did not always behave as expected (e.g . changed the dimensions of the black frame). – Rickson Oct 24 '18 at 19:05
  • This answer helped me when I had a similar issue before: https://stackoverflow.com/a/4701285/6064179 – killian95 Oct 24 '18 at 19:32
  • Unfortunately, this answer relies on pyplot. – Rickson Oct 24 '18 at 19:41
  • Does the `subplot.legend()` not take a `bbox_to_anchor` argument? – killian95 Oct 24 '18 at 19:44
  • This solution is working well. It does not change the dimensions of the content, but enlarges the canvas instead. This is exactly what the question asks about. – ImportanceOfBeingErnest Oct 24 '18 at 19:52
  • The main issue with tight() is that it also crops existing white spice to make the picture more tight (what I do not want). Sorry, for making this not more clear in the question. I need a possibility to only add space to the picture to avoid the case that elements like labels or outside legends are cropped. The rest of the picture (e.g. empty white space on the left or right side of the plot) shall remain untouched. – Rickson Oct 24 '18 at 20:09
  • Not sure what you mean by `tight()`. But this answer here uses `bbox_inches='tight'` which does exactly that: It adds space to the picture such that it is as large as it needs to be. Did you actually run the code here? If so and if it does not produce the desired output you may use it in your question to explain in how far this is not desired. – ImportanceOfBeingErnest Oct 24 '18 at 20:12
  • Yes, I will update the question. Exactly, it will increase the canvas on the y-axis (what is good) but it will crop the whitespace on the x-axis (what I do not want). The are many places were you can pass a 'tight' command. I think I tried all of the with the same result. I just wrote tight() to summarize all those possible options. – Rickson Oct 24 '18 at 20:16
  • @ImportanceOfBeingErnest The question has been updated. – Rickson Oct 24 '18 at 20:34
1

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")
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • Thanks a lot. It already looks very promising. But if I run your code, the legend isn't visible anymore. – Rickson Oct 25 '18 at 00:09
  • That's unfortunate. The image shown is directly created via the code. Which matplotlib version are you using? How do you run the code? – ImportanceOfBeingErnest Oct 25 '18 at 01:29
  • I am using version 2.0.2 and was running the code with PyScripter. I will switch systems in the course of the day and test a different environment. – Rickson Oct 25 '18 at 05:47
  • Strange. If I am using matplotlib 1.5.3 the plot generated by your code looks exactly the same as the one in my question (just with a yellow background). So the code seems to behave different for different matplotlib versions. – Rickson Oct 25 '18 at 07:47
  • So I tested the code with matplotlib 2.2.3 and 3.0.0 where it works fine.Do you have the chance to update your matplotlib or do you want me to find a solution for those old versions? – ImportanceOfBeingErnest Oct 25 '18 at 11:10
  • I would love to upgrade matplotlib but I cannot. The only version that is relevant for me is 1.5.3. It would be great if you could help me since this issue is driving my mad since days... – Rickson Oct 25 '18 at 11:56
  • [Here](https://github.com/matplotlib/matplotlib/blob/26382a72ea234ee0efd40543c8ae4a30cffc4f0d/lib/matplotlib/backend_bases.py#L2182) is the code from matplotlbi 1.5.3. You would need to do the same and at the end take the minimum/maximum of the tightbox and the initial figure again. I might not have time to provide a full working code in the next few days. – ImportanceOfBeingErnest Oct 25 '18 at 12:40
  • I think I found the solution for my specific problem. I extended your answer accordingly. You may review and optimize it since you are much more deep into matplotlib than I am. – Rickson Oct 25 '18 at 16:37