Constrain axis dimensions to those of another axis - python

I've got code that produces a square image with smaller plots to the left and below the main image plot by using GridSpec with width_ratios and height_ratios:
import matplotlib.pyplot as plt
import numpy as np
# Some fake data.
imdata = np.random.random((100, 100))
extradata1 = np.max(imdata, axis=1)
extradata2 = np.max(imdata, axis=0)
fig = plt.figure(constrained_layout=True)
spec = fig.add_gridspec(ncols=2, nrows=2, width_ratios=(1, 8), height_ratios=(8, 1))
# Main image plot.
ax1 = fig.add_subplot(spec[:-1, 1:], aspect='equal')
ax1.imshow(imdata, cmap='viridis')
# Vertical (left) plot.
ax2 = fig.add_subplot(spec[:-1, 0], sharey=ax1)
ax2.plot(extradata1, range(imdata.shape[0]))
# Horizontal (bottom) plot.
ax3 = fig.add_subplot(spec[-1, 1:], sharex=ax1)
ax3.plot(range(imdata.shape[1]), extradata2)
plt.show()
I'd like the height of the left plot and the width of the bottom plot to be equal to the height and width of the main image, respectively. Currently as you can see the horizontal plot is wider than the image's horizontal size, and they also scale differently as the figure is scaled. Is it possible to constrain axis dimensions to those of other axes?

Calling imshow() with aspect='auto' should fix your problem:
ax1.imshow(imdata, cmap='viridis',aspect='auto')
For some more explanation on this, please look here:
Imshow: extent and aspect
import matplotlib.pyplot as plt
import numpy as np
# Some fake data.
imdata = np.random.random((100, 100))
extradata1 = np.max(imdata, axis=1)
extradata2 = np.max(imdata, axis=0)
fig = plt.figure(constrained_layout=True)
spec = fig.add_gridspec(ncols=2, nrows=2, width_ratios=(1, 8), height_ratios=(8, 1))
# Main image plot.
ax1 = fig.add_subplot(spec[:-1, 1:])
ax1.imshow(imdata, cmap='viridis',aspect='auto')
# Vertical (left) plot.
ax2 = fig.add_subplot(spec[:-1, 0], sharey=ax1)
ax2.plot(extradata1, range(imdata.shape[0]))
# Horizontal (bottom) plot.
ax3 = fig.add_subplot(spec[-1, 1:], sharex=ax1)
ax3.plot(range(imdata.shape[1]), extradata2)
Result:

Fourier's answer worked nicely, but I also found that I could get the desired behaviour by changing constrained_layout=True to constrained_layout=False in the plt.figure call.

Using aspect aspect="auto" works, it has the disadvantage of giving you non-square pixels.
For this kind of tasks, I found that the axes_grid toolkit is pretty useful
from mpl_toolkits.axes_grid1 import make_axes_locatable
# Some fake data.
imdata = np.random.random((100, 100))
extradata1 = np.max(imdata, axis=1)
extradata2 = np.max(imdata, axis=0)
fig, main_ax = plt.subplots()
divider = make_axes_locatable(main_ax)
bottom_ax = divider.append_axes("bottom", 1.2, pad=0.1, sharex=main_ax)
left_ax = divider.append_axes("left", 1.2, pad=0.1, sharey=main_ax)
bottom_ax.xaxis.set_tick_params(labelbottom=False)
left_ax.yaxis.set_tick_params(labelleft=False)
main_ax.imshow(imdata, cmap='viridis')
left_ax.plot(extradata1, range(imdata.shape[0]))
bottom_ax.plot(range(imdata.shape[1]), extradata2)
plt.show()

Related

plotting a grid of png with matplotlib

