how to remove the white space of invisiable axes in matplotlib during active plot? - matplotlib

I want to completely remove white space around my axes during active plot (not save_fig as others asked).
Here we cannot use bbox_inches='tight'. I can use tight_layout(pad=0).
When axis is on, it works fine, it shows all the ticks and x-y labels.
However, in some cases, I set the axis off. What I expected is to see the contents expand to fill up the empty space where the axes are. However, this does not work. It still keep the padding as there are still x-y labels and axes.
How can I remove the white space of invisible axes objects?
edit:
I am aware that I can use ax.set_yticks([]) and ax.set_xticks([]) to turn those off. But this is clumsy, I have to remember the the ticks before I clear them. And if I remove-then-add those ticks. The ticks cannot automatically update any more.
I wonder is there any more straightforward way to do this?
We can still see there is a small border spacing even after removing all ticks. If someone can come up a way to remove that too. It will be fantastic.
I would also like to keep the title if there is one. Thus the hard-coded ax.set_position([0,0,1,x]) is not very good for this usage. Surely we can still try to get the top spacing when there is a title, but if someone can provide a more direct/simple way to handle this, it will be preferred.
Example code:
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
plt.show()
return fig, ax, text

You may adjust the subplot parameters depending on whether you turned the axis off or not.
import matplotlib.pyplot as plt
from matplotlib import patches
def demo_tight_layout(w=10, h=6, axisoff=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
fig.tight_layout()
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
params = dict(bottom=0, left=0, right=1)
if ax.get_title() == "":
params.update(top=1)
fig.subplots_adjust(**params)
fig.text(0.5, 0.6, " ".join(_texts))
plt.show()
Now demo_tight_layout(axisoff=True) produces
and demo_tight_layout(axisoff=False) produces

You need to set the axes position to fill the figure. If you create your figure and plot with
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca()
ax.plot(some_x_data, some_y_data)
you need to add the following line to fill the figure with the axes:
ax.set_position([0, 0, 1, 1], which='both')
This sets the axes location relative to the figure size in the following way:
[left, bottom, width, height]
So to completely fill the figure use [0, 0, 1, 1] as shown above.
So taking your code, it should look like this (using fill_figure bool to check):
def demo_tight_layout(w=10, h=6, axisoff=False, removeticks=False, fill_figure=False):
fig,ax = plt.subplots()
fig.set_facecolor((0.8, 0.8, 0.8))
rect = patches.Rectangle((-w/2, -h/2), w, h, color='#00ffff', alpha=0.5)
ax.add_patch(rect)
ax.plot([-w/2,w/2], [-h/2,h/2])
ax.plot([-w/2,w/2], [h/2,-h/2])
ax.set_ylabel("ylabel")
ax.margins(0)
_texts = []
if axisoff:
ax.set_axis_off()
_texts.append("axisoff")
if removeticks:
ax.set_xticks([])
ax.set_yticks([])
ax.set_ylabel("")
_texts.append("removeticks")
fig.text(0.5, 0.6, " ".join(_texts))
fig.tight_layout(pad=0)
if fill_figure:
ax.set_position([0, 0, 1, 1], which='both')
plt.show()
return fig, ax, text
ax.set_position needs to be after fig.tight_layout.
If a figure title is needed, there is no direct way to do it. This unluckily can't be avoided. You need to adapt the height parameters manually so that the title fits in the figure, for example with:
ax.set_position([0, 0, 1, .9], which='both')

Related

How to make the plot's shape round?

I have created a plot, which is working just fine.
But I really want to change its shape to a circle.
This is my current plotting code:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(5,5), dpi=300)
ax = fig.add_axes([0,0,1,1])
ax.plot(30, 80, marker="o", markersize=20, markeredgecolor="#ed6033", markerfacecolor="#ed6033")
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
ax.set_facecolor('#8cc9e2')
ax.margins(0.1)
plt.setp(ax.get_xticklabels()[4], visible=False)
plt.xlim(10, 90)
plt.ylim(10, 90)
plt.grid(color='white')
plt.show()
and this is the output I get:
eventually, this is my desired output:
You can clip the path of artists including the background patch using the path of another artist.
Add this snippet before the plt.show() call:
clip_path = plt.Circle(
(0.5, 0.5), 0.5, transform=ax.transAxes, # circle coordinates defined in axes fractions
fill=None, linewidth=0 # makes circle invisible
)
ax.add_patch(clip_path)
ax.patch.set_clip_path(clip_path)

How to access and remove all unwanted objects in a matplotlib figure manually?

