Relationship between sizes of a table and figure in matplotlib - python

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

Related

Set absolute size of matplotlib subplots

I know how to set the relative size of subplots within a figure using gridspec or subplots_adjust, and I know how to set the size of a figure using figsize. My problem is setting the absolute size of the subplots.
Use case: I am making two separate plots which will be saved as pdfs for an academic paper. One has two subplots and one has three subplots (in both cases in 1 row). I need each of the 5 subplots to be the exact same size with the exact same font sizes (axis labels, tick labels, etc) in the resulting PDFs. In the example below the fonts are the same size but the subplots are not. If I make the height of the resulting PDFs the same (and thus the axes), the font on 3-subplots.pdf is smaller than that of 2-subplots.pdf.
MWE:
import matplotlib.pyplot as plt
subplots = [2, 3]
for i, cols in enumerate(subplots):
fig, ax = plt.subplots(1, cols, sharey=True, subplot_kw=dict(box_aspect=1))
for j in range(cols):
ax[j].set_title(f'plot {j*cols}')
ax[j].set_xlabel('My x label')
ax[0].set_ylabel('My y label')
plt.tight_layout()
plt.savefig(f'{cols}-subplots.pdf', bbox_inches='tight', pad_inches=0)
plt.show()
Output:
I prefer to use fig.add_axes([left, bottom, width, height]) which let you control the size and location of each subplot precisely. left and bottom decide the location of your subplots, while width and height decide the size. All quantities are in fractions of figure width and height, thus they are all float between 0 and 1.
An example:
fig = plt.figure(figsize=(8.3, 11.7))
axs = {
"ax1": fig.add_axes([0.2, 0.7, 0.6, 0.2], xticklabels=[]),
"ax2": fig.add_axes([0.2, 0.49, 0.6, 0.2], xticklabels=[]),
"ax3": fig.add_axes([0.2, 0.28, 0.6, 0.2]),
}
With this I created 3 subplots in an A4 size figure, each of them are 0.6x8.3 width and 0.2x11.7 height. The spacing between them is 0.1x11.7. "ax1" and "ax2" do not show xticklabels so that I can set shared x ticks for them later.
You can see matplotlib API refenrence for more information https://matplotlib.org/stable/api/figure_api.html
I ended up solving this by:
setting explicit absolute lengths for subplot width/height, the space between subplots and the space outside subplots,
adding them up to get an absolute figure size,
setting the subplot box_aspect to 1 to keep them square.
import matplotlib.pyplot as plt
num_subplots = [2, 3]
scale = 1 # scaling factor for the plot
subplot_abs_width = 2*scale # Both the width and height of each subplot
subplot_abs_spacing_width = 0.2*scale # The width of the spacing between subplots
subplot_abs_excess_width = 0.3*scale # The width of the excess space on the left and right of the subplots
subplot_abs_excess_height = 0.3*scale # The height of the excess space on the top and bottom of the subplots
for i, cols in enumerate(num_subplots):
fig_width = (cols * subplot_abs_width) + ((cols-1) * subplot_abs_spacing_width) + subplot_abs_excess_width
fig_height = subplot_abs_width+subplot_abs_excess_height
fig, ax = plt.subplots(1, cols, sharey=True, figsize=(fig_width, fig_height), subplot_kw=dict(box_aspect=1))
for j in range(cols):
ax[j].set_title(f'plot {j}')
ax[j].set_xlabel('My x label')
ax[0].set_ylabel('My y label')
plt.tight_layout()
plt.savefig(f'{cols}-subplots.pdf', bbox_inches='tight', pad_inches=0)
plt.show()
I created a function that creates axes with absolute sizes and acts in most ways like plt.subplots(...), for example by allowing shared y- or x-axes and returning the axes as a shaped numpy array. It centers the axes inside their grid areas, giving them as much space as possible between themselves and the edges of the figure, assuming you set figsize large enough.
The arguments include absolute height and width for the figure (see the matplotlib documentation for details) and absolute height and width for the axes, as requested in the original question.
from typing import Tuple
from matplotlib import pyplot as plt
import numpy as np
def subplots_with_absolute_sized_axes(
nrows: int, ncols: int,
figsize: Tuple[float, float],
axis_width: float, axis_height: float,
sharex: bool=False, sharey: bool=False) -> Tuple[plt.Figure, numpy.ndarray]:
''' Create axes with exact sizes.
Spaces axes as far from each other and the figure edges as possible
within the grid defined by nrows, ncols, and figsize.
Allows you to share y and x axes, if desired.
'''
fig = plt.figure(figsize=figsize)
figwidth, figheight = figsize
# spacing on each left and right side of the figure
h_margin = (figwidth - (ncols * axis_width)) / figwidth / ncols / 2
# spacing on each top and bottom of the figure
v_margin = (figheight - (nrows * axis_height)) / figheight / nrows / 2
row_addend = 1 / nrows
col_addend = 1 / ncols
inner_ax_width = axis_width / figwidth
inner_ax_height = axis_height / figheight
axes = []
sharex_ax = None
sharey_ax = None
for row in range(nrows):
bottom = (row * row_addend) + v_margin
for col in range(ncols):
left = (col * col_addend) + h_margin
if not axes:
axes.append(fig.add_axes(
[left, bottom, inner_ax_width, inner_ax_height]))
if sharex:
sharex_ax = axes[0]
if sharey:
sharey_ax = axes[0]
else:
axes.append(fig.add_axes(
[left, bottom, inner_ax_width, inner_ax_height],
sharex=sharex_ax, sharey=sharey_ax))
return fig, np.flip(np.asarray(list(axes)).reshape((nrows, ncols)), axis=0)

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

