python matplotlib's gridspec unable to reduce gap between subplots - python

I am using GridSpec to plot subplots within a subplot to show images.
In the example code below, I am creating a 1x2 subplot where each subplot axes contains 3x3 subplot (subplot within the first subplot).
3x3 subplot is basically showing an image cut into 9 square pieces arranged into 3x3 formation. I don't want any spacing between image pieces, so I set both wspace and hspace to 0. Weirdly enough, the resulting output subplots show vertical gap between rows.
I tried setting hspace to negative value to reduce vertical spacing between the rows, but it results in rows overlapping. Is there a more convenient way to achieve this?
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from PIL import Image
from sklearn.datasets import load_sample_image
flower = load_sample_image('flower.jpg')
img = Image.fromarray(flower)
img = img.crop((100, 100, 325, 325))
# Create tiles - cuts image to 3x3 square tiles
n_tiles = 9
tile_size = float(img.size[0]) / 3 # assumes square tile
tiles = [None] * n_tiles
for n in range(n_tiles):
row = n // 3
col = n % 3
# compute tile coordinates in term of the image (0,0) is top left corner of the image
left = col * tile_size
upper = row * tile_size
right = left + tile_size
lower = upper + tile_size
tile_coord = (int(left), int(upper), int(right), int(lower))
tile = img.crop(tile_coord)
tiles[n] = tile
# plot subplot of subplot using gridspec
fig = plt.figure(figsize=(7, 3))
outer = gridspec.GridSpec(1, 3, wspace=1)
# image shown as 3x3 grid of image tiles
inner = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=outer[0], wspace=0, hspace=0)
for j in range(len(tiles_tensor)):
ax1 = plt.Subplot(fig, inner[j], xticks=[], yticks=[])
ax1.imshow(tiles[j])
fig.add_subplot(ax1)
# image shown as 3x3 grid of image tiles
inner = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=outer[1], wspace=0, hspace=0)
for j in range(len(data)):
ax2 = plt.Subplot(fig, inner[j], xticks=[], yticks=[])
ax2.imshow(tiles[j])
fig.add_subplot(ax2)

The main problem is that imshow defaults to aspect='equal'. This forces the small tiles to be square. But the subplots aren't square, so 9 square tiles together can't nicely fill the subplot.
An easy solution is to turn off the square aspect ratio via imshow(..., aspect='auto'). To get the subplots more squarely, the top, bottom, left and right settings can be adapted.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from PIL import Image
from sklearn.datasets import load_sample_image
flower = load_sample_image('flower.jpg')
img = Image.fromarray(flower)
img = img.crop((100, 100, 325, 325))
# Create tiles - cuts image to 3x3 square tiles
n_tiles = 9
tile_size = float(img.size[0]) / 3 # assumes square tile
tiles = [None] * n_tiles
for n in range(n_tiles):
row = n // 3
col = n % 3
# compute tile coordinates in term of the image (0,0) is top left corner of the image
left = col * tile_size
upper = row * tile_size
right = left + tile_size
lower = upper + tile_size
tile_coord = (int(left), int(upper), int(right), int(lower))
tile = img.crop(tile_coord)
tiles[n] = tile
# plot subplot of subplot using gridspec
fig = plt.figure(figsize=(7, 3))
outer = gridspec.GridSpec(1, 2, wspace=1, left=0.1, right=0.9, top=0.9, bottom=0.1)
titles = [f'Subplot {j+1}' for j in range(outer.nrows * outer.ncols) ]
for j in range(len(titles)):
ax = plt.Subplot(fig, outer[j], xticks=[], yticks=[])
ax.axis('off')
ax.set_title(titles[j])
fig.add_subplot(ax)
# image shown as 3x3 grid of image tiles
inner = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=outer[0], wspace=0, hspace=0)
for j in range(len(tiles)):
ax1 = plt.Subplot(fig, inner[j], xticks=[], yticks=[])
ax1.imshow(tiles[j], aspect='auto')
fig.add_subplot(ax1)
# image shown as 3x3 grid of image tiles
inner = gridspec.GridSpecFromSubplotSpec(3, 3, subplot_spec=outer[1], wspace=0, hspace=0)
for j in range(len(tiles)):
ax2 = plt.Subplot(fig, inner[j], xticks=[], yticks=[])
ax2.imshow(tiles[j], aspect='auto')
fig.add_subplot(ax2)
fig.suptitle('Overall title')
plt.show()

