How to combine gridspec with plt.subplots() to eliminate space between rows of subplots - python-3.x

I am trying to plot multiple images in subplots and either eliminate the space between subplots (horizontal and vertical) or control it. I tried to use the suggestion in How to Use GridSpec.... I also tried here but they are not using subplots(): space between subplots
I am able to eliminate the horizontal space but not the vertical space with what I am doing in the code below. Please do not mark as duplicate as I have tried the other posts and they do not do what I want. My code is shown below. Maybe there is another keyword argument that I need in the gridspec_kw dictionary?
I want to use plt.subplots() not plt.subplot() for this. In case it matters, the images are not square they are rectangular. I also tried adding f.tight_layout(h_pad=0,w_pad=0) before plt.show() but it did not change anything.
def plot_image_array_with_angles(img_array,correct_angles,predict_angles,
fontsize=10,figsize=(8,8)):
'''
Imports:
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib.pyplot as plt
'''
num_images = len(img_array)
grid = int(np.sqrt(num_images)) # will only show all images if square
#f, axarr = plt.subplots(grid,grid,figsize=figsize)
f, axarr = plt.subplots(grid,grid,figsize=figsize,
gridspec_kw={'wspace':0,'hspace':0})
im = 0
for row in range(grid):
for col in range(grid):
axarr[row,col].imshow(img_array[im])
title = 'cor = ' + str(correct_angles[im]) + ' pred = ' + str(predict_angles[im])
axarr[row,col].set_title(title,fontsize=fontsize)
axarr[row,col].axis('off') # turns off all ticks
#axarr[row,col].set_aspect('equal')
im += 1
plt.show()
return

The aspect ratio of an imshow plot is automatically set such that pixels in the image are squared. This setting is stronger than any of the subplots_adjust or gridspec settings for spacing. Or in other words you cannot directly control the spacing between subplots if those subplots have their aspect set to "equal".
First obvious solution is to set the image aspect to automatic ax.set_aspect("auto"). This solves the problem of subplot spacing, but distorts the images.
The other option is to adjust the figure margins and the figure size such that the spacing between the subplots is as desired.
Let's say figh and figw are the figure height and width in inch, and s the subplot width in inch. The margins are bottom, top, left and right (relative to figure size) and the spacings hspace in vertical and wspace in horizontal direction (relative to subplot size). The number of rows is denoted n and the number of columns m. The aspect is the ratio between subplot (image) height and width (aspect = image height / image width).
Then the dimensions can be set via
fig, axes = plt.subplots(nrows=n, ncols=m, figsize=(figwidth, figheight))
plt.subplots_adjust(top=top, bottom=bottom, left=left, right=right,
wspace=wspace, hspace=hspace)
The respective values can be calculated according to:
Or, if the margins are the same:
An example:
import matplotlib.pyplot as plt
image = plt.imread("https://i.stack.imgur.com/9qe6z.png")
aspect = image.shape[0]/float(image.shape[1])
print aspect
n = 2 # number of rows
m = 4 # numberof columns
bottom = 0.1; left=0.05
top=1.-bottom; right = 1.-left
fisasp = (1-bottom-(1-top))/float( 1-left-(1-right) )
#widthspace, relative to subplot size
wspace=0.15 # set to zero for no spacing
hspace=wspace/float(aspect)
#fix the figure height
figheight= 3 # inch
figwidth = (m + (m-1)*wspace)/float((n+(n-1)*hspace)*aspect)*figheight*fisasp
fig, axes = plt.subplots(nrows=n, ncols=m, figsize=(figwidth, figheight))
plt.subplots_adjust(top=top, bottom=bottom, left=left, right=right,
wspace=wspace, hspace=hspace)
for ax in axes.flatten():
ax.imshow(image)
ax.set_title("title",fontsize=10)
ax.axis('off')
plt.show()

Related

Putting text from one corner to the opposite one

