matplotlib: remove all figure margin around ax, then show - python

Other answers on this site discuss how to remove "whitespace" margins around matplotlib plots when saving a figure to disk with fig.savefig(). None seem to show how to do this for displaying the figure with plt.show() rather than saving it.
How do I remove the gray around the following figure (without saving it to disk)?
import matplotlib.pyplot as plt
fig, ax = plt.subplots(facecolor='gray')
ax.set_facecolor('pink')
ax.scatter([5, 1, 3], [1, 2, 3], s=100, c='b')
ax.axis('on')
ax.margins(0)
ax.set_aspect('equal')
ax.tick_params(which="both", direction="in")
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
fig.subplots_adjust(0, 0, 1, 1)
fig.tight_layout()
plt.show()
Above, the gray margin of the figure remains around the pink ax, no matter what I do. Below, I can remove it when saving to disk:
extent = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig('test.jpg', facecolor='gray', bbox_inches=extent)
This is the image I want to "show." But I can find nothing to re-create that bbox_inches=extent functionality outside of the savefig function. So how do I remove the gray around the plot and then show it with plt.show?

It is a simple solution using the argument frameon=False while creating the axis instance as shown in this answer
fig, ax = plt.subplots(facecolor='gray', frameon=False)
which produces the following with plt.show().
EDIT: I also now remove the axes around the figure using the following
for spine in ['top', 'right', 'left', 'bottom']:
ax.spines[spine].set_visible(False)
or as a one-liner:
_ = [s.set_visible(False) for s in ax.spines.values()]

Related

matplotlib ax to figure extent - remove whitespace, borders, everything for plot of geopandas map

I am looking for a solution to have a seamless map image plot with matplotlib. The current code works good and stable, however, it leaves a whitespace to the left and bottom. I would like to remove this whitespace and don't know how.
My sample code for this:
import geopandas
from seaborn import despine
from pandas import read_csv
import matplotlib.pyplot as plt
# read data and shapefile
geo_path = 'shapefiles/ne_10m_admin_0_countries.shp'
df = read_csv('UNpopEstimates2100.csv')
world = geopandas.read_file(geo_path)
# specifiy what is to be plotted
cm = 'Greys'
world['2015'] = df['2015']
# set plot environment
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)
world.plot(ax=ax, column='2015', cmap=cm, scheme='quantiles')
plt.savefig('sample.png', bbox_inches='tight', tight_layout=True, pad_inches=0, frameon=None)
sample.png
smaple.png with marked whitespace I would like to remove
I followed the Tutorial at Matplotlib's Tight Layout guide, machinelearningplus.com, Removing white padding from figure on Reddit as well as several other stackoverflow posts, namely
Matplotlib scatter plot - Remove white padding,
Matplotlib: Getting subplots to fill figure,
Matplotlib plots: removing axis, legends and white spaces,
Removing white space around a saved image in matplotlib
and
matplotlib plot to fill figure only with data points, no borders, labels, axes,
What am I missing?
edit
- to provide a reproducable version with non-real-life data, but question stays the same - how do I get rid of the whitespace around my plot?
I am new to Geopandas, so I am not sure how to recreate a geodataframe, however, there is built in datasets with it.
world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
world['2015'] = np.random.uniform(low=1., high=100., size=(177,))
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
plt.subplots_adjust(left=0, right=1, bottom=0, top=1)
world.plot(ax=ax, column='2015', scheme='quantiles')
plt.savefig('sample.png')
First there is a difference when using different geopandas versions. One should probably make sure to use geopandas 0.4 at least to have the map in the correct aspect ratio.
Next one needs to remove the padding inside the axes. This can be done using the ax.margins(0) command.
Now this would lead to some whitespace in one direction (top and bottom in this case). One option is to shrink the figure to the extent of the axes.
import numpy as np
import matplotlib; print(matplotlib.__version__)
import matplotlib.pyplot as plt
import geopandas; print(geopandas.__version__)
world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
world['2015'] = np.random.uniform(low=1., high=100., size=(177,))
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
world.plot(ax=ax, column='2015', scheme='quantiles')
ax.margins(0)
ax.apply_aspect()
bbox = ax.get_window_extent().inverse_transformed(fig.transFigure)
w,h = fig.get_size_inches()
fig.set_size_inches(w*bbox.width, h*bbox.height)
plt.savefig('sample.png')
plt.show()
The advantage of this is that the physical size of the figure really fits the axes; so the result is the same whether shown on screen or saved as image.
If instead the aim is to just save the figure without whitespace you can use the bbox_inches argument to savefig and supply the actual extent of the axes in inches.
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
world.plot(ax=ax, column='2015', scheme='quantiles')
ax.margins(0)
ax.apply_aspect()
bbox = ax.get_window_extent().inverse_transformed(fig.dpi_scale_trans)
plt.savefig('sample.png', bbox_inches=bbox)
Finally, the above can be automated, using bbox_inches='tight'. However, for the 'tight' option to work correctly, one will need to make sure there are no ticks and labels around the axes, which would otherwise increase the spacing.
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis('off')
world.plot(ax=ax, column='2015', scheme='quantiles')
ax.margins(0)
ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)
plt.savefig('sample.png', bbox_inches="tight", pad_inches=0)
In all three cases above, the resulting figure would be