How to use matplotlib to add image to the top level of surface of all the existing layers

I am trying to add a logo to the current plot, which already has existing plot elements. I defined a background in the plot_pic() function. Then plot it, and I want to add a logo to the top surface. I've tried to put the zorder = 10, but it doesn't work. The codes in Jupyter Notebook are:
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from matplotlib.offsetbox import OffsetImage
%matplotlib inline
from matplotlib.patches import Circle, Rectangle, Arc, Ellipse
def plot_pic(ax=None, color='black', lw=2, scale = 15):
# get the current ax if ax is None
if ax is None:
ax = plt.gca()
big_box = Rectangle((-34 * scale, 0), width = 68 * scale, height = 105 / 2 * scale, linewidth=lw, color=color, fill=False)
middle_box = Rectangle((-(7.32 * scale / 2+ 5.5 * scale +11 * scale),0), width = (5.5 * scale * 2 + 11 * scale * 2 + 7.32 * scale), height = 16.5 * scale, linewidth = lw, color = color, fc = "white")
small_box = Rectangle((-(7.32 * scale/ 2 + 5.5 * scale), 0), width = 7.32 * scale + 5.5 * scale * 2, height = 5.5 * scale, linewidth = lw, color = color, fill = False)
arc = Circle((0, 11 * scale), radius = 9.15 * scale, color = color, lw = lw, fill = False, zorder = 0)
# List of elements to be plotted
pic_elements = [big_box, middle_box, small_box, arc]
# Add the elements onto the axes
for element in pic_elements:
ax.add_patch(element)
return ax
fig = plt.figure()
fig = plt.figure(figsize=(10, 10))
ax = plt.subplot()
logo=mpimg.imread('rbl_logo.png')
# You have to add your own logo, this is in my own folder
addLogo = OffsetImage(logo, zoom=0.6, zorder = 10)
addLogo.set_offset((200,-10)) # pass the position in a tuple
ax.add_artist(addLogo)
plt.xlim(-600,600)
plt.ylim(-100,1000)
plot_pic()
The result is that, the plot_pic() layer covers part of the logo that I wanted to show, and I just want to place the logo on the very top surface that covers all the elements below.
Is there anyway to do so? Thank you very much.
The problem is that setting the zorder with the keyword arguments sets the zorder of the image inside the OffsetBox, which will have no effect. In order to set the zorder of the box itself, you need to set this externally:
addLogo = OffsetImage(logo, zoom=0.6)
addLogo.set_zorder(10)

Matplotlib automatically scale vertical height of subplots for shared x-axis figure

