How can I add an arbitrarily big white margin to a figure with subplots? - matplotlib

I am trying to add an arbitrarily big white margin (or padding) to a figure with subplots because I would like the subtitle of the figure not to overlap with any of the subplots or titles of these subplots. I am using Matplotlib 3.1.2.
Currently, I have the following source code.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(2, 1, figsize=(15, 10))
n = 10
x = np.arange(0, n)
y = np.random.rand(n)
ax[0].plot(x, y)
ax[0].set_xlabel('x')
ax[0].set_ylabel('y')
y = np.random.rand(n)
ax[1].plot(x, y)
ax[1].set_xlabel('x')
ax[1].set_ylabel('y')
fig.suptitle("I want to have white space around me!!!")
# fig.tight_layout(rect=[0, 0.03, 1, 0.80])
plt.subplots_adjust(top=0.85)
plt.show()
If I try to use either tight_layout or subplots_adjust (as suggested in several answers to this question Matplotlib tight_layout() doesn't take into account figure suptitle), it doesn't seem to have any effect on the margins. Here's the result of the execution of the previous example.
Is there a way to add an arbitrarily big white margin to the left, right, bottom and (or) top of a figure (with subplots)? I would like to specify the figure size and arbitrarily increase or decrease the white space around an image. I also would like the solution to work in case I decide to add a title for each of the subplots. How can this be done?

fig, axs = plt.subplots(2,1, figsize=(5,5))
fig.patch.set_facecolor('grey')
fig.suptitle("Look at all that grey space around me!!!")
fig.subplots_adjust(top=0.6, bottom=0.4, left=0.4, right=0.6)

Related

How to entend the area/boudaries that shows the data from a Axes3D with matplolib when using the set_box_aspect zoom

I'm trying to zoom in a 3D plot. I'm using the ax.set_box_aspect() fonction. When doing so, the axis are zoomed in, they appear bigger, but the area where the data can be seen stay at the same size as before (the plot are not using the total available space).
The aim in the end is to have two axis, the first one 3d, the other one 2d. I would have wanted the first plot to take all the space available at the top half of the figure.
Here is the code before the Zoom
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#---- generate data
nn = 100
X = np.random.randn(nn)*20 + 0
Y = np.random.randn(nn)*50 + 30
Z = np.random.randn(nn)*10 + -5
#---- check aspect ratio
asx, asy, asz = np.ptp(X), np.ptp(Y), np.ptp(Z)
fig = plt.figure(figsize=(15,15))
ax = fig.add_subplot(211, projection='3d')
#---- set box aspect ratio
ax.set_box_aspect((asx,asy,asz))
scat = ax.scatter(X, Y, Z, c=X+Y+Z, s=500, alpha=0.8)
ax.set_xlabel('X-axis'); ax.set_ylabel('Y-axis'); ax.set_zlabel('Z-axis')
ax = fig.add_subplot(212)
plt.show()
Before using the zoom
And now when I zoom in, the scatter is limitted in a square frame :
ax.set_box_aspect((asx,asy,asz), zoom = 2 )
After using the zoom
(The data used for the plot doesn't matter here, it is just to showcase my issue.)
I tried changing the axis limit with set_xlim3d or set_xlim, but in either case, the result is the same.
It seems like the showing area (I can't find the right word for it) stays a square no matter what.
I didn't find any usefull information on that matter online, (maybe from the lack of vocabulary to describe my problem).

Pyplot axis limits within boundaries

Is there an easy way to avoid pyplot zooming far into noisy data?
Something like a lower boundary for the axis limits.
I am not trying to set a fix boundary to my axis, as this will fully disable automatic scaling.
Maybe a "minimum tick distance" would also work.
Right now I am using an additional 'invisible' plot in my graph that will define the maximum zoom.
Some example that illustrates what I want to achieve:
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0, 100, 1)
noise = np.random.randn(len(x))*0.1
y = 10+noise
y_dummy_low = [0]*len(x)
y_dummy_high = [20]*len(x)
plt.figure()
plt.plot(x, y) # noise data i actually want to plot
plt.plot(x, y_dummy_low, y_dummy_high, marker="None", linestyle="None") # this will avoid zooming too much
plt.show()
Zooming too far
Zooming OK

How to fully customize subplot size in matplotlib

I want to have two subplots in a matplotlib figure that are sized and positioned relative to each other like the example below (for stylistic reasons). All the examples I've seen for customizing subplot placement and sizes still tile and fill the entire figure footprint. What can I do to get the rightmost plot positioned with some whitespace like below?
You need to imagine some (virtual) grid on which the subplots are placed.
The grid has 3 rows and 2 columns. The first subplot covers all three rows and the first column. The second subplot covers only the second row of the second column. The ratios between the row and column sizes are not necessarily equal.
import matplotlib.pyplot as plt
import matplotlib.gridspec
gs = matplotlib.gridspec.GridSpec(3,2, width_ratios=[1,1.4],
height_ratios=[1,3,1])
fig = plt.figure()
ax1 = fig.add_subplot(gs[:,0])
ax2 = fig.add_subplot(gs[1,1])
plt.show()
In addition you may still set different values to hspace and wspace parameters.
A good overview is given in the GridSpec tutorial.
Because it was mentionned in the comments: If absolute positionning in units of inches may be desired, I would recommend directly adding an axes in the desired size,
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
w,h = fig.get_size_inches()
div = np.array([w,h,w,h])
# define axes in by rectangle [left, bottom, width, height], numbers in inches
ax1 = fig.add_axes(np.array([.7, .7, 1.8, 3.4])/div)
ax2 = fig.add_axes(np.array([3, 1.4, 3, 2])/div)
plt.show()
--EDIT: This answer ended up startlingly similar to the answer given by #ImportanceOfBeingErnest but tacks on an approach for layout control in inches units rather than fractional units. --
It helps if you grid it out with gridspec, and then populate the grid using the desired spans of the ratios or columns. For a lot of the figures I make I need them to fit on the page well, so I use this pattern pretty frequently to give me grid control down to the 10th of an inch.
import matplotlib.pyplot as plt
from matplotlib import gridspec
fig = plt.figure(figsize=(7, 5)) # 7 inches wide, 5 inches tall
row = int(fig.get_figheight() * 10)
col = int(fig.get_figwidth() * 10)
gsfig = gridspec.GridSpec(
row, col,
left=0, right=1, bottom=0,
top=1, wspace=0, hspace=0)
gs1 = gsfig[:, 0:30]
# these spans are in tenths of an inch, so left-right
# spans from col 0 to column 30 (or 3 inches)
ax1 = fig.add_subplot(gs1)
gs1 = gsfig[20:40, 35:70] # again these spans are in tenths of an inch
ax1 = fig.add_subplot(gs1)

Colorbar frame and color not aligned

I have a vexing issue with a colorbar and even after vigorous research I cannot find the question even being asked. I have a plot where I overlay a contour and a pcolormesh and I would like a colorbar to indicate values. That works fine except for one thing:
The colorbar frame and color are offset
The colorbar frame and the actual bar are offset such that below you have a white bit in the frame and on top the color is poking out. While the frame is aligned with the axis as desired, the colorbar is offset.
Here is a working example that emulates the situation I was in, i.e. multiple plots with insets.
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
figheight = 4.2 - (2.1 - 49.519 / 25.4)
matplotlib.rcParams['figure.figsize'] = (5.25, figheight)
matplotlib.rcParams['axes.linewidth'] = 0.5
fig = plt.figure()
grid = gridspec.GridSpec(2, 1, height_ratios=[49.519 / 25.4 / figheight, 2.1 / figheight])
ax0 = plt.subplot(grid[0, 0])
ax1 = plt.subplot(grid[1, 0])
plt.tight_layout()
###############################################################################################
#
# Define position of inset
#
###############################################################################################
ax1.axis('off')
pos1 = ax1.get_position()
pos2 = matplotlib.transforms.Bbox([[pos1.x0, pos1.y0],
[.8*pos1.x1,
0.8*pos1.height + pos1.y0]])
left, bottom, width, height = [pos2.x0, pos2.y0, pos2.width, pos2.height]
ax2 = fig.add_axes([left, bottom, width, height])
###############################################################################################
#
# ax2 (inset) plot
#
###############################################################################################
pos2 = ax2.get_position()
ax2.axis('on')
x = np.linspace(0,5)
z = (np.outer(np.sin(x), np.cos(x))+1)*0.5
im = ax2.pcolormesh(z)
c = ax2.contour(z, linewidths=7)
ax2pos = ax2.get_position()
cbar_axis = fig.add_axes([ax2pos.x1+0.05,ax2pos.y0, .02, ax2pos.height])
colorbar = fig.colorbar(im, ax = ax2,
cax = cbar_axis, ticks = [0.1, .5, .9])
colorbar.outline.set_visible(True)
plot = 'Minimal.pdf'
fig.savefig(plot)
plt.close()
The problem persists in both the inline display and the saved .pdf if 'Inline' graphics backend is chosen. Using tight layout or not changes how badly the offset is depending on the size of the bar - same with using PyQT5 rather than inline graphics backend. I thought it was gone when I was changing between the various combinations, but I just realized it's still there.
I would appreciate any input.
As suggested by ImportanceOfBeingErnest I have tried using np.round on the figsize and that didn't change things. While you can fiddle around with sizes to make it look okay, it always stands over on one or the other side by some amount. When I change the graphics backend on Spyder 3 from 'Inline' to 'QT5' the problem becomes less severe with or without rounding. A summary of this is in this picture Colorbar overlap cases. Note that with not rounded and PyQT5 the problem still occurs, but is not as severe.
On inspection, it is clear that the colorbar is not only bleeding out over the top of its axes, but it's also positioned slightly to the left.
So, the problem here appears to be a conflict between the position of the colorbar axis and the colorbar itself when rasterization occurs. You can find more details on this issue in matplotlib's github repository, but I'll summarize what's going on here.
Colorbars are rasterized when the output is produced, so as to avoid artifacting issues during rendering. The position of the colorbar is snapped to the nearest integer pixels during the rasterization process, while the axis is kept where it is supposed to be. Then, when the output is produced, the colorbar falls within borders of fixed pixels of the image, despite the fact that the image is, itself, vectorized. Thus, there are two strategies that can be employed to avoid this mishap.
Use a finer DPI
The conversion from vectorized coordinates to rasterized coordinates takes place assuming a given DPI on the image. By default, this is set to be 72. However, by using more DPI, the overall shift induced by the rasterization process will be smaller, as the closest pixel the colorbar will snap to will be much nearer. Here, we change the output to have fig.savefig(plot,dpi=4000), and the problem goes away:
Note, however, that on my machine, the output size changed from 62 KB to 78 KB due to this change (although the DPI adjustment was also, admittedly, extreme). If you are worried about file sizes, you should pick a lower DPI that fixes the problem.
Use a different colormap
This rasterization happens when more than 50 colors are in the colorbar. Thus, we can do a quick test, setting our colormap to Pastel1 via
im = ax2.pcolormesh(z,cmap='Pastel1'). Here, the colorbar / axis mismatch is mitigated.
As a fallback, adopting a colorbar with fewer than 50 colors should mitigate this problem.
Rasterize the Axis
For completeness, there is also a third option. If you rasterize the colorbar axis, both the axis boundaries and the colormap will be rasterized, and you'll lose the offset. This will also rasterize your labels, and the axis will shift as one, breaking alignment with the nearby axis. For this, you just need to include cbar_axis.set_rasterized(True).
First, a way to overlay a contour and a pcolormesh and create a colorbar would be the following
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import numpy as np
x = np.linspace(0,5)
z = (np.outer(np.sin(x), np.cos(x))+1)*0.5
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(111)
im = ax.pcolormesh(z)
c = ax.contour(z, linewidths=7)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", "5%", pad="3%")
colorbar = fig.colorbar(im, cax=cax, ticks = [0.1, .5, .9])
plt.show()
Now to the problem from the question. It is of course possible to create the axes to put the colorbar in manually. Replacing the colorbar creation with the code from the question still produces a nice image.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0,5)
z = (np.outer(np.sin(x), np.cos(x))+1)*0.5
fig = plt.figure(figsize=(4, 4))
ax = fig.add_subplot(111)
plt.subplots_adjust(right=0.8)
im = ax.pcolormesh(z)
c = ax.contour(z, linewidths=7)
ax2pos = ax.get_position()
cbar_axis = fig.add_axes([ax2pos.x1+0.05,ax2pos.y0, .05, ax2pos.height])
colorbar = fig.colorbar(im, ax = ax,
cax = cbar_axis, ticks = [0.1, .5, .9])
colorbar.outline.set_visible(True)
plt.show()
Conclusion so far: The issue is not reproducible, at least not without a Minimal, Complete, and Verifiable example.
I'm uncertain about the reasons for the behaviour in the example from the question. However, it seems that it can be overcome by rounding the figure size to 3 significant digits
matplotlib.rcParams['figure.figsize'] = (5.25, np.round(figheight,3))

Reducing the distance between two boxplots

I'm drawing the bloxplot shown below using python and matplotlib. Is there any way I can reduce the distance between the two boxplots on the X axis?
This is the code that I'm using to get the figure above:
import matplotlib.pyplot as plt
from matplotlib import rcParams
rcParams['ytick.direction'] = 'out'
rcParams['xtick.direction'] = 'out'
fig = plt.figure()
xlabels = ["CG", "EG"]
ax = fig.add_subplot(111)
ax.boxplot([values_cg, values_eg])
ax.set_xticks(np.arange(len(xlabels))+1)
ax.set_xticklabels(xlabels, rotation=45, ha='right')
fig.subplots_adjust(bottom=0.3)
ylabels = yticks = np.linspace(0, 20, 5)
ax.set_yticks(yticks)
ax.set_yticklabels(ylabels)
ax.tick_params(axis='x', pad=10)
ax.tick_params(axis='y', pad=10)
plt.savefig(os.path.join(output_dir, "output.pdf"))
And this is an example closer to what I'd like to get visually (although I wouldn't mind if the boxplots were even a bit closer to each other):
You can either change the aspect ratio of plot or use the widths kwarg (doc) as such:
ax.boxplot([values_cg, values_eg], widths=1)
to make the boxes wider.
Try changing the aspect ratio using
ax.set_aspect(1.5) # or some other float
The larger then number, the narrower (and taller) the plot should be:
a circle will be stretched such that the height is num times the width. aspect=1 is the same as aspect=’equal’.
http://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes.set_aspect
When your code writes:
ax.set_xticks(np.arange(len(xlabels))+1)
You're putting the first box plot on 0 and the second one on 1 (event though you change the tick labels afterwards), just like in the second, "wanted" example you gave they are set on 1,2,3.
So i think an alternative solution would be to play with the xticks position and the xlim of the plot.
for example using
ax.set_xlim(-1.5,2.5)
would place them closer.
positions : array-like, optional
Sets the positions of the boxes. The ticks and limits are automatically set to match the positions. Defaults to range(1, N+1) where N is the number of boxes to be drawn.
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.boxplot.html
This should do the job!
As #Stevie mentioned, you can use the positions kwarg (doc) to manually set the x-coordinates of the boxes:
ax.boxplot([values_cg, values_eg], positions=[1, 1.3])