I'd like my plot to have a background text to be stretched from one corner (say lower left) to the opposite corner. The x and y dimensions are not isometric and it's not a square plot, so a fixed angle of 45 degrees will not work.
So far I have the text starting in the correct corner. How can the text be rotated and stretched so it spans the entire plot?
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
# plot command not shown
ax.text(ax.get_xlim()[0], ax.get_ylim()[0] , 'PRELIMINARY' , rotation=45 )
To position something with respect to the subplot (the ax), it helps to work in axes coordinates. These go from 0,0 in the lower left to 1,1 in the top right. Putting the text at 0.5,0.5 would set it nicely centered.
To calculate the angle, one could divide the subplot's height in pixels by its width, then take the arc tangent, and convert from radians to degrees.
The optimal size for the text is harder to calculate. One would need to render it, measure it, change the font size and render it again. Or just manually try a few sizes until it looks OK.
Note that when the window size gets changed interactively, the text will stay nicely in the center, but the rotation will get a bit off.
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.text(0.5, 0.5, 'PRELIMINARY', transform=ax.transAxes, size=50,
rotation=np.degrees(np.arctan(ax.get_window_extent().height / ax.get_window_extent().width)),
ha='center', va='center')
plt.show()

How to align heights and widths subplot axes with gridspec and matplotlib?

I am trying to use matplotlib with gridspec to create a subplot such that the axes are arranged to look similar to the figure below; the figure was taken from this unrelated question.
My attempt at recreating this axes arrangement is below. Specifically, my problem is that the axes are not properly aligned. For example, the axis object for the blue histogram is taller than the axis object for the image with various shades of green; the orange histogram seems to properly align in terms of width, but I attribute this to luck. How can I properly align these axes? Unlike the original figure, I would like to add/pad extra empty space between axes such that there borders do not intersect; the slice notation in the code below does this by adding a blank row/column. (In the interest of not making this post longer than it has to be, I did not make the figures "pretty" by playing with axis ticks and the like.)
Unlike the original picture, the axes are not perfectly aligned. Is there a way to do this without using constrained layout? By this, I mean some derivative of fig, ax = plt.subplots(constrained_layout=True)?
The MWE code to recreate my figure is below; note that there was no difference between ax.imshow(...) and ax.matshow(...).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize
## initialize figure and axes
fig = plt.figure()
gs = fig.add_gridspec(6, 6, hspace=0.2, wspace=0.2)
ax_bottom = fig.add_subplot(gs[4:, 2:])
ax_left = fig.add_subplot(gs[:4, :2])
ax_big = fig.add_subplot(gs[:4, 2:])
## generate data
x = np.random.normal(loc=50, scale=10, size=100)
y = np.random.normal(loc=500, scale=50, size=100)
## get singular histograms
x_counts, x_edges = np.histogram(x, bins=np.arange(0, 101, 5))
y_counts, y_edges = np.histogram(y, bins=np.arange(0, 1001, 25))
x_mids = (x_edges[1:] + x_edges[:-1]) / 2
y_mids = (y_edges[1:] + y_edges[:-1]) / 2
## get meshed histogram
sample = np.array([x, y]).T
xy_counts, xy_edges = np.histogramdd(sample, bins=(x_edges, y_edges))
## subplot histogram of x
ax_bottom.bar(x_mids, x_counts,
width=np.diff(x_edges),
color='darkorange')
ax_bottom.set_xlim([x_edges[0], x_edges[-1]])
ax_bottom.set_ylim([0, np.max(x_counts)])
## subplot histogram of y
ax_left.bar(y_mids, y_counts,
width=np.diff(y_edges),
color='steelblue')
ax_left.set_xlim([y_edges[0], y_edges[-1]])
ax_left.set_ylim([0, np.max(y_counts)])
## subplot histogram of xy-mesh
ax_big.imshow(xy_counts,
cmap='Greens',
norm=Normalize(vmin=np.min(xy_counts), vmax=np.max(xy_counts)),
interpolation='nearest',
origin='upper')
plt.show()
plt.close(fig)
EDIT:
One can initialize the axes by explicitly setting width_ratios and height_ratios per row/column; this is shown below. This doesn't affect the output, but maybe I'm using it incorrectly?
## initialize figure and axes
fig = plt.figure()
gs = gridspec.GridSpec(ncols=6, nrows=6, figure=fig, width_ratios=[1]*6, height_ratios=[1]*6)
ax_bottom = fig.add_subplot(gs[4:, 2:])
ax_left = fig.add_subplot(gs[:4, :2])
ax_big = fig.add_subplot(gs[:4, 2:])
The problem is with imshow, which resizes the axes automatically to maintain a square pixel aspect.
You can prevent this by calling:
ax_big.imshow(..., aspect='auto')