I have found multiple similar questions with this subject but so far I couldn't adapt any solution to my needs, so I'm sorry for reposting.
I'm trying to plot a grid of png images using matplotlib, the closest I've got to what I want is using the code below, which can be found here https://matplotlib.org/stable/gallery/axes_grid1/simple_axesgrid.html .
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import ImageGrid
import numpy as np
im1 = np.arange(100).reshape((10, 10))
im2 = im1.T
im3 = np.flipud(im1)
im4 = np.fliplr(im2)
fig = plt.figure(figsize=(4., 4.))
grid = ImageGrid(fig, 111, # similar to subplot(111)
nrows_ncols=(2, 2), # creates 2x2 grid of axes
axes_pad=0.1, # pad between axes in inch.
)
for ax, im in zip(grid, [im1, im2, im3, im4]):
# Iterating over the grid returns the Axes.
ax.imshow(im)
plt.show()
My question is, how do I get rid of the x and y ticks/labels and also give each image a title?
Again, I'm sorry for repeating the question.
This code
import matplotlib.pyplot as plt
image = plt.imread("sample.png")
fig, axes = plt.subplots(2, 3)
for row in [0, 1]:
for column in [0, 1, 2]:
ax = axes[row, column]
ax.set_title(f"Image ({row}, {column})")
ax.axis('off')
ax.imshow(image)
plt.show()
is going to produce

How to make the size of subplot equally?

I am using matplotlib and GridSpec to plot 9 images in 3x3 subplots.
fig = plt.figure(figsize=(30,40))
fig.patch.set_facecolor('white')
gs1 = gridspec.GridSpec(3,3)
gs1.update(wspace=0.05, hspace=0.05)
ax1 = plt.subplot(gs1[0])
ax2 = plt.subplot(gs1[1])
ax3 = plt.subplot(gs1[2])
ax4 = plt.subplot(gs1[3])
ax5 = plt.subplot(gs1[4])
ax6 = plt.subplot(gs1[5])
ax7 = plt.subplot(gs1[6])
ax8 = plt.subplot(gs1[7])
ax9 = plt.subplot(gs1[8])
ax1.imshow(img1,cmap='gray')
ax2.imshow(img2,cmap='gray')
...
ax9.imshow(img9,cmap='gray')
However, the images have a different size from each row. For example, the first-row images size is 256x256, the images in the second row have a size of 200x200 and the third row has a size of 128x128
I want to plot the images in the subplot with same size. How should I use it in python? Thanks
This is an example of 4x3 subplot
Don't use matplotlib.gridspec, but use figure.add_subplot as demonstrated with the runnable code below. However, when doing some plotting, you need to set_autoscale_on(False) to suppress its behavior of size adjusting.
import numpy as np
import matplotlib.pyplot as plt
# a function that creates image array for `imshow()`
def make_img(h):
return np.random.randint(16, size=(h,h))
fig = plt.figure(figsize=(8, 12))
columns = 3
rows = 4
axs = []
for i in range(columns*rows):
axs.append( fig.add_subplot(rows, columns, i+1) )
# axs[-1] is the new axes, write its title as `axs[number]`
axs[-1].set_title("axs[%d]" % (i))
# plot raster image on this axes
plt.imshow(make_img(i+1), cmap='viridis', alpha=(i+1.)/(rows*columns))
# maniputate axs[-1] here, plot something on it
axs[-1].set_autoscale_on(False) # suppress auto sizing
axs[-1].plot(np.random.randint(2*(i+1), size=(i+1)), color="red", linewidth=2.5)
fig.subplots_adjust(wspace=0.3, hspace=0.4)
plt.show()
The resulting plot:
I suppose you want to show the images in different sizes, such that all pixels of the different images are equally sized.
This is in general hard, but for the case where all images in a row (or column) of the subplot grid are of the same size, it becomes easy. The idea can be to use the gridspec's height_ratios (or width_ratios in case of columns) argument and set it to the image's pixel height (width).
import matplotlib.pyplot as plt
import numpy as np
images = [np.random.rand(r,r) for r in [25,20,12] for _ in range(3)]
r = [im.shape[0] for im in images[::3]]
fig, axes = plt.subplots(3,3, gridspec_kw=dict(height_ratios=r, hspace=0.3))
for ax, im in zip(axes.flat, images):
ax.imshow(im)
plt.show()

Matplotlib colorbar with consistent size for multiple subplots