Related

Draw a grid of cells using Matplotlib

I'm trying to draw a grid of cells using Matplotlib where each border (top, right, bottom, left) of a cell can have a different width (random number between 1 and 5). I should note also that the width and height of the inner area of a cell (white part) can vary between 15 and 20.
I want to know how to get the coordinates of each cell in order to avoid any extra space between the cells.
I tried several ideas however I did not get the right coordinates.
You could draw thin rectangles with a random thickness, in horizontal and vertical orientation to simulate the edges of the cells:
import matplotlib.pyplot as plt
import random
fig, ax = plt.subplots()
color = 'darkgreen'
size = 20
m, n = 4, 3
for i in range(m + 1):
for j in range(n + 1):
if j < n: # thick vertical line
w1 = random.randint(0, 5)
w2 = random.randint(w1 + 1, 6)
ax.add_patch(plt.Rectangle((i * size - w1, j * size), w2, size, color=color, lw=0))
if i < m: # thick horizontal line
h1 = random.randint(0, 5)
h2 = random.randint(h1 + 1, 6)
ax.add_patch(plt.Rectangle((i * size, j * size - h1), size, h2, color=color, lw=0))
ax.autoscale() # fit all rectangles into the view
ax.axis('off') # hide the surrounding axes
plt.tight_layout()
plt.show()

How to programtically set a suitable padding for matplotlib colorbar?

I'm creating some subplots each with its own colorbar at the bottom. The colorbar is added using:
cax, kw = mcbar.make_axes_gridspec(ax, orientation='horizontal',
pad=pad,
fraction=0.07, shrink=0.85, aspect=35)
figure.colorbar(cs, cax=cax, orientation='horizontal')
The pad argument is adjusted, so that if there is no xticklabels, the value is smaller, to avoid wasting space.
The complete script:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colorbar as mcbar
x = np.linspace(-1, 1, 100)
y = np.linspace(-1, 1, 50)
X, Y = np.meshgrid(x, y)
Z = X**2+np.sin(Y)
figure = plt.figure(figsize=(12, 10))
nrow = 3
ncol = 2
for ii in range(nrow*ncol):
ax = figure.add_subplot(nrow, ncol, ii+1)
row, col = np.unravel_index(ii, (nrow, ncol))
cs = ax.contourf(X, Y, Z)
if row == nrow-1:
# larger padding to make room for xticklabels
pad = 0.15
else:
# smaller padding otherwise
pad = 0.05
ax.tick_params(labelbottom=False)
if row == 1 and col == 1:
# add xlabel would need more padding
ax.set_xlabel('X')
cax, kw = mcbar.make_axes_gridspec(ax, orientation='horizontal',
pad=pad,
fraction=0.07, shrink=0.85, aspect=35)
figure.colorbar(cs, cax=cax, orientation='horizontal')
ax.set_title(str(ii+1))
figure.tight_layout()
figure.show()
The output figure:
But the current solution is using hard-coded padding values (0.15 if with xticklabels, 0.05 otherwise), and it doesn't adjust well to the existence of xlabels (see subplot 4), or changing figure sizes.
Is there a way to programmatically work out a suitable padding value to place the colorbar? Maybe by adjusting the bounding box of the parent axis object so that its bbox is smaller if there is no xlabels or xticklabels, or by finding out the coordinates of the parent axis and somehow computing a padding?
You can get the space needed for tick labels and the axis label by comparing the bounding boxes of the whole axes and the yaxis. To get these bounding boxes we need a renderer. To make it available we first need to draw the canvas. The bounding boxes are returned in display coordinates, so we transform them to axes coordinates using the inverted axes transformation. The difference of their y coordinates gives the required extra padding:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colorbar as mcbar
from matplotlib.transforms import Bbox
x = np.linspace(-1, 1, 100)
y = np.linspace(-1, 1, 50)
X, Y = np.meshgrid(x, y)
Z = X**2+np.sin(Y)
figure = plt.figure(figsize=(12, 10))
figure.canvas.draw() # to get renderer
nrow = 3
ncol = 2
for ii in range(nrow*ncol):
ax = figure.add_subplot(nrow, ncol, ii+1)
row, col = np.unravel_index(ii, (nrow, ncol))
cs = ax.contourf(X, Y, Z)
if row != nrow-1:
ax.tick_params(labelbottom=False)
if row == 1 and col == 1:
# add xlabel would need more padding
ax.set_xlabel('X')
# get height of ticklabels and label
b = ax.transAxes.inverted().transform(
[ax.yaxis.get_tightbbox(figure.canvas.renderer).p0,
ax.get_tightbbox(figure.canvas.renderer).p0]
)
pad = 0.05 + (b[0]-b[1])[1]
cax, kw = mcbar.make_axes_gridspec(ax, orientation='horizontal',
pad=pad,
fraction=0.07, shrink=0.85, aspect=35)
figure.colorbar(cs, cax=cax, orientation='horizontal')
ax.set_title(str(ii+1))
This solution has the flaw that axes 3 and 4 have different heights. You can fix this by adjusting ymin of all axes in a row to the row maximum:
figure.tight_layout()
for i in range(0, 2*ncol*nrow, 2*ncol):
ymin = 0
for j in range(0, 2*ncol, 2):
ymin = max(ymin, figure.axes[i+j].get_position().ymin)
for j in range(0, 2*ncol, 2):
b = figure.axes[i+j].get_position()
figure.axes[i+j].set_position(Bbox([[b.xmin,ymin],[b.xmax,b.ymax]]))
Please note that this adjustment must be done before applying tight_layout!