How do you adjust the spacing on a barchart and it's xtick labels?

I am trying to create a barchart (overlaid on a line graph with days as the x axis instead of quarters) where the labels are end-of-quarter days. That is all fine, and generates nicely, but I am trying to set the labels so that they are lined up with the right edge of the plot and the corresponding bar's right-side is aligned with the x-tick.
A reproducible example (with just the bar chart, not the line) is:
import matplotlib.pyplot as pyplot
import pandas
import random
random.seed(2020)
dates = pandas.date_range("2016-12-31", "2017-12-31")
bar = pandas.DataFrame([.02, .01, -0.01, .05], index = ["2017-03-31", "2017-06-30", "2017-09-30", "2017-12-31"], columns = ["test"])
line = pandas.DataFrame([random.random() for r in range(len(dates))], index = dates, columns = ["test"])
fig, ax = pyplot.subplots(1, 1, figsize = (7, 3))
ax2 = fig.add_subplot(111, frame_on = False)
bar.plot(kind = "bar", ax = ax, width = 1)
line.plot(kind = "line", ax = ax2)
ax2.set_xticks([])
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
fig.tight_layout()
pyplot.show()
Which yields a plot as:
My goal is to have the right side of the 2017-12-31 column aligned with the right edge of the plot and the 2017-12-31 label at the right side as well. Further, the left side of the 2017-03-31 bar touch the left side of the plot. For the remaining bars, I would like them evenly spaced with all labels aligned with the right side of each bar, and no space in between bars. Like this example below:
Frankly, I'm at a loss. I've tried adding ha="right" to no such avail and just shifting the graphs but that leaves me with other problems and doesn't really address the problem. Even with the bars shifted, I'm still fairly constrained as to moving the tick labels and haven't found anything online that remotely addresses the problem.
Would it be better to create the bar chart so that it has the same index as the line chart, then set the x tick labels to be the desired dates?
Does anyone have any guidance? I've spent too much time on this problem today and it's driving me nuts.
In order to plot the bar chart tightly, you can use the autoscale function as below.
To move the tick labels, you can modify the transformations to include some offset. Below I used 0.7 but you can select it based on other sizes used in your chart.
import matplotlib.pyplot as pyplot
import pandas
import matplotlib.transforms as tr
df = pandas.DataFrame([.02, .01, -0.01, .05], index = ["2017-03-31", "2017-06-30", "2017-09-30", "2017-12-31"], columns = ["test"])
fig, ax = pyplot.subplots(1, 1, figsize = (7, 3))
df.plot(kind = "bar", ax = ax, width = 1)
pyplot.autoscale(enable=True, axis='x', tight=True) # tight layout
# for each tick label, shift 0.7 to right
for tick in ax.get_xticklabels():
tick.set_transform(tick.get_transform()+tr.ScaledTranslation(0.7, 0, fig.dpi_scale_trans))
pyplot.show()
The result looks like this.

Fix the subplots size rather than have it derived from the main figure size [duplicate]