I want to automatically scale the vertical height of subplots for shared x-axis figures based on their data span! I want to compare the relative intensity of the displayed data. If i use the sharey=True kwarg for the subbplots the data is displayed in a way that the relative intensity is recognizable:
import matplotlib.pyplot as plt
from matplotlib import gridspec
import numpy as np
SIZE = (12, 8) #desired overall figure size
# Simple data to display in various forms
x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)
y2 = 2*(np.sin(x ** 2))
y3 = 3*(np.sin(x ** 2))
fig, ax = plt.subplots(3,ncols=1, sharex=True, sharey=True)
fig.set_size_inches(SIZE[1], SIZE[0])
fig.subplots_adjust(hspace=0.001)
ax[0].plot(x, y)
ax[1].plot(x, y2)
ax[2].plot(x, y3)
plt.show()
All subplots have the same height now and the data span in the y-Axis is recognizable as the data is displayed with the correct relative proportion.
What i would like to achieve is that the scales of each plot end where the data ends. Essentially eliminating the not used white space. The size of the subplot would than represent the relative height ratios of the data. They should still have the same scaling on the Y axis in order for the viewer to estimate the relative data height ( which cold be a countrate for example).
I found the following links to similar problems but none really helped me to solve my issue:
Link1 Link2
Here an example that determines the ratio for you and creates the subplots accordingly:
import matplotlib.pyplot as plt
from matplotlib import gridspec
import numpy as np
SIZE = (12, 8) #desired overall figure size
# Simple data to display in various forms
x = np.linspace(0, 2 * np.pi, 400)
# the maximum multiplier for the function
N = 3
# the y-ranges:
ys = [i * np.sin(x**2) for i in range(1,N+1)]
# the maximum extent of the plot in y-direction (cast as int)
hs = [int(np.ceil(np.max(np.abs(y)))) for y in ys]
# determining the size of the GridSpec:
gs_size = np.sum(hs)
gs = gridspec.GridSpec(gs_size,1)
# the figure
fig = plt.figure(figsize = SIZE)
# creating the subplots
base = 0
ax = []
for y,h in zip(ys,hs):
ax.append(fig.add_subplot(gs[base:h+base,:]))
base += h
ax[-1].plot(x,y)
##fig, ax = plt.subplots(3,ncols=1, sharex=True, sharey=True)
##fig.set_size_inches(SIZE[1], SIZE[0])
fig.subplots_adjust(hspace=0.001)
##ax[0].plot(x, ys[0])
##ax[1].plot(x, ys[1])
##ax[2].plot(x, ys[2])
plt.show()
The code determines the maximum y-extend for each set of data, casts it into an integer and then divides the figure into subplots using the sum of these extends as scale for the GridSpec.
The resulting figure looks like this:
Tested on Python 3.5
EDIT:
If the maximum and minimum extents of your data are not comparable, it may be better to change the way hs is calculated into
hs = [int(np.ceil(np.max(y))) - int(np.floor(np.min(y))) for y in ys]

How can I create a figure with optimal resolution for printing?

I'm rather fond of The Logistic Map' Period Doubling Bifurcation and would like to print it on a canvas.
I can create the plot in python, but need some help preparing figure properties so that it has suitable resolution to be printed. My code right ow produces some jagged lines.
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
# overall image properties
width, height, dpi = 2560, 1440, 96
picture_background = 'white'
aspect_ratio = width / height
plt.close('all')
R = np.linspace(3.5,4,5001)
fig = plt.figure(figsize=(width / dpi, height / dpi), frameon=False)
ylim = -0.1,1.1
ax = plt.Axes(fig, [0, 0, 1, 1], xlim = (3.4,4))
ax.set_axis_off()
fig.add_axes(ax)
for r in R:
x = np.zeros(5001)
x[0] = 0.1
for i in range(1,len(x)):
x[i] = r*x[i-1]*(1-x[i-1])
ax.plot(r*np.ones(2500),x[-2500:],marker = '.', markersize= 0.01,color = 'grey', linestyle = 'none')
plt.show()
plt.savefig('figure.eps', dpi=dpi, bbox_inches=0, pad_inches=0, facecolor=picture_background)
Here is what the code produces:
As you can see, some of the lines to the far left of the plot are rather jagged.
How can I create this figure so that the resolution is suitable to be printed on a variety of frame dimensions?
I think the source of the jaggies is underlying pixel size + that you are drawing this using very small 'point' markers. The pixels that the line are going through are getting fully saturated so you get the 'jaggy'.
A somewhat better way to plot this data is to do the binning ahead of time and then have mpl plot a heat map:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
plt.ion()
width, height, dpi = 2560 / 2, 1440 / 2, 96 # cut to so SO will let me upload result
aspect_ratio = width / height
fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), frameon=False,
tight_layout=True)
ylim = -0.1, 1.1
ax.axis('off')
# make heatmap at double resolution
accumulator = np.zeros((height, width), dtype=np.uint64)
burn_in_count = 25000
N = 25000
R = np.linspace(3.5, 4, width)
x = 0.1 * np.ones_like(R)
row_indx = np.arange(len(R), dtype='uint')
# do all of the r values in parallel
for j in range(burn_in_count):
x = R * x * (1 - x)
for j in range(N):
x = R * x * (1 - x)
col_indx = (height * x).astype('int')
accumulator[col_indx, row_indx] += 1
im = ax.imshow(accumulator, cmap='gray_r',
norm=mcolors.LogNorm(), interpolation='none')
Note that this is log-scaled, if you just want to see what pixels are hit
use
im = ax.imshow(accumulator>0, cmap='gray_r', interpolation='nearest')
but these still have issues of the jaggies and (possibly worse) sometimes the narrow lines get aliased out.
This is the sort of problem that datashader or rasterized scatter is intended to solve by re-binning the data at draw time in an intelligent way (see this PR for a prototype datashader/mpl integartion). Both of those are still prototype/proof-of-concept, but usable.
http://matplotlib.org/examples/event_handling/viewlims.html which re-compute the Mandelbrot set on zoom might also be of interest to you.

Categories

Resources