I want to to create a figure using matplotlib where I can explicitly specify the size of the axes, i.e. I want to set the width and height of the axes bbox.
I have looked around all over and I cannot find a solution for this. What I typically find is how to adjust the size of the complete Figure (including ticks and labels), for example using fig, ax = plt.subplots(figsize=(w, h))
This is very important for me as I want to have a 1:1 scale of the axes, i.e. 1 unit in paper is equal to 1 unit in reality. For example, if xrange is 0 to 10 with major tick = 1 and x axis is 10cm, then 1 major tick = 1cm. I will save this figure as pdf to import it to a latex document.
This question brought up a similar topic but the answer does not solve my problem (using plt.gca().set_aspect('equal', adjustable='box') code)
From this other question I see that it is possible to get the axes size, but not how to modify them explicitly.
Any ideas how I can set the axes box size and not just the figure size. The figure size should adapt to the axes size.
Thanks!
For those familiar with pgfplots in latex, it will like to have something similar to the scale only axis option (see here for example).
The axes size is determined by the figure size and the figure spacings, which can be set using figure.subplots_adjust(). In reverse this means that you can set the axes size by setting the figure size taking into acount the figure spacings:
import matplotlib.pyplot as plt
def set_size(w,h, ax=None):
""" w, h: width, height in inches """
if not ax: ax=plt.gca()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
figw = float(w)/(r-l)
figh = float(h)/(t-b)
ax.figure.set_size_inches(figw, figh)
fig, ax=plt.subplots()
ax.plot([1,3,2])
set_size(5,5)
plt.show()
It appears that Matplotlib has helper classes that allow you to define axes with a fixed size Demo fixed size axes
I have found that ImportanceofBeingErnests answer which modifies that figure size to adjust the axes size provides inconsistent results with the paticular matplotlib settings I use to produce publication ready plots. Slight errors were present in the final figure size, and I was unable to find a way to solve the issue with his approach. For most use cases I think this is not a problem, however the errors were noticeable when combining multiple pdf's for publication.
In lieu of developing a minimum working example to find the real issue I am having with the figure resizing approach I instead found a work around which uses the fixed axes size utilising the divider class.
from mpl_toolkits.axes_grid1 import Divider, Size
def fix_axes_size_incm(axew, axeh):
axew = axew/2.54
axeh = axeh/2.54
#lets use the tight layout function to get a good padding size for our axes labels.
fig = plt.gcf()
ax = plt.gca()
fig.tight_layout()
#obtain the current ratio values for padding and fix size
oldw, oldh = fig.get_size_inches()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
#work out what the new ratio values for padding are, and the new fig size.
neww = axew+oldw*(1-r+l)
newh = axeh+oldh*(1-t+b)
newr = r*oldw/neww
newl = l*oldw/neww
newt = t*oldh/newh
newb = b*oldh/newh
#right(top) padding, fixed axes size, left(bottom) pading
hori = [Size.Scaled(newr), Size.Fixed(axew), Size.Scaled(newl)]
vert = [Size.Scaled(newt), Size.Fixed(axeh), Size.Scaled(newb)]
divider = Divider(fig, (0.0, 0.0, 1., 1.), hori, vert, aspect=False)
# the width and height of the rectangle is ignored.
ax.set_axes_locator(divider.new_locator(nx=1, ny=1))
#we need to resize the figure now, as we have may have made our axes bigger than in.
fig.set_size_inches(neww,newh)
Things worth noting:
Once you call set_axes_locator() on an axis instance you break the tight_layout() function.
The original figure size you choose will be irrelevent, and the final figure size is determined by the axes size you choose and the size of the labels/tick labels/outward ticks.
This approach doesn't work with colour scale bars.
This is my first ever stack overflow post.
another method using fig.add_axes was quite accurate. I have included 1 cm grid aswell
import matplotlib.pyplot as plt
import matplotlib as mpl
# This example fits a4 paper with 5mm margin printers
# figure settings
figure_width = 28.7 # cm
figure_height = 20 # cm
left_right_magrin = 1 # cm
top_bottom_margin = 1 # cm
# Don't change
left = left_right_magrin / figure_width # Percentage from height
bottom = top_bottom_margin / figure_height # Percentage from height
width = 1 - left*2
height = 1 - bottom*2
cm2inch = 1/2.54 # inch per cm
# specifying the width and the height of the box in inches
fig = plt.figure(figsize=(figure_width*cm2inch,figure_height*cm2inch))
ax = fig.add_axes((left, bottom, width, height))
# limits settings (important)
plt.xlim(0, figure_width * width)
plt.ylim(0, figure_height * height)
# Ticks settings
ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(5))
ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(5))
ax.yaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))
# Grid settings
ax.grid(color="gray", which="both", linestyle=':', linewidth=0.5)
# your Plot (consider above limits)
ax.plot([1,2,3,5,6,7,8,9,10,12,13,14,15,17])
# save figure ( printing png file had better resolution, pdf was lighter and better on screen)
plt.show()
fig.savefig('A4_grid_cm.png', dpi=1000)
fig.savefig('tA4_grid_cm.pdf')
result:
Related
Here's a simplified version of what I'm trying that exhibits the same issue I'm experiencing.
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['toolbar'] = 'None'
class StackExample:
def __init__(self, coords):
self.coords = coords
# Create figure / axis, add points to axis, set their colors
# Width value chosen at random, height chosen to maintain the aspect ratio of the data
width = 4
height = 1.75 * width
# figsize = (width, height) does make the window generate at the size I want it to successfully.
self.fig = plt.figure(figsize=(width, height), dpi=100)
# Setting color to red for easier debugging
self.fig.set_facecolor('red')
self.ax = self.fig.add_subplot(111, projection='3d')
self.ax.set_position([0, 0, 1, 1.75])
self.ax.set_facecolor('lightgray')
# The problem is that the lightgray area always has an aspect ratio of 1:1.
# I cannot figure out how to change its shape to match that of its containing window.
# I would like it to have the same aspect ratio as self.fig, so no red is visible at all when the window generates.
self.scatter = self.ax.scatter(coords[:,0], coords[:,1], coords[:,2])
# Camera is parallel to the xy plane
self.ax.elev = 0
# Camera is pointing backward along the positive x-axis
self.ax.azim = 0
self.ax.dist = 6
# Adjust axis bounds
self.ax.set_xlim3d(-1, 1)
self.ax.set_ylim3d(-1, 1)
self.ax.set_zlim3d(0, 3.5)
# Set the aspect ratio.
self.ax.set_box_aspect((1, 1, 1.75))
# Adjust plot layout
self.fig.tight_layout()
self.fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
# Show the plot - need `block=False` to not lock the thread
plt.show(block=False)
# 800 randomly generated points. x and y coordinates vary from -1 to 1, z coordinates vary from 0 to 3.5.
# Note that this means the data has a 1:1.75 width:height ratio when viewed from the positive x-axis
coords = np.random.rand(800, 3)
coords[:,0] = coords[:,0] * 2 - 1
coords[:,1] = coords[:,1] * 2 - 1
coords[:,2] = coords[:,2] * 3.5
tree = StackExample(coords)
When I run the code, it generates that output. I'm trying to force a 1:1.75 aspect ratio - and the red area, which is defined by self.fig, is correctly in that ratio. I would like to get self.ax, the lightgray subplot to self.fig, to expand to fill the window - so no red should be visible at all. The lightgray self.ax region is exactly a 1:1 aspect ratio, and I cannot figure out how to change that.
I've been messing with this for a while, and nothing I've tried has successfully made the lightgray region anything but a 1:1 square. It's difficult to go into specifics of what I tried, because I basically tried everything I could find when googling for what I want.
Note that I'm not asking how to change the aspect ratio of the actual axes that make up the graph, which is something the provided code already does - it's the actual part of the matplotlib window where points are displayed, shown in gray in the included image, that I want to modify.
I'm very much a matplotlib novice - am I missing something obvious here? This feels like it should be a very basic feature. I'm getting the impression that using a 3D projection makes all or almost all of the solutions people have posted online invalid.
I want to create an image like this, but I'm unable to put the individual plots inside a frame.
Figures and axes have a patch attribute, which is the rectangle that makes up the background. Setting a figure frame is hence pretty straightforward:
import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 1)
# add a bit more breathing room around the axes for the frames
fig.subplots_adjust(top=0.85, bottom=0.15, left=0.2, hspace=0.8)
fig.patch.set_linewidth(10)
fig.patch.set_edgecolor('cornflowerblue')
# When saving the figure, the figure patch parameters are overwritten (WTF?).
# Hence we need to specify them again in the save command.
fig.savefig('test.png', edgecolor=fig.get_edgecolor())
Now the axes are a much tougher nut to crack. We could use the same approach as for the figure (which #jody-klymak I think is suggesting), however, the patch only corresponds to the area that is inside the axis limits, i.e. it does not include the tick labels, axis labels, nor the title.
However, axes have a get_tightbbox method, which is what we are after. However, using that also has some gotchas, as explained in the code comments.
# We want to use axis.get_tightbbox to determine the axis dimensions including all
# decorators, i.e. tick labels, axis labels, etc.
# However, get_tightbox requires the figure renderer, which is not initialized
# until the figure is drawn.
plt.ion()
fig.canvas.draw()
for ii, ax in enumerate(axes):
ax.set_title(f'Title {ii+1}')
ax.set_ylabel(f'Y-Label {ii+1}')
ax.set_xlabel(f'X-Label {ii+1}')
bbox = ax.get_tightbbox(fig.canvas.get_renderer())
x0, y0, width, height = bbox.transformed(fig.transFigure.inverted()).bounds
# slightly increase the very tight bounds:
xpad = 0.05 * width
ypad = 0.05 * height
fig.add_artist(plt.Rectangle((x0-xpad, y0-ypad), width+2*xpad, height+2*ypad, edgecolor='red', linewidth=3, fill=False))
fig.savefig('test2.png', edgecolor=fig.get_edgecolor())
plt.show()
I found something very similar and somehow configured it out what its doing .
autoAxis1 = ax8i[1].axis() #ax8i[1] is the axis where we want the border
import matplotlib.patches as ptch
rec = ptch.Rectangle((autoAxis1[0]-12,autoAxis1[2]-30),(autoAxis1[1]-
autoAxis1[0])+18,(autoAxis1[3]-
autoAxis1[2])+35,fill=False,lw=2,edgecolor='cyan')
rec = ax8i[1].add_patch(rec)
rec.set_clip_on(False)
The code is a bit complex but once we get to know what part of the bracket inside the Rectangle() is doing what its quite easy to get the code .
I have a series of plots with categorical data on the y-axis. It seems that the additional margin between the axis and the data is correlated with the number of categories on the y-axis. If there are many categories, an additional margin appears, but if there are few, the margin is so small that the data points are being cut. The plots look like this:
The plot with few categories and too small margin:
The plot with many categories and too big margins (click for full size):
For now, I only found solutions to manipulate the white space around the plot, like bbox_inches='tight' or fig.tight_layout(), but this doesn't solve my problem.
I don't have such problems with the x-axis, can this be a question of x-axis containing only numerical values and y-axis categorical data?
The code I'm using to generate all the plots looks like this:
sns.set(style='whitegrid')
plt.xlim(left=left_lim, right=right_lim)
plt.xticks(np.arange(left_lim, right_lim, step))
plot = sns.scatterplot(method.loc[:,'Len'],
method.loc[:,'Bond'],
hue = method.loc[:,'temp'],
palette= palette,
legend = False,
s = 50)
set_size(width, height)
plt.savefig("method.png", dpi = 100, bbox_inches='tight', pad_inches=0)
plt.show()
The set_size() comes from the first answer to Axes class - set explicitly size (width/height) of axes in given units.
We can slightly adapt the function from Axes class - set explicitly size (width/height) of axes in given units
to add a line setting the axes margins.
import matplotlib.pyplot as plt
def set_size(w,h, ax=None, marg=(0.1, 0.1)):
""" w, h: width, height in inches """
if not ax: ax=plt.gca()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
figw = float(w)/(r-l)
figh = float(h)/(t-b)
ax.figure.set_size_inches(figw, figh)
ax.margins(x=marg[0]/w, y=marg[1]/h)
And call it with
set_size(width, height, marg=(xmargin, ymargin))
where xmargin, ymargin are the margins in inches.
I'm dynamically generating a horizontal bar plot using MatPlotLib. It works pretty well most of the time, until people try to plot a very large numbers of data points. MatPlotLib tries to squish all of the bars into the plot and they start to disappear.
The ideal solution would be to generate the plot so that every horizontal bar is one pixel in height, with 1px separating every bar. The total height of the resulting plot image would then be dependent on the number of bars. But as everything in MatPlotLib is relative, I'm getting really stuck in how to do this. Any help would be much appreciated!
One option is to generate an image with the bars as pixels.
import matplotlib.pyplot as plt
import numpy as np
dpi = 100
N = 100 # numbner of bars (approx. half the number of pixels)
w = 200 #width of plot in pixels
sp = 3 # spacing within axes in pixels
bp = 50; lp = 70 # bottom, left pixel spacing
bottom=float(bp)/(2*N+2*sp+2*bp)
top = 1.-bottom
left=float(lp)/(w+2*lp)
right=1.-left
figheight = (2*N+2*sp)/float(dpi)/(1-(1-top)-bottom) #inch
figwidth = w/float(dpi)/(1-(1-right)-left)
# this is the input array to plot
inp = np.random.rand(N)+0.16
ar = np.zeros((2*N+2*sp,w))
ninp = np.round(inp/float(inp.max())*w).astype(np.int)
for n in range(N):
ar[2*n+sp, 0: ninp[n]] = np.ones(ninp[n])
fig, ax=plt.subplots(figsize=(figwidth, figheight), dpi=dpi)
plt.subplots_adjust(left=left, bottom=bottom, right=right, top=top)
plt.setp(ax.spines.values(), linewidth=0.5)
ext = [0,inp.max(), N-0.5+(sp+0.5)/2., -(sp+0.5)/2.]
ax.imshow(ar, extent=ext, interpolation="none", cmap="gray_r", origin="upper", aspect="auto")
ax.set_xlim((0,inp.max()*1.1))
ax.set_ylabel("item")
ax.set_xlabel("length")
plt.savefig(__file__+".png", dpi=dpi)
plt.show()
This will work for any setting of dpi.
Note that the ticklabels might appear a bit off, which is an inaccuracy from matplotlib; which I don't know how to overcome.
This example shows how you can plot lines with 1 pixel width:
yinch = 2
fig, ax = plt.subplots(figsize=(3,yinch), facecolor='w')
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
ypixels = int(yinch*fig.get_dpi())
for i in range(ypixels):
if i % 2 == 0:
c = 'k'
else:
c = 'w'
ax.plot([0,np.random.rand()], [i,i], color=c, linewidth=72./fig.get_dpi())
ax.set_ylim(0,ypixels)
ax.axis('off')
This is what the result looks like (magnified 200%):
edit:
Using a different dpi is not problem, but then using plot() becomes less useful because you cant specify the linewidth units. You can calculate the needed linewidth, but i think using barh() is more clear in that scenario.
In the example above i simply disabled the axis to focus on the 1px bars, if you remove that you can plot as normal. Spacing around it is not a problem because Matplotlib isn't bound to the 0-1 range for a Figure, but you want to add bbox_inches='tight' to your savefig to include artists outside of the normal 0-1 range. If you spend a lot of time 'precise' plotting within you axes, i think its easier to stretch the axis to span the entire figure size. You of course take a different approach but that would require you to also calculate the axes size in inches. Both angles would work, it depends or your precise case which might be more convenient.
Also be aware that old versions of Matplotlib (<2.0?) have a different default figure.dpi and savefig.dpi. You can avoid this by adding dpi=fig.get_dpi() to your savefig statement. One of many reasons to upgrade. ;)
yinch = 2
dpi = 128
fig, ax = plt.subplots(figsize=(3,yinch), facecolor='w', dpi=dpi)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
ypixels = int(yinch*fig.get_dpi())
for i in range(ypixels):
if i % 2 == 0:
c = '#aa0000'
else:
c = 'w'
ax.barh(i,np.random.rand(), height=1, color=c)
ax.set_title('DPI %i' % dpi)
ax.set_ylim(0,ypixels)
fig.savefig('mypic.png', bbox_inches='tight')
Changing the vertical distance between two subplot using tight_layout(h_pad=-1) changes the total figuresize. How can I define the figuresize using tight_layout?
Here is the code:
#define figure
pl.figure(figsize=(10, 6.25))
ax1=subplot(211)
img=pl.imshow(np.random.random((10,50)), interpolation='none')
ax1.set_xticklabels(()) #hides the tickslabels of the first plot
subplot(212)
x=linspace(0,50)
pl.plot(x,x,'k-')
xlim( ax1.get_xlim() ) #same x-axis for both plots
And here is the results:
If I write
pl.tight_layout(h_pad=-2)
in the last line, then I get this:
As you can see, the figure is bigger...
You can use a GridSpec object to control precisely width and height ratios, as answered on this thread and documented here.
Experimenting with your code, I could produce something like what you want, by using a height_ratio that assigns twice the space to the upper subplot, and increasing the h_pad parameter to the tight_layout call. This does not sound completely right, but maybe you can adjust this further ...
import numpy as np
from matplotlib.pyplot import *
import matplotlib.pyplot as pl
import matplotlib.gridspec as gridspec
#define figure
fig = pl.figure(figsize=(10, 6.25))
gs = gridspec.GridSpec(2, 1, height_ratios=[2,1])
ax1=subplot(gs[0])
img=pl.imshow(np.random.random((10,50)), interpolation='none')
ax1.set_xticklabels(()) #hides the tickslabels of the first plot
ax2=subplot(gs[1])
x=np.linspace(0,50)
ax2.plot(x,x,'k-')
xlim( ax1.get_xlim() ) #same x-axis for both plots
fig.tight_layout(h_pad=-5)
show()
There were other issues, like correcting the imports, adding numpy, and plotting to ax2 instead of directly with pl. The output I see is this:
This case is peculiar because of the fact that the default aspect ratios of images and plots are not the same. So it is worth noting for people looking to remove the spaces in a grid of subplots consisting of images only or of plots only that you may find an appropriate solution among the answers to this question (and those linked to it): How to remove the space between subplots in matplotlib.pyplot?.
The aspect ratios of the subplots in this particular example are as follows:
# Default aspect ratio of images:
ax1.get_aspect()
# 1.0
# Which is as it is expected based on the default settings in rcParams file:
matplotlib.rcParams['image.aspect']
# 'equal'
# Default aspect ratio of plots:
ax2.get_aspect()
# 'auto'
The size of ax1 and the space beneath it are adjusted automatically based on the number of pixels along the x-axis (i.e. width) so as to preserve the 'equal' aspect ratio while fitting both subplots within the figure. As you mentioned, using fig.tight_layout(h_pad=xxx) or the similar fig.set_constrained_layout_pads(hspace=xxx) is not a good option as this makes the figure larger.
To remove the gap while preserving the original figure size, you can use fig.subplots_adjust(hspace=xxx) or the equivalent plt.subplots(gridspec_kw=dict(hspace=xxx)), as shown in the following example:
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
np.random.seed(1)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6.25),
gridspec_kw=dict(hspace=-0.206))
# For those not using plt.subplots, you can use this instead:
# fig.subplots_adjust(hspace=-0.206)
size = 50
ax1.imshow(np.random.random((10, size)))
ax1.xaxis.set_visible(False)
# Create plot of a line that is aligned with the image above
x = np.arange(0, size)
ax2.plot(x, x, 'k-')
ax2.set_xlim(ax1.get_xlim())
plt.show()
I am not aware of any way to define the appropriate hspace automatically so that the gap can be removed for any image width. As stated in the docstring for fig.subplots_adjust(), it corresponds to the height of the padding between subplots, as a fraction of the average axes height. So I attempted to compute hspace by dividing the gap between the subplots by the average height of both subplots like this:
# Extract axes positions in figure coordinates
ax1_x0, ax1_y0, ax1_x1, ax1_y1 = np.ravel(ax1.get_position())
ax2_x0, ax2_y0, ax2_x1, ax2_y1 = np.ravel(ax2.get_position())
# Compute negative hspace to close the vertical gap between subplots
ax1_h = ax1_y1-ax1_y0
ax2_h = ax2_y1-ax2_y0
avg_h = (ax1_h+ax2_h)/2
gap = ax1_y0-ax2_y1
hspace=-(gap/avg_h) # this divided by 2 also does not work
fig.subplots_adjust(hspace=hspace)
Unfortunately, this does not work. Maybe someone else has a solution for this.
It is also worth mentioning that I tried removing the gap between subplots by editing the y positions like in this example:
# Extract axes positions in figure coordinates
ax1_x0, ax1_y0, ax1_x1, ax1_y1 = np.ravel(ax1.get_position())
ax2_x0, ax2_y0, ax2_x1, ax2_y1 = np.ravel(ax2.get_position())
# Set new y positions: shift ax1 down over gap
gap = ax1_y0-ax2_y1
ax1.set_position([ax1_x0, ax1_y0-gap, ax1_x1, ax1_y1-gap])
ax2.set_position([ax2_x0, ax2_y0, ax2_x1, ax2_y1])
Unfortunately, this (and variations of this) produces seemingly unpredictable results, including a figure resizing similar to when using fig.tight_layout(). Maybe someone else has an explanation for what is happening here behind the scenes.