I am trying to create a figure with several subplots that have a common colorbar. The subplots have to have an equal aspect ratio and the colorbar has to have the same height as the subplots. However, I don't manage to get a narrow colorbar with the same height as the other subplots.
I am using this recipe to generate a colorbar with a range suitable for all subplots; hence this issue is not addressed in the MWE.
When using the axes divider recipe to attach the colorbar, the height of the subplot changes due to the aspect ratio.
Here's the MWE
from matplotlib import pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import itertools as it
import numpy as np
mean = [0, 0]
cov = [[1, 0.5],
[0.5, 4]]
n_samples = 10000
hrange = [[-5,5],[-5,5]]
bins = 20
# RANDOM DATA
Z_random = np.random.multivariate_normal(mean, cov, size=n_samples)
Z, xedges, yedges = np.histogram2d(Z_random[:,0], Z_random[:,1], bins=bins, range=hrange, normed=True)
X, Y = np.meshgrid(xedges, yedges)
# PLOT PCOLORMESHS
fig, axes = plt.subplots(2,3, subplot_kw=dict(aspect="equal"))
axes = dict(enumerate(fig.get_axes(),1))
for i,ax in axes.items():
if i==6:
break
pcm = ax.pcolormesh(X,Y,Z)
# PLOT COLORBAR
divider = make_axes_locatable(axes[6])
cax = divider.append_axes("left", size="15%", pad=0.0)
fig.colorbar(pcm, cax=cax, label=r"Colorbar label")
I can plot the colorbar over the complete subplot, in which case the height is correct, but it's much to wide to be appealing.
Does anybody have a "robust" solution, i.e. without manually fiddling around with the dimension of the subplots holding the colorbar?
Thanks in advance!
EDIT: Increased width of colorbar to emphasize that it becomes smaller in height.
If the only aim is to get the height of the colorbar correctly aligned with its horizontal neighbor, the last solution from this answer would help.
If however you also want the colorbar to be left-aligned with the plot on top of it, the solution is probably more complicated.
You may use a callback to set the position of the colorbar explicitely as follows:
from matplotlib import pyplot as plt
from matplotlib.transforms import Bbox
import numpy as np
mean = [0, 0]
cov = [[1, 0.5],
[0.5, 4]]
n_samples = 10000
hrange = [[-5,5],[-5,5]]
bins = 20
# RANDOM DATA
Z_random = np.random.multivariate_normal(mean, cov, size=n_samples)
Z, xedges, yedges = np.histogram2d(Z_random[:,0], Z_random[:,1], bins=bins, range=hrange, normed=True)
X, Y = np.meshgrid(xedges, yedges)
# PLOT PCOLORMESHS
fig, axes = plt.subplots(2,3, subplot_kw=dict(aspect="equal"))
for i,ax in enumerate(axes.flat):
if i==5:
break
pcm = ax.pcolormesh(X,Y,Z)
# PLOT COLORBAR
cax = fig.add_axes([0.6,0.01,0.1,0.4])
fig.colorbar(pcm, cax=cax, label=r"Colorbar label")
def align_cbar(cax, hax, vax):
hpos = hax.get_position()
vpos = vax.get_position()
bb = Bbox.from_extents(vpos.x0, hpos.y0, vpos.x0+vpos.width*.05,hpos.y1)
if cax.get_position() != bb:
cax.set_position(bb)
fig.canvas.draw_idle()
align_cbar(cax, axes[1,1], axes[0,2])
fig.canvas.mpl_connect("draw_event", lambda x: align_cbar(cax, axes[1,1], axes[0,2]))
plt.show()

Make matplotlib.pyplot color bar span two rows alongside waveform and specgram