Change size subplots matplotlib

I want to plot a list of images. However, the plots are too small, so I cannot see them well. I tried to increase the size but the output is not really what i wanted.
I know there are lot of examples out there, but they are mostly contain mostly overkill solutions.
plt.figure(figsize=(30, 100))
for img in images:
plt.subplots(n_img, 1, figsize=(8,10))
plt.imshow(im, 'gray')
plt.axis('off')
plt.tight_layout()
plt.show()
Thanks
Hard to say if this will help, but to avoid potential confusion with setting figsize twice, I wouldn't call subplots inside your loop. Instead I'd set up the figure and axes first, then plot to each axis in turn:
fig, axs = plt.subplots(n_img, 1, figsize=(8,10))
for img, ax in zip(images, axs):
ax.imshow(img, 'gray')
ax.axis('off')
plt.tight_layout()
plt.show()

matplotlib add rectangle to Figure not to Axes

I need to add a semi transparent skin over my matplotlib figure. I was thinking about adding a rectangle to the figure with alpha <1 and a zorder high enough so its drawn on top of everything.
I was thinking about something like that
figure.add_patch(Rectangle((0,0),1,1, alpha=0.5, zorder=1000))
But I guess rectangles are handled by Axes only. is there any turn around ?
Late answer for others who google this.
There actually is a simple way, without phantom axes, close to your original wish. The Figure object has a patches attribute, to which you can add the rectangle:
fig, ax = plt.subplots(nrows=1, ncols=1)
ax.plot(np.cumsum(np.random.randn(100)))
fig.patches.extend([plt.Rectangle((0.25,0.5),0.25,0.25,
fill=True, color='g', alpha=0.5, zorder=1000,
transform=fig.transFigure, figure=fig)])
Gives the following picture (I'm using a non-default theme):
The transform argument makes it use figure-level coordinates, which I think is what you want.
You can use a phantom axes on top of your figure and change the patch to look as you like, try this example:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
ax.set_zorder(1000)
ax.patch.set_alpha(0.5)
ax.patch.set_color('r')
ax2 = fig.add_subplot(111)
ax2.plot(range(10), range(10))
plt.show()
If you aren't using subplots, using gca() will work easily.
from matplotlib.patches import Rectangle
fig = plt.figure(figsize=(12,8))
plt.plot([0,100],[0,100])
plt.gca().add_patch(Rectangle((25,50),15,15,fill=True, color='g', alpha=0.5, zorder=100, figure=fig))

matplotlib.pyplot.imshow: removing white space within plots when using attributes "sharex" and "sharey"

I have a problem which is similar to the one posted here. The difference is that I get unwanted white spaces inside the plot area when I plot two subplots which share axes via the sharex and sharey attributes. The white spaces persist even after setting autoscale(False). For example, using similar code as in the answer to the post mentioned above:
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(2, 1, 1)
ax.imshow(np.random.random((10,10)))
ax.autoscale(False)
ax2 = fig.add_subplot(2, 1, 2, sharex=ax, sharey=ax) # adding sharex and sharey
ax2.imshow(np.random.random((10,10)))
ax2.autoscale(False)
plt.show()
results in this image.
I have also tried ax.set_xlim(0, 10) and ax.set_xbound(0, 10) as per suggestions here, but to no avail. How can I get rid of the extra white spaces? Any ideas would be appreciated.
As suggested here, adding:
ax.set_adjustable('box-forced')
ax2.set_adjustable('box-forced')
solves the problem.
(documentation)
Using plt.subplots as:
fig, ax = plt.subplots(nrows=2, ncols=1, sharex=True, sharey=False)
ax[0].imshow(np.random.random((10,10)))
ax[0].autoscale(False)
ax[1].imshow(np.random.random((10,10)))
ax[1].autoscale(False)
I get with no white spaces within axes.
Using figsize within plt.subplots or fig.subplots_adjust you can get better axis ratios.
The issue is the helpful machinery from using add_subplot. Notice that the amount of white space changes if you resize the figure.
The following seems to work (until you re-size the figure)
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(5, 5))
ax = fig.add_axes([.3, .55, .35, .35])
ax.imshow(np.random.random((10,10)))
ax.autoscale(False)
ax2 = fig.add_axes([.3, .05, .35, .35], sharex=ax, sharey=ax )
ax2.imshow(np.random.random((10,10)))
ax2.autoscale(False)
plt.show()
This looks like a bad interaction between the size/location of the axes object, the shared axes, and the equal aspect ratio from imshow.
If you can live with out the ticks, you can do
ax.set_axis_off()
ax2.set_axis_off()
I think it is worth opening an issue on the matplotlib github for this.