I want to to create a figure using matplotlib where I can explicitly specify the size of the axes, i.e. I want to set the width and height of the axes bbox.
I have looked around all over and I cannot find a solution for this. What I typically find is how to adjust the size of the complete Figure (including ticks and labels), for example using fig, ax = plt.subplots(figsize=(w, h))
This is very important for me as I want to have a 1:1 scale of the axes, i.e. 1 unit in paper is equal to 1 unit in reality. For example, if xrange is 0 to 10 with major tick = 1 and x axis is 10cm, then 1 major tick = 1cm. I will save this figure as pdf to import it to a latex document.
This question brought up a similar topic but the answer does not solve my problem (using plt.gca().set_aspect('equal', adjustable='box') code)
From this other question I see that it is possible to get the axes size, but not how to modify them explicitly.
Any ideas how I can set the axes box size and not just the figure size. The figure size should adapt to the axes size.
Thanks!
For those familiar with pgfplots in latex, it will like to have something similar to the scale only axis option (see here for example).
The axes size is determined by the figure size and the figure spacings, which can be set using figure.subplots_adjust(). In reverse this means that you can set the axes size by setting the figure size taking into acount the figure spacings:
import matplotlib.pyplot as plt
def set_size(w,h, ax=None):
""" w, h: width, height in inches """
if not ax: ax=plt.gca()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
figw = float(w)/(r-l)
figh = float(h)/(t-b)
ax.figure.set_size_inches(figw, figh)
fig, ax=plt.subplots()
ax.plot([1,3,2])
set_size(5,5)
plt.show()
It appears that Matplotlib has helper classes that allow you to define axes with a fixed size Demo fixed size axes
I have found that ImportanceofBeingErnests answer which modifies that figure size to adjust the axes size provides inconsistent results with the paticular matplotlib settings I use to produce publication ready plots. Slight errors were present in the final figure size, and I was unable to find a way to solve the issue with his approach. For most use cases I think this is not a problem, however the errors were noticeable when combining multiple pdf's for publication.
In lieu of developing a minimum working example to find the real issue I am having with the figure resizing approach I instead found a work around which uses the fixed axes size utilising the divider class.
from mpl_toolkits.axes_grid1 import Divider, Size
def fix_axes_size_incm(axew, axeh):
axew = axew/2.54
axeh = axeh/2.54
#lets use the tight layout function to get a good padding size for our axes labels.
fig = plt.gcf()
ax = plt.gca()
fig.tight_layout()
#obtain the current ratio values for padding and fix size
oldw, oldh = fig.get_size_inches()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
#work out what the new ratio values for padding are, and the new fig size.
neww = axew+oldw*(1-r+l)
newh = axeh+oldh*(1-t+b)
newr = r*oldw/neww
newl = l*oldw/neww
newt = t*oldh/newh
newb = b*oldh/newh
#right(top) padding, fixed axes size, left(bottom) pading
hori = [Size.Scaled(newr), Size.Fixed(axew), Size.Scaled(newl)]
vert = [Size.Scaled(newt), Size.Fixed(axeh), Size.Scaled(newb)]
divider = Divider(fig, (0.0, 0.0, 1., 1.), hori, vert, aspect=False)
# the width and height of the rectangle is ignored.
ax.set_axes_locator(divider.new_locator(nx=1, ny=1))
#we need to resize the figure now, as we have may have made our axes bigger than in.
fig.set_size_inches(neww,newh)
Things worth noting:
Once you call set_axes_locator() on an axis instance you break the tight_layout() function.
The original figure size you choose will be irrelevent, and the final figure size is determined by the axes size you choose and the size of the labels/tick labels/outward ticks.
This approach doesn't work with colour scale bars.
This is my first ever stack overflow post.
another method using fig.add_axes was quite accurate. I have included 1 cm grid aswell
import matplotlib.pyplot as plt
import matplotlib as mpl
# This example fits a4 paper with 5mm margin printers
# figure settings
figure_width = 28.7 # cm
figure_height = 20 # cm
left_right_magrin = 1 # cm
top_bottom_margin = 1 # cm
# Don't change
left = left_right_magrin / 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
# specifying the width and the height of the box in inches
fig = plt.figure(figsize=(figure_width*cm2inch,figure_height*cm2inch))
ax = fig.add_axes((left, bottom, width, height))
# limits settings (important)
plt.xlim(0, figure_width * width)
plt.ylim(0, figure_height * height)
# Ticks settings
ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(5))
ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(5))
ax.yaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))
# Grid settings
ax.grid(color="gray", which="both", linestyle=':', linewidth=0.5)
# your Plot (consider above limits)
ax.plot([1,2,3,5,6,7,8,9,10,12,13,14,15,17])
# save figure ( printing png file had better resolution, pdf was lighter and better on screen)
plt.show()
fig.savefig('A4_grid_cm.png', dpi=1000)
fig.savefig('tA4_grid_cm.pdf')
result:

Matplotlib - sequentially creating figures with the same size

