Reducing vertical space between subplots in matplotlib - python

images = wcs_request.get_data() # get image data
fig, axs = plt.subplots((len(images) + (6 - 1)) // 6, 6, figsize=(20, 20),
gridspec_kw={'hspace': 0.0, 'wspace': 0.0})
total = ((len(images) + (6 - 1)) // 6) * 6
for idx, (image, time) in enumerate(zip(images, wcs_request.get_dates())):
# Plot bbox
axs.flat[idx].imshow(image)
# Set title
axs.flat[idx].set_title(time.date().strftime("%d %B %Y"), fontsize=10, fontweight='bold')
# delete plots which have no data
for idx in range(len(images), total):
fig.delaxes(axs.flat[idx])
plt.suptitle(id, fontsize=12, fontweight='bold')
# fig.tight_layout(pad=0, h_pad=.1, w_pad=.1)
# fig.subplots_adjust(wspace=0, hspace=0)
plt.savefig(dir_out / f'{id}_map.png', dpi=300)
plt.close()
When I run the code above, I get a subplot with much larger vertical blank space than I want. How can I fix it? I already set wspace and hspace to 0.0

Well, there are many ways to generate a "nice" array of subplots; but assuming that your goal is to, e.g. create two rows of images where len(images)=10:
import matplotlib.pyplot as plt
images=range(10)
## assuming you want e.g. axes on your first row:
ncols = 6
# figure out how many plots will fall into the last row using modulo
ncols_last = (len(images) % ncols)
# and (if mod > 0 !) add one to the floor operation here:
nrows = (len(images) // ncols ) + (ncols_last > 0)
fig = plt.figure()
axes={}
for i in range(len(images)):
# note that for some reason, add_subplot() counts from 1, hence we use i+1 here
axes[i] = fig.add_subplot(nrows,ncols,i+1)
# add some content
for i,ax in axes.items():
ax.text(0,0,i)
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
plt.show()
Which should give you 6 plots on the first row and 4 on the second. You should be able to add your plot content like this:
for idx, (image, time) in enumerate(zip(images, wcs_request.get_dates())):
# Plot bbox
axes[idx].imshow(image)
# Set title
axes[idx].set_title(time.date().strftime("%d %B %Y"), fontsize=10, fontweight='bold')
Or alternatively, using gridspec in order to get access to further layout options:
import matplotlib.pyplot as plt
from matplotlib import gridspec
images=range(10)
ncols = 6
ncols_last = (len(images) % ncols)
nrows = (len(images) // ncols ) + (ncols_last > 0)
fig = plt.figure()
axes = {}
gs = gridspec.GridSpec(nrows, ncols,
left=0.1,right=.9,
bottom=0.1,top=.9,
wspace=0.25,hspace=0.3,
)
for i,(r,c) in enumerate([(r,c) for r in range(nrows) for c in range(ncols)]):
if i < len(images):
print(f"axes[{i}]: relates to the gridspec at index ({r},{c})")
axes[i] = fig.add_subplot(gs[r,c])
for i,ax in axes.items():
ax.text(0,0,i)
ax.set_xlim(-1,1)
ax.set_ylim(-1,1)
plt.show()

You may want to check out subplots_adjust, which let you specify:
The height of the padding between subplots, as a fraction of the average axes height.
fig, axs = plt.subplots(2,1)
fig.subplots_adjust(hspace=0.0)
So with hspace=0 there is no spacing at all:

Related

subplots, how to set the xlabel and xlim, but removing axis

I'd like to plot EEG data and get this result:
But I am stuck on how to display the x axis label and its xlim.
After reading other questions, which use set_visible(False), I cannot resolve my issue.
I write my code in order to be reproducible:
sfreq = 256
raw_data = np.random.rand(14, 1000 * sfreq)
duration = 10 # duration of the signal
start = 200 * sfreq
final = start + int(sfreq * duration)
channels = list(np.arange(1, len(channels) + 1 ))
fig, ax = plt.subplots(len(channels), 1, sharex=True, figsize=(10, 10))
for idx, node in enumerate(channels):
data = raw_data[idx, start:final]
times = np.arange(1, data.size + 1) / sfreq
ax[idx].plot(times, data, lw=1., ls='-', c='k')
ax[idx].axis('off') # to remove bounding subplot
ax[idx].set_yticks([]) # to remove values from y axis
ax[idx].text(-1, 0, node, fontsize=12) # write text
# plt.axis(True)
# plt.axes().get_xaxis().set_visible(True)
# plt.xlim([200, 220])
plt.xlabel('Time (seconds)', fontsize=12)
plt.tight_layout()
plt.show()
This is my result:
But I'd like this:
Here are some possible changes to the plot:
make the code more python by using zip instead of an index in the for loop
change the visibility of the "spines" (the lines surrounding the subplot) instead of use axis('off')
remove the padding (margins)
use the axes transform to position the text of the y-axis
...
import matplotlib.pyplot as plt
import numpy as np
sfreq = 256
raw_data = np.random.rand(14, 1000 * sfreq)
duration = 10 # duration of the signal
start = 200 * sfreq
final = start + int(sfreq * duration)
channels = np.arange(len(raw_data)) + 1
fig, axs = plt.subplots(len(channels), 1, sharex=True, figsize=(10, 10))
for ax, node, data in zip(axs, channels, raw_data):
data = data[start:final]
times = np.arange(1, data.size + 1) / sfreq
ax.plot(times, data, lw=1., ls='-', c='k')
ax.set_yticks([]) # remove y ticks
for sp in ax.spines:
ax.spines[sp].set_visible(False) # hide the 4 lines surrounding the subplot
ax.text(-0.01, 0.5, node, fontsize=12, ha='right', va='center', transform=ax.transAxes) # write text
ax.margins(x=0) # avoid the empty space left and right
if ax != axs[-1]:
# ax.tick_params(axis='x', length=0) # hide the tick marks
ax.tick_params(bottom=False) # no tick marks at the bottom
axs[-1].set_xlabel('Time (seconds)', fontsize=12, labelpad=-10) # use negative padding to get closer to the xaxis
axs[-1].set_xticks([0, duration])
axs[-1].set_xticklabels([start // sfreq, final // sfreq])
axs[-1].spines['bottom'].set_bounds([0, duration]) # only draw the spine between the two ticks
axs[-1].spines['bottom'].set_visible(True)
axs[-1].spines['bottom'].set_linewidth(2)
plt.tight_layout()
plt.show()

How to arrange graphs and texts like titles and axis lables appealingly with matplotlib with multiple subplots?

I want to create a before-and-after plot of the three axis of my measurement system. This is close to what I want. However:
How do I have the "before" and "after" titles span subplot 1+2 and 4+5, respectively? (the tabs dont work as expected)
Like "before" and "after" should be above a column, i would like to have the "x-Axis", "y-Axis" etc infront of the row of graphs. How do I do that?
How do I join subplot 1+2 and 4+5 together, so that they touch? wspace=.0 doesnt work as expected.
How do I reduce the width in the middle, where subplot 3 would be, so that the other sides can take up more space?
How do I add some hspace between the fig.suptitle and the graphs?
How can I make my code more elegant? There is a lot of repetition.
from matplotlib.pyplot import figure
def plot_before_and_after(data_before, data_after, title):
shape = data_before.shape
sensor_num = shape[0]
n_start = 20 # number picked at random
N = 2 ** 12 # power of two is good
n_stop = n_start + N
p_stop = n_start + 40 # one periode #50Hz at the sampling rate
x_long = range(n_start, n_stop)
x_short = range(n_start, p_stop)
cmap = plt.get_cmap('jet_r')
axis_list = ['x', 'y', 'z']
fig = figure(num=None, figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
fig.suptitle(title + "\nbefore \t\t\t\t\tafter")
plots = []
for axis_cnt in range(0, 3):
ax0 = plt.subplot(3, 5, axis_cnt * 5 + 1,
title="before, {}-Axis".format(axis_list[axis_cnt]))
for sensor_cnt in range(0, sensor_num):
color = cmap(float(sensor_cnt) / sensor_num)
plt.plot(x_long, data_before[sensor_cnt, n_start:n_stop, axis_cnt], color=color,
label="sensor" + str(sensor_cnt))
ax1 = plt.subplot(3, 5, axis_cnt * 5 + 2,
title="before, {}-Axis".format(axis_list[axis_cnt]),
sharey=ax0)
for sensor_cnt in range(0, sensor_num):
color = cmap(float(sensor_cnt) / sensor_num)
plt.plot(x_short, data_before[sensor_cnt, n_start:p_stop, axis_cnt], color=color,
label="sensor" + str(sensor_cnt))
plt.setp(ax1.get_yticklabels(), visible=False)
ax3 = plt.subplot(3, 5, axis_cnt * 5 + 4,
title="after, {}-Axis".format(axis_list[axis_cnt]))
for sensor_cnt in range(0, sensor_num):
color = cmap(float(sensor_cnt) / sensor_num)
plt.plot(x_long, data_after[sensor_cnt, n_start:n_stop, axis_cnt], color=color,
label="sensor" + str(sensor_cnt))
ax4 = plt.subplot(3, 5, axis_cnt * 5 + 5,
title="after, {}-Axis".format(axis_list[axis_cnt]),sharey=ax3)
for sensor_cnt in range(0, sensor_num):
color = cmap(float(sensor_cnt) / sensor_num)
plt.plot(x_short, data_after[sensor_cnt, n_start:p_stop, axis_cnt], color=color,
label="sensor" + str(sensor_cnt))
plt.setp(ax4.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace=.0)
plt.legend()
plt.show()
Here's a preliminary answer that may help you. I used Matplotlib's GridSpec (see here for useful information) and the add_subplot method, both of which seem to be convenient in these cases. The gridspec is what allows us to create the two groups of subplots independently formatted; the add_subplot generates the individual axes.
Code
import matplotlib.pyplot as plt
nrow, ncol = 3, 2 # Number of rows and cols of gridspecs
lborder = [0.1, 0.6] # Left border coordinates of gridspecs
rborder = [0.45, 0.95] # Right border coordinates of gridspecs
tborder = 0.92 # Top border coordinate of gridspecs
gtitles = ['Before', 'After']
txt_axis = ['X-axis', 'Y-axis', 'Z-axis']
fig = plt.figure(figsize=(10, 10), dpi=80, facecolor='w', edgecolor='k')
for i in range(2):
gs = fig.add_gridspec(nrows=nrow, ncols=ncol, left=lborder[i],
right=rborder[i], top=tborder, hspace=0.45, wspace=0)
for j in range(nrow):
ax0 = fig.add_subplot(gs[j,0])
ax0.plot([1,2,3])
plt.text(-0.4, 0.5, txt_axis[j],
horizontalalignment='center',verticalalignment='center',
transform = ax0.transAxes, fontsize = 12)
if j == 0:
fig.text(1, 1.1, gtitles[i], fontsize=12, horizontalalignment =
'center', transform = ax0.transAxes)
for k in range(1, ncol):
ax1 = fig.add_subplot(gs[j,k], sharey = ax0)
plt.setp(ax1.get_yticklabels(), visible=False)
ax1.plot([1,2,3])
fig.suptitle('Figure title', fontsize = 14)
As for your questions:
I created the 'Before' and 'After' titles using text, based on this answer).
Same thing for the "-axis" text. Note that it will probably overlap with any axes label you write on the vertical axis. Also note that now we have to shift the left gridspec slightly to the right (via the leftargument of add_gridspec).
wspace can be introduced in add_gridspec too. I don't know why it doesn't work in your code.
For the space in between the 2 gridspecs, use the left and right arguments in the add_gridspec function.
The space between the main title and the subplots can be achieved via the top argument in add_gridspec.
Your inner loops seem very similar, perhaps you could define a function and save some lines of code. In my case, I tried to encase everything in a loop.
Hope it helps.

How to adjust space between every second row of subplots in matplotlib

I'm hoping to adjust the space between subplots horizontally. Specifically between every second row. I can adjust every row using fig.subplots_adjust(hspace=n). But is it possible to apply this to every 2nd row?
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize = (10,10))
plt.style.use('ggplot')
ax.grid(False)
ax1 = plt.subplot2grid((5,2), (0, 0))
ax2 = plt.subplot2grid((5,2), (0, 1))
ax3 = plt.subplot2grid((5,2), (1, 0))
ax4 = plt.subplot2grid((5,2), (1, 1))
ax5 = plt.subplot2grid((5,2), (2, 0))
ax6 = plt.subplot2grid((5,2), (2, 1))
ax7 = plt.subplot2grid((5,2), (3, 0))
ax8 = plt.subplot2grid((5,2), (3, 1))
fig.subplots_adjust(hspace=0.9)
Using the subplots below I'm hoping to add a space between rows 2 and 3 and keep the rest as is.
You may interlace two grids such that there is a larger spacing between every second subplot.
To illustrate the concept:
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
n = 3 # number of double-rows
m = 2 # number of columns
t = 0.9 # 1-t == top space
b = 0.1 # bottom space (both in figure coordinates)
msp = 0.1 # minor spacing
sp = 0.5 # major spacing
offs=(1+msp)*(t-b)/(2*n+n*msp+(n-1)*sp) # grid offset
hspace = sp+msp+1 #height space per grid
gso = GridSpec(n,m, bottom=b+offs, top=t, hspace=hspace)
gse = GridSpec(n,m, bottom=b, top=t-offs, hspace=hspace)
fig = plt.figure()
axes = []
for i in range(n*m):
axes.append(fig.add_subplot(gso[i]))
axes.append(fig.add_subplot(gse[i]))
plt.show()
Without going to tedious low-level hacks like adjusting the position of the axes manually, I would suggest using a grid but just leaving some of the rows blank.
I tried this:
import matplotlib.pyplot as plt
plt.figure(figsize=(10., 10.))
num_rows = 6
num_cols = 2
row_height = 3
space_height = 2
num_sep_rows = lambda x: int((x-1)/2)
grid = (row_height*num_rows + space_height*num_sep_rows(num_rows), num_cols)
ax_list = []
for ind_row in range(num_rows):
for ind_col in range(num_cols):
grid_row = row_height*ind_row + space_height*num_sep_rows(ind_row+1)
grid_col = ind_col
ax_list += [plt.subplot2grid(grid, (grid_row, grid_col), rowspan=row_height)]
plt.subplots_adjust(bottom=.05, top=.95, hspace=.1)
# plot stuff
ax_list[0].plot([0, 1])
ax_list[1].plot([1, 0])
# ...
ax_list[11].plot([0, 1, 4], c='C2')
which gives this result:
Note that you can change the number of rows; also, you can adjust the size of the blank space compared to the subplots by tweaking the row_height/space_height ratio (both must be integers).
Here's a solution with getting into tedious low-level hacks:
import matplotlib.pyplot as plt
def tight_pairs(n_cols, fig=None):
"""
Stitch vertical pairs together.
Input:
- n_cols: number of columns in the figure
- fig: figure to be modified. If None, the current figure is used.
Assumptions:
- fig.axes should be ordered top to bottom (ascending row number).
So make sure the subplots have been added in this order.
- The upper-half's first subplot (column 0) should always be present
Effect:
- The spacing between vertical pairs is reduced to zero by moving all lower-half subplots up.
Returns:
- Modified fig
"""
if fig is None:
fig = plt.gcf()
for ax in fig.axes:
if hasattr(ax, 'get_subplotspec'):
ss = ax.get_subplotspec()
row, col = ss.num1 // n_cols, ss.num1 % n_cols
if (row % 2 == 0) and (col == 0): # upper-half row (first subplot)
y0_upper = ss.get_position(fig).y0
elif (row % 2 == 1): # lower-half row (all subplots)
x0_low, _ , width_low, height_low = ss.get_position(fig).bounds
ax.set_position(pos=[x0_low, y0_upper - height_low, width_low, height_low])
return fig
Here's a test for above function:
def test_tight_pairs():
def make_template(title):
fig = plt.figure(figsize=(8, 6))
for i in range(12):
plt.subplot(6, 2, i+1)
plt.plot([0,1], [0,1][::-1 if i%2==1 else 1])
fig.suptitle(title)
return fig
make_template("The vertical spacing should have increased (disappeared) between (within) pairs.")
tight_pairs(2)
make_template("Default spacing.")
plt.show()
test_tight_pairs()
Extra notes:
This will also work if some subplot pairs in the grid are missing, e.g. for a "lower triangle" arrangement of subplots.
To keep some distance between the pairs, you can add some padding via
y0_upper - height_low - padding, or
y0_upper - height_low - p * height_low
The labels and ticks on the y axis might need some fixing if they overlap.

Loop to create subplot /Python

i have a little problem to create a subplot loop.
The following code show my result for one plot.... So it starts with a dayloop than with a hour loop (8 timesteps).
If i run the code i get a nice QUiver plot with the colorbar.
for dd in range(1,15):
day=str(dd)
readfile=fns[files_indizes[dd]]
if dd < 10:
nc_u_comp = NetCDFFile(ROOT+u_comp1+'0'+day+comp)
nc_v_comp = NetCDFFile(ROOT+v_comp1+'0'+day+comp)
else:
nc_u_comp = NetCDFFile(ROOT+u_comp1+day+comp)
nc_v_comp = NetCDFFile(ROOT+v_comp1+day+comp)
time = nc_u_comp.variables['time'][:]
index=readfile.find(comp)
index=index+len(comp)
date=readfile[index-14:index-6]
plt.clf()
for tt in range(0,len(time)):
if tt < 10:
h =str(0)+str(tt)
else:
h=str(tt)
varU=nc_u_comp.variables['u10'][tt,:,:]
varV=nc_v_comp.variables['v10'][tt,:,:]
lat = nc_u_comp.variables['latitude'][:]
lon = nc_u_comp.variables['longitude'][:]
plt.rcParams["figure.figsize"] = [10,10]
#plane projection of the world
#map with box size (defintion on the top)
box = sgeom.box(minx=llcrnrlon, maxx=urcrnrlon, miny=llcrnrlat, maxy=urcrnrlat)
x0, y0, x1, y1 = box.bounds
#Map plot. The middel of the map is central_longitude
#proj = ccrs.PlateCarree(central_longitude=0)
proj=ccrs.PlateCarree()
#Change middelpoint of the map
box_proj = ccrs.PlateCarree(central_longitude=0)
ax2 = plt.axes(projection=proj)
ax2.set_extent([x0, x1, y0, y1], box_proj)
ax2.add_feature(cartopy.feature.BORDERS, linestyle='-', alpha=.5)
ax2.coastlines(resolution='50m')
#Definition of the scale_bar
gl = ax2.gridlines(ccrs.PlateCarree(), \
linestyle='--', alpha=1, linewidth=0.5, draw_labels=True)
gl.xlabels_top = False
gl.ylabels_right = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
magnitude = (varU ** 2 + varV ** 2) ** 0.5
strm =plt.streamplot(lon , lat , varU, varV, linewidth=2, density=2, color=magnitude)
cbar= plt.colorbar()
cbar.set_label('$m/s$')
name='Wind in 10 m '+ date + h+' UTC'
ax2.set_aspect('auto')
plt.title(name, y=1)
Now i want to create an 2x4 Subplot array with a colorbar allocate to the complete Subplot array.
I find some infromation in the internet, but it doesn't run with my code. Maybe someone can help me?
This shows how to plot an array of simple Cartopy maps in 4 rows 2 columns. Also shows how to plot a colorbar to accompany the maps array. Hope it helps.
import numpy as np
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import matplotlib as mpl
# create figure with figsize big enough to accomodate all maps, labels, etc.
fig = plt.figure(figsize=(8, 10), tight_layout=False)
# define plot array's arrangement
columns = 2
rows = 4
# set projection to use
projex = ccrs.PlateCarree()
# set the colormap and norm for
# the colorbar to use
cmap1 = mpl.cm.magma
norm1 = mpl.colors.Normalize(vmin=0, vmax=100)
def plotmymap(axs):
# your plot specs of each map should replace this
img = np.random.randint(100, size=(15, 30)) # 2d array of random values (1-100)
# render image on current axis
plims = plt.imshow(img, extent=[-180,180,-90,90], alpha=0.5, cmap=cmap1, norm=norm1)
axs.set_global()
axs.coastlines()
# add title to the map
axs.set_title("Map_"+str(i))
return plims # for use by colorbar
for i in range(1, columns*rows +1):
# add a subplot into the array of plots
ax = fig.add_subplot(rows, columns, i, projection=projex)
plims = plotmymap(ax) # a simple maps is created on subplot
# add a subplot for vertical colorbar
bottom, top = 0.1, 0.9
left, right = 0.1, 0.8
fig.subplots_adjust(top=top, bottom=bottom, left=left, right=right, hspace=0.15, wspace=0.25)
cbar_ax = fig.add_axes([0.85, bottom, 0.05, top-bottom])
fig.colorbar(plims, cax=cbar_ax) # plot colorbar
plt.show() # this plot all the maps
The resulting plots:

How to adjust table for a plot? More space for table and graph matplotlib python

I want to separate or increase the distance of my table and my graph so they don't layover. I thought of increasing the size to right and put the table there but I can't seem to make it work, and I can't find a way to offset the table by 1 line.
Graph
global dataread
global top4
global iV
top4mod = [] #holder for table, combines amplitude and frequency (bin*3.90Hz)
plt.plot(x1, fy1, '-') #plot x-y
plt.axis([0, 500, 0, 1.2]) #range for x-y plot
plt.xlabel('Hz')
columns = ('Frequency','Hz')
rows = ['# %d' % p for p in (1,2,3,4)] #top4
colors = 'C0'
print(len(rows))
print(len(str(top4)))
print(top4)
iV=[d*bins for d in iV] # convert bins into frequency
i=0;
FirstCol = [4, 3, 2, 1]
while i < 4:
Table.append([iV[i]] + [top4[i]])#[FirstCol[i]]
i = i+1
cell_text = []
n_rows = len(Table)
index = np.arange(len(columns)) + 1 #0.3 orginal
bar_width = 0.4
y_offset = np.array([0.0] * len(columns))
for row in range(n_rows):
#plt.bar(index, Table[row], bar_width, bottom=y_offset, color='C0') #dont use this
y_offset = y_offset + Table[row]
cell_text.append(['%1.1f' % p for p in y_offset])
the_table = plt.table(cellText=Table,rowLabels=rows, colLabels=columns,loc='bottom')
#plt.figure(figsize=(7,8))
# Adjust layout to make room for the table:
plt.subplots_adjust(bottom=0.2) #left=0.2, bottom=0.2
plt.show() #display plot
Using bbox
You can set the position of the table using the bbox argument. It expects either a bbox instance or a 4-tuple of values (left, bottom, width, height), which are in axes coordinates. E.g.
plt.table(..., bbox=[0.0,-0.5,1,0.3])
produces a table that is as wide as the axes (left=0, width=1) but positionned below the axes (bottom=-0.5, height=0.3).
import numpy as np
import matplotlib.pyplot as plt
data = np.random.rand(4,2)
columns = ('Frequency','Hz')
rows = ['# %d' % p for p in (1,2,3,4)]
plt.plot(data[:,0], data[:,1], '-') #plot x-y
plt.axis([0, 1, 0, 1.2]) #range for x-y plot
plt.xlabel('Hz')
the_table = plt.table(cellText=data,rowLabels=rows, colLabels=columns,
loc='bottom', bbox=[0.0,-0.45,1,.28])
plt.subplots_adjust(bottom=0.3)
plt.show()
Create dedicated axes
You can also create an axes (tabax) to put the table into. You would then set the loc to "center", turn the axis spines off and only use a very small subplots_adjust bottom parameter.
import numpy as np
import matplotlib.pyplot as plt
data = np.random.rand(4,2)
columns = ('Frequency','Hz')
rows = ['# %d' % p for p in (1,2,3,4)]
fig, (ax, tabax) = plt.subplots(nrows=2)
ax.plot(data[:,0], data[:,1], '-') #plot x-y
ax.axis([0, 1, 0, 1.2]) #range for x-y plot
ax.set_xlabel('Hz')
tabax.axis("off")
the_table = tabax.table(cellText=data,rowLabels=rows, colLabels=columns,
loc='center')
plt.subplots_adjust(bottom=0.05)
plt.show()

Categories