Using matplotlib.pyplot, I have two plots. One is a waveform of an audio file. The second is a spectrogram of the same audio. I want the wave form to be directly above the spectrogram (same x-axis, and aligned together). I also want a colorbar for the spectrogram.
Problem - when I put the colorbar in, it attaches to the spectrogram row and the waveform extends over the colorbar (i.e. is no longer time-aligned with the spectrogram and is wider than the spectrogram).
I am close to the solution, I think, but I just can't quite figure out what I'm doing wrong or what to change to get it working the way I want. Hope someone can point me in the right direction!
Using the following python code (I made the code as MWE as possible):
import matplotlib
matplotlib.use("TkAgg")
from scipy.io import wavfile
from matplotlib import mlab
from matplotlib import pyplot as plt
import numpy as np
from numpy.lib import stride_tricks
samplerate, data = wavfile.read('FILENAME.wav')
times = np.arange(len(data))/float(samplerate)
plt.close("all")
####
#Waveform
####
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(13.6, 7.68))
plt.subplot(211)
plt.plot(times, data, color='k')
plt.xlabel('time (s)')
plt.xlim(times[0], times[-1])
max_amp = max(abs(np.amin(data)), abs(np.amax(data)))
min_amp = (max_amp * -1) - abs(np.amin(data) - np.amax(data))/50
max_amp = max_amp + abs(np.amin(data) - np.amax(data))/50
plt.ylim(min_amp, max_amp)
ax = plt.gca()
ax.set_yticks(np.array([min_amp, min_amp/2, 0, max_amp/2, max_amp]))
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_tick_params(pad=115)
####
#Spectrogram
####
Fs = 5000*2.#10000.
NFFT = min(512, len(data))
noverlap = NFFT / 2
pad_to = NFFT * 16
dynamicRange = 27.5
vmin = 20*np.log10(np.max(data)) - dynamicRange
cmap = plt.get_cmap('inferno')
plt.subplot(212)
Pxx, freqs, times, cax = plt.specgram(data, NFFT=NFFT, Fs=samplerate, noverlap=noverlap, mode='magnitude', scale='dB', vmin=vmin, pad_to=pad_to, cmap=cmap)
axes_spec = plt.gca()
axes_spec.set_xlim([0., max(times)])
axes_spec.set_ylim([0, 5000])
plt.xlabel("Time (s)")
plt.ylabel("Frequency (hz)")
plt.colorbar(cax, label='(dB)').ax.yaxis.set_label_position('left')
plt.tight_layout()
plt.show()
I can get the following plot:
Making these slight modifications below, I can get the plot to look almost how I want. The problem is, it creates a blank figure next to the colorbar. This version, minus the blank figure, is what I am trying to create.
#Replace this for waveform
plt.subplot(221)
#Replace this for spectrogram
plt.subplot(223)
#Add this before colorbar
plt.subplot(122)
New version of plot:
EDIT: There is another possibility that I am also OK with (or perhaps both, for good measure!)
Here is an example of colorbar based on one of the answers in matplotlib-2-subplots-1-colorbar. The parameter pad in fig.colorbar is used to specify the space between the plots and the colorbar, and aspect is used to specify the aspect ratio between the height and width of the colorbar. Specgram outputs the image as the 4th output parameter, so I'm using that for the colorbar.
fig,axs = matplotlib.pyplot.subplots(ncols=1, nrows=2 )
N=1000; fs=10e3
x = np.sin(np.arange(N))+np.random.random(N)
spectrum, freqs, t, im = axs[1].specgram(x,Fs=fs,
cmap=matplotlib.cm.inferno,noverlap=255)
axs[0].plot(np.arange(0,N)/fs,x,'-');
axs[0].set_xlim(t[0],t[-1]);axs[1].set_xlim(t[0],t[-1])
axcb = fig.colorbar(im, ax=axs.ravel().tolist(), pad=0.04, aspect = 30)
It is important to notice that when fig.colorbar function is called using the ax parameter, the original plots will be resized to make room for the colorbar. If it is only applied to one of the plots, only that axis will be resized. Se below:
fig,axs = matplotlib.pyplot.subplots(ncols=1, nrows=2 )
N=1000; fs=10e3
x = np.sin(np.arange(N))+np.random.random(N)
spectrum, freqs, t, im = axs[1].specgram(x,Fs=fs,
cmap=matplotlib.cm.inferno,noverlap=255)
axs[0].plot(np.arange(0,N)/fs,x,'-')
axs[0].set_xlim(t[0],t[-1]);axs[1].set_xlim(t[0],t[-1])
axcb = fig.colorbar(im, ax=axs[1], pad=0.04, aspect = 30)
Below it is shown a way of controlling the resizing of your original axes in order to make room for a colorbar using fig.colorbar with the cax parameter that will not resize further your original plots. This approach requires to manually make some room for your colorbar specifying the right parameter inside the function fig.subplots_adjust :
fig,axs = matplotlib.pyplot.subplots(ncols=1, nrows=2 )
N=1000; fs=10e3
x = np.sin(np.arange(N))+np.random.random(N)
spectrum, freqs, t, im = axs[1].specgram(x,Fs=fs,
cmap=matplotlib.cm.inferno,noverlap=255)
axs[0].plot(np.arange(0,N)/fs,x,'-')
axs[0].set_xlim(t[0],t[-1]);axs[1].set_xlim(t[0],t[-1])
fig.subplots_adjust(right=0.85) # making some room for cbar
# getting the lower left (x0,y0) and upper right (x1,y1) corners:
[[x10,y10],[x11,y11]] = axs[1].get_position().get_points()
pad = 0.01; width = 0.02
cbar_ax = fig.add_axes([x11+pad, y10, width, y11-y10])
axcb = fig.colorbar(im, cax=cbar_ax)
And doing the same to span two rows by reading coordinates of the original two plots:
fig,axs = matplotlib.pyplot.subplots(ncols=1, nrows=2 )
N=1000; fs=10e3
x = np.sin(np.arange(N))+np.random.random(N)
spectrum, freqs, t, im = axs[1].specgram(x,Fs=fs,
cmap=matplotlib.cm.inferno,noverlap=255)
axs[0].plot(np.arange(0,N)/fs,x,'-')
axs[0].set_xlim(t[0],t[-1]);axs[1].set_xlim(t[0],t[-1])
fig.subplots_adjust(right=0.85) # making some room for cbar
# getting the lower left (x0,y0) and upper right (x1,y1) corners:
[[x00,y00],[x01,y01]] = axs[0].get_position().get_points()
[[x10,y10],[x11,y11]] = axs[1].get_position().get_points()
pad = 0.01; width = 0.02
cbar_ax = fig.add_axes([x11+pad, y10, width, y01-y10])
axcb = fig.colorbar(im, cax=cbar_ax)
The best solution I came up with is subplot2grid() function. This requies the use of subplots, which I was not using originally. Following this method, I needed to change everything from using plt (matplotlib.pyplot) to using the axes for the given plot for each .plot() or .specgram() invocation. The relevant changes are included here:
#No rows or columns need to be specified, because this is handled within a the `subplot2grid()` details
fig, axes = plt.subplots(figsize=(13.6, 7.68))
#Setup for waveform
ax1 = plt.subplot2grid((2, 60), (0, 0), rowspan=1, colspan=56)
####WAVEFORM PLOTTING
#Setup for spectrogram
ax2 = plt.subplot2grid((2, 60), (1, 0), rowspan=1, colspan=56)
####SPECTROGRAM PLOTTING
#Setup for colorbar
ax3 = plt.subplot2grid((2, 60), (0, 59), rowspan=1, colspan=1)
cbar = plt.colorbar(cax, cax=ax3, ax=ax2)
And a MWE bringing it all together:
import matplotlib as mpl
mpl.use("TkAgg")
from scipy.io import wavfile
from matplotlib import mlab
from matplotlib import pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
from numpy.lib import stride_tricks
samplerate, data = wavfile.read('FILENAME.wav')
times = np.arange(len(data))/float(samplerate)
plt.close("all")
fig, axes = plt.subplots(figsize=(13.6, 7.68))#nrows=2, ncols=2,
gs = gridspec.GridSpec(2, 60)
####
#Waveform
####
ax1 = plt.subplot2grid((2, 60), (0, 0), rowspan=1, colspan=56)
ax1.plot(times, data, color='k')
ax1.xaxis.set_ticks_position('none')
ax1.yaxis.set_ticks_position('none')
####
#Spectrogram
####
maxFrequency = 5000
Fs = maxFrequency*2.#10000.
NFFT = min(512, len(data))
noverlap = NFFT / 2
pad_to = NFFT * 16
dynamicRange = 27.5
vmin = 20*np.log10(np.max(data)) - dynamicRange
cmap = plt.get_cmap('inferno')
ax2 = plt.subplot2grid((2, 60), (1, 0), rowspan=1, colspan=56)
Pxx, freqs, times, cax = ax2.specgram(data, NFFT=NFFT, Fs=samplerate, noverlap=noverlap, mode='magnitude', scale='dB', vmin=vmin, pad_to=pad_to, cmap=cmap)
ax2.set_ylim([0, maxFrequency])
ax2.xaxis.set_ticks_position('none')
ax2.yaxis.set_ticks_position('none')
####
#Colorbar (for spectrogram)
####
ax3 = plt.subplot2grid((2, 60), (1, 59), rowspan=1, colspan=1)
cbar = plt.colorbar(cax, cax=ax3, ax=ax2)
cbar.ax.yaxis.set_tick_params(pad=3, left='off', right='off', labelleft='on', labelright='off')
plt.show()
Here's an example of the output from this MWE:
Best part! You need only change the 0 to 1 and the rowspan to be 1 in this line (i.e. :)
ax3 = plt.subplot2grid((2, 60), (1, 59), rowspan=1, colspan=1)
to make the colorbar span only the height of the spectrogram. Meaning that changing between the two options is incredibly simple. Here's an example of the output from this change:
EDIT: GridSpec actually was unused, and so I edited it out. The only relevant details that I needed involved calling subplot2grid() to set up the subplots.