I am trying to understand the underlying concepts of matplotlib, especially Axes and Figure. Therefore I am trying to plot two scatters and then remove any superfluous space (the red one below) by accessing different APIs & objects in the hierarchy.
Yet I fail to understand where the remaining red space is coming from. This is the code:
# Random data
df = pd.DataFrame(np.random.randint(0,100,size=(100, 2)), columns=list('AB'))
# Create a single Axes and preconfigure the figure with red facecolor.
# Then plot a scatter
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10,5), facecolor='r')
ax1 = df.plot(kind='scatter', x='A', y='B', ax=axes[0])
ax2 = df.plot(kind='scatter', x='B', y='A', ax=axes[1])
# Remove except the scatter
for a in [ax1, ax2]:
a.set_xlabel(''), a.set_ylabel('') # Remove x and y labels
for loc in ['left', 'right', 'bottom', 'top']:
a.spines[loc].set_visible(False) # Remove spines
a.set_xticks([], []), a.set_yticks([], []) # Remove ticks
a.set_xmargin(0), a.set_ymargin(0) # No margin beyond outer values
# On figure-level we can make it more tight
fig.tight_layout()
It produces the following figure:
I saw that there is something like..
a.set_axis_off()
.. but this doesn't seem to be the right solution. Somewhere there seems to be some kind of padding that remains. It doesn't look like it's from some X/Y axis as it's the same for all four edges in both subplots.
Any help appreciated.
Solution
Two things are needed:
First we need to initialize the Figure with frameon=False:
fig, axes = plt.subplots(
// ...
frameon=False)
The space between the subplots can be removed using the subplot layout:
plt.subplots_adjust(wspace=.0, hspace=.0)
For the finest level of layout control, you can position your axes manually instead of relying on matplotlib to do it for you. There are a couple of ways of doing this.
One option is Axes.set_position
# Random data
df = pd.DataFrame(np.random.randint(0,100,size=(100, 2)), columns=list('AB'))
# Create a pair of Axes and preconfigure the figure with red facecolor.
# Then plot a scatter
fig, axes = plt.subplots(1, 2, figsize=(10, 5), facecolor='r')
df.plot(kind='scatter', x='A', y='B', ax=axes[0]).set_position([0, 0, 0.5, 1])
df.plot(kind='scatter', x='B', y='A', ax=axes[1]).set_position([0, 0.5, 0.5, 1])
You could also use the old-fashioned Figure.add_axes method:
# Random data
df = pd.DataFrame(np.random.randint(0,100,size=(100, 2)), columns=list('AB'))
# Create a pair of Axes and preconfigure the figure with red facecolor.
# Then plot a scatter
fig = plt.figure(figsize=(10, 5), facecolor='r')
df.plot(kind='scatter', x='A', y='B', ax=fig.add_axes([0, 0, 0.5, 1]))
df.plot(kind='scatter', x='B', y='A', ax=fig.add_axes([0, 0.5, 0.5, 1]))

Creating figure with exact size and no padding (and legend outside the axes)