Relationship between sizes of a table and figure in matplotlib

I cannot figure out how to "synchronize" sizes of a table and a figure, so that the table lies completely within the figure.
import matplotlib.pyplot as plt
from string import ascii_uppercase
from random import choice
#content for the table
height = 9
width = 9
grid = [[choice(ascii_uppercase) for j in range(width)] for i in range(height)]
#desired size of a cell
cell_size = 0.3
fig = plt.figure(figsize=(width * cell_size, height * cell_size))
ax = fig.add_subplot(1, 1, 1)
the_table = ax.table(cellText=grid, loc='center')
for pos, cell in the_table._cells.items():
cell._height = cell._width = cell_size
plt.show()
My understanding is that the area within the axis (+ some outer margins) is the figure - when I save it as an image file, it saves only this area, cropping all the rest, and the size of the image is 194x194, which matches the figure size and DPI:
fig.get_size_inches()
>>array([2.7, 2.7])
fig.dpi
>>72.0
So I guess my question is when I set cell size in the table, isn't it in inches (same as for figure size)? Or DPI for the table is different? I couldn't find any dpi-related methods or attributes for matplotlib.table.Table class.
The width of the cells is by default automatically adjusted to fit the width of the axes, if loc="center".
What remains is to set the height of the cells. This is given in units of axes coordinates. So in order to fill the complete height of the axes (== 1 in axes coordinates), you can divide 1 by the number of rows in the table to get the height of each cell. Then set the height to all cells.
import matplotlib.pyplot as plt
from string import ascii_uppercase
from random import choice
#content for the table
height = 9
width = 9
grid = [[choice(ascii_uppercase) for j in range(width)] for i in range(height)]
fig, ax = plt.subplots()
#ax.plot([0,2])
the_table = ax.table(cellText=grid, loc='center')
the_table.auto_set_font_size(False)
cell_height = 1 / len(grid)
for pos, cell in the_table.get_celld().items():
cell.set_height(cell_height)
plt.show()

Border around a upper/lower triangle of imshow