Get legend as a separate picture in Matplotlib

I'm developing a Web application and want to display a figure and its legend in different locations on the page. Which means I need to save the legend as a separate png file. Is this possible in Matplotlib in a more or less straightforward way?
This could work:
import pylab
fig = pylab.figure()
figlegend = pylab.figure(figsize=(3,2))
ax = fig.add_subplot(111)
lines = ax.plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
figlegend.legend(lines, ('one', 'two'), 'center')
fig.show()
figlegend.show()
figlegend.savefig('legend.png')
You may limit the saved region of a figure to the bounding box of the legend using the bbox_inches argument to fig.savefig. Below to versions of a function which you can simply call with the legend you want to save as argument. You may either use the legend created in the original figure here (and remove it afterwards, legend.remove()) or you may create a new figure for the legend and simply use the function as it is.
Export legend boundingbox
In case the complete legend shall be saved, the bounding box supplied to the bbox_inches argument would be simply the transformed bounding box of the legend. This works well if the legend has no border around it.
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=False)
def export_legend(legend, filename="legend.png"):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
Export extended legend bounding box
If there is a border around the legend, the above solution may be suboptimal. In this case it makes sense to extend the bounding box by some pixels to include the border to its full.
import numpy as np
import matplotlib.pyplot as plt
colors = ["crimson", "purple", "gold"]
f = lambda m,c: plt.plot([],[],marker=m, color=c, ls="none")[0]
handles = [f("s", colors[i]) for i in range(3)]
labels = colors
legend = plt.legend(handles, labels, loc=3, framealpha=1, frameon=True)
def export_legend(legend, filename="legend.png", expand=[-5,-5,5,5]):
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent()
bbox = bbox.from_extents(*(bbox.extents + np.array(expand)))
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
export_legend(legend)
plt.show()
use pylab.figlegend(..) and get_legend_handles_labels(..):
import pylab, numpy
x = numpy.arange(10)
# create a figure for the data
figData = pylab.figure()
ax = pylab.gca()
for i in xrange(3):
pylab.plot(x, x * (i+1), label='line %d' % i)
# create a second figure for the legend
figLegend = pylab.figure(figsize = (1.5,1.3))
# produce a legend for the objects in the other figure
pylab.figlegend(*ax.get_legend_handles_labels(), loc = 'upper left')
# save the two figures to files
figData.savefig("plot.png")
figLegend.savefig("legend.png")
It can be tricky though to get the size of the legend figure right in an automated manner.
It is possible to use axes.get_legend_handles_labels to get the legend handles and labels from one axes object and to use them to add them to an axes in a different figure.
# create a figure with one subplot
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1,2,3,4,5], [1,2,3,4,5], 'r', label='test')
# save it *without* adding a legend
fig.savefig('image.png')
# then create a new image
# adjust the figure size as necessary
figsize = (3, 3)
fig_leg = plt.figure(figsize=figsize)
ax_leg = fig_leg.add_subplot(111)
# add the legend from the previous axes
ax_leg.legend(*ax.get_legend_handles_labels(), loc='center')
# hide the axes frame and the x/y labels
ax_leg.axis('off')
fig_leg.savefig('legend.png')
If for some reason you want to hide only the axes label, you can use:
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
or if, for some weirder reason, you want to hide the axes frame but not the axes labels you can use:
ax.set_frame_on(False)
ps: this answer has been adapted from my answer to a duplicate question
I've found that the easiest way is just to create your legend and then just turn off the axis with plt.gca().set_axis_off():
# Create a color palette
palette = dict(zip(['one', 'two'], ['b', 'g']))
# Create legend handles manually
handles = [mpl.patches.Patch(color=palette[x], label=x) for x in palette.keys()]
# Create legend
plt.legend(handles=handles)
# Get current axes object and turn off axis
plt.gca().set_axis_off()
plt.show()
This calculates the size of the legend automatically. If mode == 1, the code is similar to Steve Tjoa's answer, while mode == 2 is similar Andre Holzner's answer.
The loc parameter must be set to 'center' to make it work (but I do not know why this is necessary).
mode = 1
#mode = 2
import pylab
fig = pylab.figure()
if mode == 1:
lines = fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10))
legend_fig = pylab.figure(figsize=(3,2))
legend = legend_fig.legend(lines, ('one', 'two'), 'center')
if mode == 2:
fig.gca().plot(range(10), pylab.randn(10), range(10), pylab.randn(10), label='asd')
legend_fig = pylab.figure()
legend = pylab.figlegend(*fig.gca().get_legend_handles_labels(), loc = 'center')
legend.get_frame().set_color('0.70')
legend_fig.canvas.draw()
legend_fig.savefig('legend_cropped.png',
bbox_inches=legend.get_window_extent().transformed(legend_fig.dpi_scale_trans.inverted()))
legend_fig.savefig('legend_original.png')
Original (uncropped) legend:
Cropped legend:
Inspired by Maxim and ImportanceOfBeingErnest's answers,
def export_legend(ax, filename="legend.pdf"):
fig2 = plt.figure()
ax2 = fig2.add_subplot()
ax2.axis('off')
legend = ax2.legend(*ax.get_legend_handles_labels(), frameon=False, loc='lower center', ncol=10,)
fig = legend.figure
fig.canvas.draw()
bbox = legend.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi="figure", bbox_inches=bbox)
which allows me to save legend horizontally in a separate file. As an example
In November 2020, I tried almost everything on this post, but none worked for me. After struggling for a while, I found a solution that does what I want.
Pretend you want to draw a figure and a legend separately that looks like below (apparently I don't have enough reputation to embed pictures in a post; click the links to see the picture).
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots()
ax.plot([1, 2, 3], label="test1")
ax.plot([3, 2, 1], label="test2")
ax.legend()
target figure
You can separate the figure and the legend in two different ax objects:
fig, [ax1, ax2] = plt.subplots(1, 2)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
fixed figure 1
You can easily control where the legend should go:
fig, [ax1, ax2] = plt.subplots(2, 1)
ax1.plot([1, 2, 3], label="test1")
ax1.plot([3, 2, 1], label="test2")
ax2.plot([1, 2, 3], label="test1")
ax2.plot([3, 2, 1], label="test2")
h, l = ax2.get_legend_handles_labels()
ax2.clear()
ax2.legend(h, l, loc='upper left')
ax2.axis('off')
fixed figure 2
I'd like to add a small contribution for the specific case where your legend is customized such as here: https://matplotlib.org/3.1.1/gallery/text_labels_and_annotations/custom_legends.html
In that case, you might have to go for a different method. I've been exposed to that problem and the answers above did not work for me.
The code below sets-up the legend.
import cmocean
import matplotlib
from matplotlib.lines import Line2D
lightcmap = cmocean.tools.lighten(cmo.solar, 0.7)
custom_legend = []
custom_legend_strings=['no impact - high confidence', 'no impact - low confidence', 'impact - low confidence', 'impact - high confidence']
for nbre_classes in range(len(custom_legend_strings)):
custom_legend.append(Line2D([0], [0], color=lightcmap(nbre_classes/len(custom_legend_strings)), lw=4))
I think because this kind of legend is attached the axes, a little trick was necessary :
center the legend with a big font to make it take most of the available space and do not erase but set the axes to off :
fig,ax = plt.subplots(figsize=(10,10))
ax.legend(custom_legend,custom_legend_strings, loc = 10, fontsize=30)
plt.axis('off')
fig.savefig('legend.png', bbox_inches='tight')
The result is :
the result
I was not able to find exactly what I wanted in the existing answer so I implemented it. It wanted to generate a standalone legend without any figure attached nor visual "glitches". I came up with this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Patch
palette = dict(zip(['one', 'two', 'tree', 'four'], ['b', 'g', 'r', 'k']))
def export_legend(palette, dpi="figure", filename="legend.png"):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox = bbox.transformed(fig.dpi_scale_trans.inverted())
fig.savefig(filename, dpi=dpi, transparent=True, bbox_inches=bbox)
# Delete the legend along with its temporary figure
plt.close(fig)
export_legend(palette, dpi=400)
Note that the surrounding background is transparent, so adding the legend on top of a figure should not have white "glitches" in the corners, nor issue of cropped border.
And if you don't want to save the image of disk, here is the trick !
DPI = 400
def export_legend(palette):
# Create empty figure with the legend
handles = [Patch(color=c, label=l) for l, c in palette.items()]
fig = plt.figure()
legend = fig.gca().legend(handles=handles, framealpha=1, frameon=True)
# Render the legend
fig.canvas.draw()
# Export the figure, limiting the bounding box to the legend area,
# slighly extended to ensure the surrounding rounded corner box of
# is not cropped. Transparency is enabled, so it is not an issue.
bbox = legend.get_window_extent().padded(2)
bbox_inches = bbox.transformed(fig.dpi_scale_trans.inverted())
bbox_inches = bbox.from_extents(np.round(bbox_inches.extents * 400) / 400)
io_buf = io.BytesIO()
fig.savefig(io_buf, format='rgba', dpi=DPI, transparent=True, bbox_inches=bbox_inches)
io_buf.seek(0)
img_raw = io_buf.getvalue()
img_size = (np.asarray(bbox_inches.bounds)[2:] * DPI).astype(int)
# Delete the legend along with its temporary figure
plt.close(fig)
return img_raw, img_size
The raw buffer can be read directly using PIL or whatever dealing with raw buffer.
So I was playing with this idea and simplest thing I have found is this (works with multiple axes):
def export_legend(filename="legend.png", fig=fig):
legend = fig.legend(framealpha=1)
fig2 = legend.figure
fig2.canvas.draw()
bbox = legend.get_window_extent().transformed(fig2.dpi_scale_trans.inverted())
fig2.savefig(filename, dpi="figure", bbox_inches=bbox, facecolor="w")
legend.remove() # removes legend from showing on plot
export_legend()
Output of a function (I hided labels with boxes):
fig is from fig, ax = plt.subplots()
If you want legend to still show on plot you can use (for example):
fig.legend(loc="upper right", bbox_to_anchor=(1, 1), bbox_transform=ax.transAxes)

Categories