I am trying to make some figures for a scientific article, so I want my figures to have a specific size. I also see that Matplotlib by default adds a lot of padding on the border of the figures, which I don't need (since the figures will be on a white background anyway).
To set a specific figure size I simply use plt.figure(figsize = [w, h]), and I add the argument tight_layout = {'pad': 0} to remove the padding. This works perfectly, and even works if I add a title, y/x-labels etc. Example:
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
plt.savefig('figure01.pdf')
This creates a pdf file with exact size 3x2 (inches).
The issue I have is that when I for example add a text box outside the axis (typically a legend box), Matplotlib does not make room for the text box like it does when adding titles/axis labels. Typically the text box is cut off, or does not show in the saved figure at all. Example:
plt.close('all')
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure02.pdf')
A solution I found elsewhere on SO was to add the argument bbox_inches = 'tight' to the savefig command. The text box is now included like I wanted, but the pdf is now the wrong size. It seems like Matplotlib just makes the figure bigger, instead of reducing the size of the axes like it does when adding titles and x/y-labels.
Example:
plt.close('all')
fig = plt.figure(
figsize = [3,2],
tight_layout = {'pad': 0}
)
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
t = ax.text(0.7, 1.1, 'my text here', bbox = dict(boxstyle = 'round'))
plt.savefig('figure03.pdf', bbox_inches = 'tight')
(This figure is 3.307x2.248)
Is there any solution to this that covers most cases with a legend just outside the axes?
So the requirements are:
Having a fixed, predefined figure size
Adding a text label or legend outside the axes
Axes and text cannot overlap
The axes, together with the title and axis labels, sits tightly agains the figure border.
So tight_layout with pad = 0, solves 1. and 4. but contradicts 2.
One could think on setting pad to a larger value. This would solve 2. However, since it's is symmetric in all directions, it would contradict 4.
Using bbox_inches = 'tight' changes the figure size. Contradicts 1.
So I think there is no generic solution to this problem.
Something I can come up with is the following: It sets the text in figure coordinates and then resizes the axes either in horizontal or in vertical direction such that there is no overlap between the axes and the text.
import matplotlib.pyplot as plt
import matplotlib.transforms
fig = plt.figure(figsize = [3,2])
ax = fig.add_subplot(111)
plt.title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
def text_legend(ax, x0, y0, text, direction = "v", padpoints = 3, margin=1.,**kwargs):
ha = kwargs.pop("ha", "right")
va = kwargs.pop("va", "top")
t = ax.figure.text(x0, y0, text, ha=ha, va=va, **kwargs)
otrans = ax.figure.transFigure
plt.tight_layout(pad=0)
ax.figure.canvas.draw()
plt.tight_layout(pad=0)
offs = t._bbox_patch.get_boxstyle().pad * t.get_size() + margin # adding 1pt
trans = otrans + \
matplotlib.transforms.ScaledTranslation(-offs/72.,-offs/72.,fig.dpi_scale_trans)
t.set_transform(trans)
ax.figure.canvas.draw()
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 = trans2.transform(t._bbox_patch.get_window_extent())
bbox = ax.get_position()
if direction=="v":
ax.set_position([bbox.x0, bbox.y0,bbox.width, tbox[0][1]-bbox.y0])
else:
ax.set_position([bbox.x0, bbox.y0,tbox[0][0]-bbox.x0, bbox.height])
# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#text_legend(ax, 1,1, 'my text here', bbox = dict(boxstyle = 'round'), )
# case 2: place text left of axes, (1, y), direction=="v"
text_legend(ax, 1., 0.8, 'my text here', margin=2., direction="h", bbox = dict(boxstyle = 'round') )
plt.savefig(__file__+'.pdf')
plt.show()
case 1 (left) and case 2 (right):
Doin the same with a legend is slightly easier, because we can directly use the bbox_to_anchor argument and don't need to control the fancy box around the legend.
import matplotlib.pyplot as plt
import matplotlib.transforms
fig = plt.figure(figsize = [3.5,2])
ax = fig.add_subplot(111)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([1,2,3], marker="o", label="quantity 1")
ax.plot([2,1.7,1.2], marker="s", label="quantity 2")
def legend(ax, x0=1,y0=1, direction = "v", padpoints = 3,**kwargs):
otrans = ax.figure.transFigure
t = ax.legend(bbox_to_anchor=(x0,y0), loc=1, bbox_transform=otrans,**kwargs)
plt.tight_layout(pad=0)
ax.figure.canvas.draw()
plt.tight_layout(pad=0)
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])
# case 1: place text label at top right corner of figure (1,1). Adjust axes height.
#legend(ax, borderaxespad=0)
# case 2: place text left of axes, (1, y), direction=="h"
legend(ax,y0=0.8, direction="h", borderaxespad=0.2)
plt.savefig(__file__+'.pdf')
plt.show()
Why 72? The 72 is the number of points per inch (ppi). This is a fixed typographic unit e.g. fontsizes are always given in points (like 12pt). Because matplotlib defines the padding of the text box in units relative to fontsize, which is points, we need to use 72 to transform back to inches (and then to display coordinates). The default dots per inch (dpi) is not touched here, but is accounted for in fig.dpi_scale_trans. If you want to change dpi you need to make sure the figure dpi is set when creating the figure as well as when saving it (use dpi=.. in the call to plt.figure() as well as plt.savefig()).
As of matplotlib==3.1.3, you can use constrained_layout=True to achieve the desired result. This is currently experimental, but see the docs for a very helpful guide (and the section specifically on legends). Note that the legend will steal space from the plot, but this is unavoidable. I've found that as long as the legend does not take up too much space relative to the size of the plot, then the figure gets saved without cropping anything.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(3, 2), constrained_layout=True)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([0,1], [0,1], label='my text here')
ax.legend(loc='center left', bbox_to_anchor=(1.1, 0.5))
fig.savefig('figure03.pdf')

How do I extend the margin at the bottom of a figure in Matplotlib?