Colorbar sticking to axes

I've got a small problem with the positioning of a colorbar using matplotlib. I'm plotting several subplots and one of them is an image. I want this image to have a colorbar but I want it to be "stuck" to the figure, so that there is no space between the two axes (the one from the figure and the one from the colorbar). Even if the figure is resized, the colorbar should always stick to the image axes.
PS - I don't mind if ax3 (the axes of my image) is deformed.
Here's what I've got for the moment:
# Imports
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from scipy import *
# Generating data
data = (rand(2048,2048), rand(2048,2048)+1000)
colorplot = "blue"
metadata = ("Test1", "Test2", "spectral")
# Generating figure and subplots
fig = plt.figure()
fig.subplots_adjust(right = 0.8)
gs1 = gridspec.GridSpec(3, 5)
gs1.update(left=0.05,\
right=0.95,\
top=0.95,\
bottom=0.05,\
wspace=0.2,\
hspace=0.05)
ax1 = fig.add_subplot(gs1[0,0])
ax2 = fig.add_subplot(gs1[0,1])
ax3 = fig.add_subplot(gs1[1:3,0:2])
ax4 = fig.add_subplot(gs1[:,2:])
list_axes = [ax1, ax2, ax3, ax4]
for i in list_axes:
i.autoscale_view(tight=False, scalex=False, scaley=True)
# Misc computation
array = data[1]-data[0]
mean_value = np.mean(array)
std_value = np.std(array)
nb_sigma = 5
ax1.imshow(data[0], interpolation = "nearest", cmap = metadata[2])
ax2.imshow(data[1], interpolation = "nearest", cmap = metadata[2])
im = ax3.imshow(array, vmin = np.min(array[array>mean_value-nb_sigma*std_value]), vmax = np.max(array[array<mean_value+nb_sigma*std_value]), interpolation = "nearest", cmap = metadata[2])
ax3.set_adjustable('box-forced')
# Creating axes for the colorbar
axes_cb = fig.add_axes([ax3.get_position().bounds[0],ax3.get_position().bounds[1], ax3.get_position().bounds[2], 0.05])
fig.colorbar(im, cax = axes_cb, orientation = 'horizontal')
axes_cb.yaxis.tick_left()
n, bins, patches = ax4.hist(array.flatten(), color = colorplot, bins = 50, normed = True)
plt.show()
Thank you!

Categories