This question relates to #bgbg's question about how to visualize only the upper or lower triangle of a symmetric matrix in matplotlib. Using his code (shown at the end), we can generate a figure like this:
Now my question: how can we draw a dark border around just this set of blocks? I ask, because I want to plot two sets of correlation data and put them next to each other as an upper and lower triangle. We can then draw a dark border around each triangle independently, to separate out the two triangles and show they are different metrics. So, like this, but not confusing:
How to do it?
#Figure 1
import numpy as NP
from matplotlib import pyplot as PLT
from matplotlib import cm as CM
A = NP.random.randint(10, 100, 100).reshape(10, 10)
mask = NP.tri(A.shape[0], k=-1)
A = NP.ma.array(A, mask=mask) # mask out the lower triangle
fig = PLT.figure()
ax1 = fig.add_subplot(111)
cmap = CM.get_cmap('jet', 10) # jet doesn't have white color
cmap.set_bad('w') # default value is 'k'
ax1.imshow(A, interpolation="nearest", cmap=cmap)
ax1.grid(True)
axis('off')
#Figure 2
A = NP.random.randint(10, 100, 100).reshape(10, 10)
mask = NP.tri(A.shape[0], k=-1)
mask = NP.zeros_like(A)
mask[NP.arange(10), NP.arange(10)] = 1
A = NP.ma.array(A, mask=mask) # mask out the lower triangle
fig = PLT.figure()
ax1 = fig.add_subplot(111)
cmap = CM.get_cmap('jet', 10) # jet doesn't have white color
cmap.set_bad('w') # default value is 'k'
ax1.imshow(A, interpolation="nearest", cmap=cmap)
title("Correlation Data 1")
ylabel("Correlation Data 2")
yticks([])
xticks([])
You could draw a border using patches.Polygon:
import numpy as NP
from matplotlib import pyplot as PLT
import matplotlib.patches as patches
N = 10
A = NP.random.randint(10, 100, N * N).reshape(N, N)
mask = NP.tri(A.shape[0], k=-1)
mask = NP.zeros_like(A)
mask[NP.arange(N), NP.arange(N)] = 1
A = NP.ma.array(A, mask=mask) # mask out the lower triangle
fig, ax = PLT.subplots()
cmap = PLT.get_cmap('jet', 10) # jet doesn't have white color
cmap.set_bad('w') # default value is 'k'
ax.imshow(A, interpolation="nearest", cmap=cmap, extent=[0, N, 0, N])
line = ([(0, N - 1), (0, 0), (N - 1, 0)] +
[(N - 1 - i - j, i + 1) for i in range(N - 1) for j in (0, 1)])
lines = [line, [(N - x, N - y) for x, y in line]]
for line in lines:
path = patches.Polygon(line, facecolor='none', edgecolor='black',
linewidth=5, closed=True, joinstyle='round')
ax.add_patch(path)
ax.set_xlabel("Correlation Data 1")
ax.xaxis.set_label_position('top')
ax.set_ylabel("Correlation Data 2")
ax.set_yticks([])
ax.set_xticks([])
margin = 0.09
ax.set_xlim(-margin, N + margin)
ax.set_ylim(-margin, N + margin)
ax.set_frame_on(False)
PLT.show()

How do I offset lines in matplotlib by X points

I'm using matplotlib to plot some data that I wish to annotate with arrows (distance markers). These arrows should be offset by several points so as not to overlap with the plotted data:
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
fig, ax = plt.subplots()
x = [0, 1]
y = [0, 0]
# Plot horizontal line
ax.plot(x, y)
dy = 5/72
offset = transforms.ScaledTranslation(0, dy, ax.get_figure().dpi_scale_trans)
verttrans = ax.transData+offset
# Plot horizontal line 5 points above (works!)
ax.plot(x, y, transform = verttrans)
# Draw arrow 5 points above line (doesn't work--not vertically translated)
ax.annotate("", (0,0), (1,0),
size = 10,
transform=verttrans,
arrowprops = dict(arrowstyle = '<|-|>'))
plt.show()
Is there any way to make lines drawn by ax.annotate() be offset by X points? I wish to use absolute coordinates (e.g., points or inches) instead of data coordinates because the axis limits are prone to changing.
Thanks!
The following code does what I desired. It uses ax.transData and figure.get_dpi():
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
fig, ax = plt.subplots()
x = [0, 1]
y = [0, 0]
ax.plot(x, y)
dy = 5/72
i = 1 # 0 for dx
tmp = ax.transData.transform([(0,0), (1,1)])
tmp = tmp[1,i] - tmp[0,i] # 1 unit in display coords
tmp = 1/tmp # 1 pixel in display coords
tmp = tmp*dy*ax.get_figure().get_dpi() # shift pixels in display coords
ax.plot(x, y)
ax.annotate("", [0,tmp], [1,tmp],
size = 10,
arrowprops = dict(arrowstyle = '<|-|>'))
plt.show()
What's your expected output? If you're just looking to move the arrow you're drawing vertically, the API for annotate is
annotate(s, xy, xytext=None, ...)
so you can draw something like
ax.annotate("", (0,0.01), (1,0.01),
size = 10,
arrowprops = dict(arrowstyle = '<|-|>'))
which is moved up by 0.01 in data coordinates in the y direction. You can also specify coordinates as a fraction of the total figure size in annotate (see doc). Is that what you wanted?

Categories

Resources