The following screenshot shows my x-axis.
I added some labels and rotated them by 90 degrees in order to better read them. However, pyplot truncates the bottom such that I'm not able to completely read the labels.
How do I extend the bottom margin in order to see the complete labels?
Two retroactive ways:
fig, ax = plt.subplots()
# ...
fig.tight_layout()
Or
fig.subplots_adjust(bottom=0.2) # or whatever
Here's a subplots_adjust example: http://matplotlib.org/examples/pylab_examples/subplots_adjust.html
(but I prefer tight_layout)
A quick one-line solution that has worked for me is to use pyplot's auto tight_layout method directly, available in Matplotlib v1.1 onwards:
plt.tight_layout()
This can be invoked immediately before you show the plot (plt.show()), but after your manipulations on the axes (e.g. ticklabel rotations, etc).
This convenience method avoids manipulating individual figures of subplots.
Where plt is the standard pyplot from:
import matplotlib.pyplot as plt
fig.savefig('name.png', bbox_inches='tight')
works best for me, since it doesn't reduce the plot size compared to
fig.tight_layout()
Subplot-adjust did not work for me, since the whole figure would just resize with the labels still out of bounds.
A workaround I found was to keep the y-axis always a certain margin over the highest or minimum y-values:
x1,x2,y1,y2 = plt.axis()
plt.axis((x1,x2,y1 - 100 ,y2 + 100))
fig, ax = plt.subplots(tight_layout=True)
This is rather complicated, but it gives a general and neat solution.
import numpy as np
value1 = 3
xvalues = [0, 1, 2, 3, 4]
line1 = [2.0, 3.0, 2.0, 5.0, 4.0]
stdev1 = [0.1, 0.2, 0.1, 0.4, 0.3]
line2 = [1.7, 3.1, 2.5, 4.8, 4.2]
stdev2 = [0.12, 0.18, 0.12, 0.3, 0.35]
max_times = [max(line1+stdev1),max(line2+stdev2)]
min_times = [min(line1+stdev1),min(line2+stdev2)]
font_size = 25
max_total = max(max_times)
min_total = min(min_times)
max_minus_min = max_total - min_total
step_size = max_minus_min/10
head_space = (step_size*3)
plt.figure(figsize=(15, 15))
plt.errorbar(xvalues, line1, yerr=stdev1, fmt='', color='b')
plt.errorbar(xvalues, line2, yerr=stdev2, fmt='', color='r')
plt.xlabel("xvalues", fontsize=font_size)
plt.ylabel("lines 1 and 2 Test "+str(value1), fontsize=font_size)
plt.title("Let's leave space for the legend Experiment"+ str(value1), fontsize=font_size)
plt.legend(("Line1", "Line2"), loc="upper left", fontsize=font_size)
plt.tick_params(labelsize=font_size)
plt.yticks(np.arange(min_total, max_total+head_space, step=step_size) )
plt.grid()
plt.tight_layout()
Result:

PyQt4 and matplotlib: Legend outside of the plot is cut off

I've been googling a lot, but I haven't found a solution.
The problem is the same as here: Moving matplotlib legend outside of the axis makes it cutoff by the figure box
But I don't want to save the figure, I just want to have the legend inside the whole figure. I've also tried tight_layout but that didn't work. I'm totally new to matplotlib and can't figure it out. Here is my source code (I'm embedding a matplot into PyQt4):
class GraphACanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=5, dpi=100):
self.fig = Figure(figsize=(width, height), facecolor=backColorHexName, dpi=dpi)
self.axes = self.fig.add_subplot(1, 1, 1, autoscale_on=False)
self.axes.set_xticks(arange(-0.1, 1.4, 0.1))
self.axes.set_yticks(arange(-0.1, 1.4, 0.1))
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self, QtGui.QSizePolicy.Expanding, Gui.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
def computeFigure(self):
col = []
for i in xrange(0, 100):
x = i/100
y = y/100
line, = self.axes.plot(x, y, 'o', color=blue, label='label')
col.append(line.get_c())
self.axes.set_xlabel('x', labelpad=10, rotation=0)
self.axes.set_ylabel('y', labelpad=10, rotation=0)
leg = self.axes.legend(loc=2, bbox_to_anchor=(1.0, 0.5), borderaxespad=0., fontsize='x-small', framealpha=0.25, markerscale=0.5, ncol=1, numpoints=1)
for color,text in zip(col,leg.get_texts()):
text.set_color(color)
test = GraphACanvas()
test.computeFigure()
I've put in here just dummy values. But in the application, a user can select nodes, and depending on how many nodes he select, the bigger/smaller is the legend. I would like to shrink the x-axis side to have more place for the legend.
--> The subplot should not fill out 100% (width/height) of the figure, rather 100% height and 80% width, so that my legend has enough space.
This line:
self.axes = self.fig.add_subplot(1, 1, 1, autoscale_on=False)
Will create an axes that fills out the whole figure (more or less) as you describe.
Try:
self.axes = self.fig.add_axes([0.1, 0.1, 0.7, 0.8])
The elements for that list are:
[left_edge, bottom_edge, width, height] where all values are fractions of the total figure dimensions.