Draw a grid of cells using Matplotlib - python

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()

Related

Is there an easy way to print a Matplotlib figure (Saros Dial) to an exact size on a DIN A4 jpeg?

As part of a COVID/Lockdown/Geek project making a 3D Antikythera model I need to print a Saros Dial to be exactly 8.9 cm in width. I have muddled my way through playing around with the print scale. This works for the office printer. However I want to have it etched on plexiglas and I need to upload a DIN A4 jpeg or pdf with the (0,0) coordinate right in the centre
Code for the dial below:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# define parameters
a = 2 # determines the width of the turns - set to 1 means width is 2 Pi
thetaMin, thetaMax = 26*np.pi, 34*np.pi # Dial starts at the 13th turn and finishes at the 17th turn
steps = 223+1 # Number of Saros periods (+1 for end marker)
# Generate plotting values
# Main spiral
theta = np.linspace(thetaMin, thetaMax, steps)
r = theta * a
x = r*np.cos(theta)
y = r*np.sin(-theta)
# Main spiral outer rim completion
thetaMinComp, thetaMaxComp = thetaMax, thetaMax+(2*np.pi)
thetaComp= np.linspace(thetaMinComp, thetaMaxComp,steps)
rComp = thetaComp * a
xComp = rComp*np.cos(thetaComp)
yComp = rComp*np.sin(-thetaComp)
# Seperator lines
# Need to plot between the period startpoint accros the turn(out from the center (0,0))
l=np.sqrt(np.square(x)+np.square(y)+1.2) # calculate length from center, just shortening it makes it not overshoot
xnew= x*1/l*(l+2*np.pi*a) # get the x for the line extendeaccros the turn
ynew= y*1/l*(l+2*np.pi*a) # get the y for the line extendeaccros the turn
#start plotting
fig, ax = plt.subplots(figsize=(20,20))
plt.axis('off')
for i in range(len(x)):
plt.plot([x[i],xnew[i]],[y[i],ynew[i]], c='r', lw=2)
# plt.scatter(x[i], y[i], c = 'g') # Plots dial points for reference
steps = 223+1 # Number of Saros periods (+1 for end marker)
a = 2 # determines the width of the turns - set to 1 means width is 2 Pi
thetaMin, thetaMax = (26*np.pi+(2*np.pi)/(.5*steps)), (34*np.pi+(2*np.pi)/(.5*steps)) # Dial starts at the 13th turn and finishes at the 17th turn
theta = np.linspace(thetaMin, thetaMax, steps)
r = theta * a
xx = r*np.cos(theta)
yy = r*np.sin(-theta)
xxnew= xx*1/l*(l+2*np.pi*(a*.5)) # get the x for the line extendeaccros the turn
yynew= yy*1/l*(l+2*np.pi*(a*.5)) # get the y for the line extendeaccros the turn
plt.plot(x, y, c='r', lw=2)
plt.plot(xComp, yComp,c='r', lw=2)
plt.scatter(0,0)
plt.text(0,75,'Saros Dial', {'fontname': 'Herculanum',
'fontsize': '100',
'fontweight' : 'bold',
'verticalalignment': 'baseline',
'horizontalalignment': 'center'})
plt.show()
I have found Using matplotlib, how can I print something "actual size"? but is still fiddling. Is there an easier way?
This proved to be frustrating. It is printer dependent...and online printer specifications for the margins may not match the actual margins when printing. I have a hunch it may even be OS or driver specific.
import matplotlib.pyplot as plt
import matplotlib as mpl
# This example fits DIN A4 paper on a HP Laserjet Pro 200 MFP
# figure settings
left_margin = 0.2
right_margin = 0.9
top_margin = 0.6
bottom_margin = 0.6
left_right_margin = left_margin+right_margin
top_bottom_margin = top_margin+bottom_margin
figure_width = 21-left_right_margin # cm
figure_height = 29.7-top_bottom_margin
# Don't change
left = left_right_margin / 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
h_corr = 1.017
v_corr = 1.011
# specifying the width and the height of the box in inches
fig = plt.figure(figsize=(figure_width*cm2inch*h_corr,figure_height*cm2inch*v_corr))
ax = fig.add_axes((left, bottom, width, height))
# limits settings (important)
# plt.xlim(0, figure_width * width)#(0,0) at left bottom
# plt.ylim(0, figure_height * height)#(0,0) at left bottom
plt.xlim(-figure_width * width/2, figure_width * width/2) #centers (0,0)
plt.ylim(-figure_height * height/2, figure_height * height/2)#centers (0,0)
# your Plot (consider above limits)
# # define parameters
a = .03538 # determines the width of the turns - set to 1 means width is 2 Pi
thetaMin, thetaMax = 26*np.pi, 34*np.pi # Dial starts at the 13th turn and finishes at the 17th turn
steps = 223+1 # Number of Saros periods (+1 for end marker)
# Generate plotting values
# Main spiral
theta = np.linspace(thetaMin, thetaMax, steps)
r = theta * a
x = r*np.cos(theta)
y = r*np.sin(-theta)
# Main spiral outer rim completion
thetaMinComp, thetaMaxComp = thetaMax, thetaMax+(2*np.pi)
thetaComp= np.linspace(thetaMinComp, thetaMaxComp,steps)
rComp = thetaComp * a
xComp = rComp*np.cos(thetaComp)
yComp = rComp*np.sin(-thetaComp)
# Seperator lines
# Need to plot between the period startpoint accros the turn(out from the center (0,0))
l=np.sqrt(np.square(x)+np.square(y)+1.2) # calculate length from center, just shortening it makes it not overshoot
xnew= x*1/l*(l+2*np.pi*a) # get the x for the line extendeaccros the turn
ynew= y*1/l*(l+2*np.pi*a) # get the y for the line extendeaccros the turn
for i in range(len(x)):
plt.plot([x[i],xnew[i]],[y[i],ynew[i]], c='r', lw=1)
xx = r*np.cos(theta)
yy = r*np.sin(-theta)
xxnew= xx*1/l*(l+2*np.pi*(a*.5)) # get the x for the line extendeaccros the turn
yynew= yy*1/l*(l+2*np.pi*(a*.5)) # get the y for the line extendeaccros the turn
plt.plot(x, y, c='b', lw=1)
plt.plot(xComp, yComp,c='g', lw=1)
plt.scatter(0,0)
plt.text(0,4.75,'Saros Dial',
{'fontname': 'Herculanum',
'fontsize': '30',
'fontweight' : 'bold',
'verticalalignment': 'baseline',
'horizontalalignment': 'center'})
plt.plot([-8,-8],[-10,10])
plt.text(-7,0, ' 20 centimeters' , rotation=90, ha='center', va='center', fontsize=30, color='magenta')
plt.show()
plt.show()
# save figure ( printing png file had better resolution, pdf was lighter and better on screen)
fig.savefig('A4_grid_cm.png', dpi=1000)
fig.savefig('tA4_grid_cm.pdf')