I need to create a sequence of .pdf files where each .pdf contains a figure with five plots.
As I am going to include them in a LaTeX article, I wanted them all to be the same width and height so that each figure's corners are vertically aligned on both left and right sides.
I thought this would be enough, but apparently not:
common_figsize=(6,5)
fig, ax = plt.subplots(figsize = common_figsize)
# five plots in a loop for the first figure.
# my_code()...
plt.savefig("Figure-1.pdf", transparent=True)
plt.close(fig)
fig, ax = plt.subplots(figsize = common_figsize)
# five plots in a loop for the new figure.
# my_code()...
plt.savefig("Figure-2.pdf", transparent=True)
plt.close(fig)
If I understand correctly, this does not do exactly what I want because of different scales originating from different yticks resolutions.
For both figures, pyplot is fed the same list for xticks.
In this case, it is a list of 50 values, from 1 to 50.
CHUNK_COUNT = 50
x_step = CHUNK_COUNT / 10
new_xticks = list(range(x_step, CHUNK_COUNT + x_step, x_step)) + [1]
plt.xticks(new_xticks)
ax.set_xlim(left=1, right=CHUNK_COUNT)
This creates both figures with an X-axis that goes from 1 to 50.
So far so good.
However, I haven't figured out how to deal with the problem of yticks resolution.
One of the figures had less yticks than the other, so I overrode it to have as many ticks as the other:
# Add yticks to Figure 1.
y_divisor = 6
y_step = (100 - min_y_tick) / y_divisor
new_yticks = [min_y_tick + y_step * i for i in range(0, y_divisor + 1)]
plt.yticks(new_yticks)
This resulted in the following images:
(click on each to open in new tab to see that in fact the bounding square of each figure is different)
Figure 1:
Figure 2:
In summary, I believe matplotlib is accepting the figsize parameter, but then rearranges plot elements to accommodate for different tick values and text lengths.
Is it possible for it to operate in reverse? To change label and text rotations automagically so that the squares are absolutely the same length and height?
Apologies if this is a duplicate and thanks for the help.
EDIT:
Finally able to provide a minimal, complete and verifiable example.
Among the tests, I removed the custom yticks code and the problem still persists:
from matplotlib.lines import Line2D
import matplotlib.ticker as mtick
import matplotlib.pyplot as plt
from matplotlib import rc
# activate latex text rendering
rc('text', usetex=True)
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})
CHUNK_COUNT = 50
common_figsize=(6,5)
plot_counter = 5
x_step = int(int(CHUNK_COUNT) / 10)
new_xticks = list(range(x_step, int(CHUNK_COUNT) + x_step, x_step)) + [1]
##### Plot Figure 1
fig, ax = plt.subplots(figsize = common_figsize)
plt.ylabel("Summary of a simple YY axis")
plt.yticks(rotation=45)
ax.yaxis.set_major_formatter(mtick.PercentFormatter(is_latex=False))
for i in range(0, plot_counter):
xvals = range(1, CHUNK_COUNT + 1)
yvals = []
for j in xvals:
yvals.append(j + i)
plt.plot(xvals, yvals)
plt.xticks(new_xticks)
ax.set_xlim(left=1, right=int(CHUNK_COUNT))
plt.savefig("Figure_1.png", transparent=True)
plt.close(fig)
##### Plot Figure 2
fig, ax = plt.subplots(figsize = common_figsize)
plt.ylabel("Summary of another YY axis")
plt.yticks(rotation=45)
ax.yaxis.set_major_formatter(mtick.PercentFormatter(is_latex=False))
for i in range(0, plot_counter):
xvals = range(1, CHUNK_COUNT + 1)
yvals = []
for j in xvals:
yvals.append((j + i) / 100)
plt.plot(xvals, yvals)
plt.xticks(new_xticks)
ax.set_xlim(left=1, right=int(CHUNK_COUNT))
plt.savefig("Figure_2.png", transparent=True)
plt.close(fig)
It turns out this was due to a mistake on my part.
I carried over code from another context where
autolayout
was active:
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})
After setting it to False, the figure squares all had the same dimensions:
from matplotlib import rcParams
rcParams.update({'figure.autolayout': False})
Despite the length differences in ytick elements, it is now respecting the dimensions specified in my original question.
These results were generated with the MWE example I added at the end of my question:

Resources