python matplotlib's gridspec unable to reduce gap between subplots

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()

Draw linewidth inside rectangle matplotlib

I'm having trouble drawing rectangles in matplotlib using Patches. When linewidth is supplied to patches.Rectangle, the border is drawn on the outside of the rectangle. Here's an example:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig, ax = plt.subplots(1)
rect = patches.Rectangle((1, 1), 1, 1, facecolor = 'blue')
rect2 = patches.Rectangle((1, 2.1), 1, 1, facecolor = 'none', edgecolor = 'black', linewidth = 6)
ax.add_patch(rect)
ax.add_patch(rect2)
ax.set_xlim([0, 3.5])
ax.set_ylim([0, 3.5])
here's the result:
Note that the border is drawn on the outside of the box such that the box + border now exceeds the size of the blue box. I would like the border to be drawn inside the box, such that it is always the same size as the blue box regardless of linewidth.
I've tried this in two different ways and neither was satisfying:
Convert the linewidth from absolute units into units of the data, then calculate a smaller box that could be drawn with a normal border that matches the other box.
Explore some of the offsetbox functionality as in this post, though I didn't get very far as I couldn't figure out how to specify pad correctly.
Any help would be appreciated!
The easiest approach is to set a clip rectangle that hides everything outside the rectangle. As you already have a rectangle, it can be used to clip itself.
As the border is drawn centered on the border line, half of it will be clipped away. This can be tackled by setting the width to double the desired width.
Note that for clipping to work as desired, the rectangle already needs to be transformed to axes coordinates. So, first add the rectangle patch to the ax and only then use is to set the clipping.
Also note that with default parameters, a rectangle uses the same color for the inside as well as for a thin border. Setting the linewidth to zero ensures that it doesn't draw outside the rectangle.
Similarly, ellipses can be drawn with the line only at the inside.
The code below uses a thickness of 10 and some extra dotted red lines to illustrate what's happening.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig, ax = plt.subplots()
pad = 0.1 # distance between the rectangles
for i in range(3):
for j in range(2):
x = .5 + i * (1 + pad)
y = .5 + j * (1 + pad)
if i == j:
patch = patches.Rectangle((x, y), 1, 1, facecolor='blue', linewidth=0)
elif i < 2:
patch = patches.Rectangle((x, y), 1, 1, facecolor='none', edgecolor='black',
linewidth=10*2 if j == 0 else 10)
else:
patch = patches.Ellipse((x+0.5, y+0.5), 1, 1, facecolor='none', edgecolor='black',
linewidth=10*2 if j == 0 else 10)
ax.add_patch(patch)
if j == 0:
patch.set_clip_path(patch)
for i in range(3):
x = .5 + i * (1 + pad)
for s in 0,1:
ax.axvline (x+s, color='crimson', ls=':', lw=1)
for j in range(2):
y = .5 + j * (1 + pad)
for s in 0,1:
ax.axhline (y+s, color='crimson', ls=':', lw=1)
ax.set_xlim([0, 4.0])
ax.set_ylim([0, 3.0])
ax.set_aspect('equal')
plt.show()
The image below shows the standard way of drawing at the top, and clipping with double linewidth at the bottom.

Generate grid cells (occupancy grid), color cells, and remove xlabels

I am working on some graph based planning algorithms but I first wanted to setup a plotting script that takes a 2D matrix and plots it as grid cells, and colors the cells based on certain values.
I am trying to determine two things:
How can I completely remove the Xticks, if you look at the images generated they are quite faint but still there?
Is there a better approach to the grid plotting and generation that I am overlooking? I know the generate_moves is not perfect but this is the first take.
Here is a link to the repo where I have got to (same as the code below)
Here is the code I have
import matplotlib.pyplot as plt
from matplotlib import colors
import numpy as np
import random
EMPTY_CELL = 0
OBSTACLE_CELL = 1
START_CELL = 2
GOAL_CELL = 3
MOVE_CELL = 4
# create discrete colormap
cmap = colors.ListedColormap(['white', 'black', 'green', 'red', 'blue'])
bounds = [EMPTY_CELL, OBSTACLE_CELL, START_CELL, GOAL_CELL, MOVE_CELL ,MOVE_CELL + 1]
norm = colors.BoundaryNorm(bounds, cmap.N)
def plot_grid(data, saveImageName):
fig, ax = plt.subplots()
ax.imshow(data, cmap=cmap, norm=norm)
# draw gridlines
ax.grid(which='major', axis='both', linestyle='-', color='k', linewidth=1)
ax.set_xticks(np.arange(0.5, rows, 1));
ax.set_yticks(np.arange(0.5, cols, 1));
plt.tick_params(axis='both', labelsize=0, length = 0)
# fig.set_size_inches((8.5, 11), forward=False)
plt.savefig(saveImageName + ".png", dpi=500)
def generate_moves(grid, startX, startY):
num_rows = np.size(grid, 0)
num_cols = np.size(grid, 1)
# Currently do not support moving diagonally so there is a max
# of 4 possible moves, up, down, left, right.
possible_moves = np.zeros(8, dtype=int).reshape(4, 2)
# Move up
possible_moves[0, 0] = startX - 1
possible_moves[0, 1] = startY
# Move down
possible_moves[1, 0] = startX + 1
possible_moves[1, 1] = startY
# Move left
possible_moves[2, 0] = startX
possible_moves[2, 1] = startY - 1
# Move right
possible_moves[3, 0] = startX
possible_moves[3, 1] = startY + 1
# Change the cell value if the move is valid
for row in possible_moves:
if row[0] < 0 or row[0] >= num_rows:
continue
if row[1] < 0 or row[1] >= num_cols:
continue
grid[row[0], row[1]] = MOVE_CELL
if __name__ == "__main__":
rows = 20
cols = 20
# Randomly create 20 different grids
for i in range(0, 20):
data = np.zeros(rows * cols).reshape(rows, cols)
start_x = random.randint(0, rows - 1)
start_y = random.randint(0, cols - 1)
data[start_x, start_y] = START_CELL
goal_x = random.randint(0, rows - 1)
# Dont want the start and end positions to be the same
# so keep changing the goal x until its different.
# If X is different dont need to check Y
while goal_x is start_x:
goal_x = random.randint(0, rows - 1)
goal_y = random.randint(0, cols - 1)
data[goal_x, goal_y] = GOAL_CELL
generate_moves(data, start_x, start_y)
plot_grid(data, "week1/images/grid_" + str(i))
Use the following to hide ticks and labels
def plot_grid(data, saveImageName):
fig, ax = plt.subplots()
ax.imshow(data, cmap=cmap, norm=norm)
# draw gridlines
ax.grid(which='major', axis='both', linestyle='-', color='k', linewidth=1)
ax.set_xticks(np.arange(0.5, rows, 1));
ax.set_yticks(np.arange(0.5, cols, 1));
plt.tick_params(axis='both', which='both', bottom=False,
left=False, labelbottom=False, labelleft=False)
fig.set_size_inches((8.5, 11), forward=False)
plt.savefig(saveImageName + ".png", dpi=500)

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()

Categories

Resources