Plotting series of images using matplot - python

I am trying to find an efficient way to draw an arbitrary number of images in a series inside a figure in Python using matplotlib.
This is what I want to achieve:
Each image will have a different width but the same height. Of course, we can use subplot but the problem with subplot images must have the same width plus the issue of spacing between different columns.
If I need to use another library/package to achieve that that is totally fine for me.
Notes:
The borders around each image should not exist but it is just for illustration.
I want to consider the figure as an area where I can plot images in any position and add text in a specific location.

You can use gridspec and specifying left, right, bottom, top, width_ratios and height_ratios to generate subplots in a very flexible way. Pls see doc for details.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
nrows=2
ncols=2
fig = plt.figure(figsize=(7.2, 7.2))
gs = fig.add_gridspec(nrows=nrows, ncols=ncols, left=0.1, right=0.6, bottom=0.55, top=0.95, width_ratios=[1, 2], height_ratios=[1, 1])
axes = [fig.add_subplot(gs[row, col]) for row in range(nrows) for col in range(ncols)]

Related

How do I position the axis frame inside a figure without changing the size of the figure? [Python, matplotlib]

I'm trying to create a video of many figures, so I need the axis to remain steady across multiple, independent figures. However, the y-axis changes scale, so the framing of the axis keeps moving as the ticklabels change. I'm trying to manually tell matplotlib exactly what size the whole figure should be and tell it exactly the position of the axis within the figure, but it's not working properly.
Here's what a base figure looks like:
import matplotlib.pyplot as plt
fig=plt.figure(figsize=(8,4),facecolor=(0.5,0.5,0.5))
ax=fig.add_subplot()
ax.plot([5,10],[800,900])
plt.show()
Here is one way for how I'm trying to change it if I want the axis frame to start at left=0.5, bottom=0.5, width=0.2, and height=0.2. I've tried many different ways, and all have failed, so this is illustrative of what I'm trying to do:
fig=plt.figure(figsize=(8,4),facecolor=(0.5,0.5,0.5))
ax=fig.add_axes((0.5,0.5,0.2,0.2))
ax.plot([5,10],[800,900])
plt.show()
Now, I want it to look more like this so that the black box of the axis frame will be in the exact same position for every figure, and each figure will be the exact same size. That way, when I make it an animation, the black frame won't be jerking around. (Obviously, I wouldn't make the buffer that big in the real video.)
You need to use ax.set_position.
If your ax box initially occupies the full figure, you can create a new size relatively to the old one, for example:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 4), facecolor=(0.5, 0.5, 0.5))
ax = fig.add_subplot(111)
bbox = ax.get_position()
new_bbox = (bbox.x0+0.40, bbox.y0+0.40, bbox.width*0.5, bbox.height*0.5)
ax.set_position(new_bbox)
ax.plot([5, 10], [800, 900])
plt.show()

How to position suptitle

I'm trying to adjust a suptitle above a multi-panel figure and am having trouble figuring out how to adjust the figsize and subsequently position the suptitle.
The problem is that calling plt.suptitle("my title", y=...) to adjust the position of the suptitle also adjusts the figure dimensions. A few questions:
where does suptitle(..., y=1.1) actually put the title? As far as I can tell, the documentation for the y parameter of suptitle points to matplotlib.text.Text, but I don't know what figure coordinates mean when you have multiple subplots.
what is the effect on figure size when specifying y to suptitle?
how do I manually adjust figure size and spacing (subplots_adjust?) to add a figure title per panel and a suptitle for the entire figure, maintaining the size of each ax in the figure?
An example:
data = np.random.random(size=100)
f, a = plt.subplots(2, 2, figsize=(10, 5))
a[0,0].plot(data)
a[0,0].set_title("this is a really long title\n"*2)
a[0,1].plot(data)
a[1,1].plot(data)
plt.suptitle("a big long suptitle that runs into the title\n"*2, y=1.05);
Obviously I can tweak y each time I make a figure, but I need a solution that generally works without manual intervention. I've tried both constrained layout and tight layout; neither works reliably with figures of any complexity.
1. What do figure coordinates mean?
Figure coordinates go 0 to 1, where (0,0) is the lower left corner and (1,1) is the upper right corner. A coordinate of y=1.05 is hence slightly outside the figure.
2. what is the effect on figure size when specifying y to suptitle?
Specifying y to suptitle has no effect whatsoever on the figure size.
3a. How do I manually adjust figure size and spacing to add a figure title per panel and a suptitle for the entire figure?
First, one would not add an additional linebreak. I.e. if you want to have 2 lines, don't use 3 linebreaks (\n). Then one can adjust the subplot parameters as desired to leave space for the titles. E.g. fig.subplots_adjust(top=0.8) and use a y <= 1 for the title to be inside the figure.
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random(size=100)
fig, axes = plt.subplots(2, 2, figsize=(10, 5))
fig.subplots_adjust(top=0.8)
axes[0,0].plot(data)
axes[0,0].set_title("\n".join(["this is a really long title"]*2))
axes[0,1].plot(data)
axes[1,1].plot(data)
fig.suptitle("\n".join(["a big long suptitle that runs into the title"]*2), y=0.98)
plt.show()
3b. ... while maintaining the size of each ax in the figure?
Maintaining the size of the axes and still have enough space for the titles is only possible by changing the overall figure size.
This could look as follows, where we define a function make_space_above which takes the array of axes as input, as well as the newly desired top margin in units of inches. So for example, you come to the conclusion that you need 1 inch of margin on top to host your titles:
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random(size=100)
fig, axes = plt.subplots(2, 2, figsize=(10, 5), squeeze = False)
axes[0,0].plot(data)
axes[0,0].set_title("\n".join(["this is a really long title"]*2))
axes[0,1].plot(data)
axes[1,1].plot(data)
fig.suptitle("\n".join(["a big long suptitle that runs into the title"]*2), y=0.98)
def make_space_above(axes, topmargin=1):
""" increase figure size to make topmargin (in inches) space for
titles, without changing the axes sizes"""
fig = axes.flatten()[0].figure
s = fig.subplotpars
w, h = fig.get_size_inches()
figh = h - (1-s.top)*h + topmargin
fig.subplots_adjust(bottom=s.bottom*h/figh, top=1-topmargin/figh)
fig.set_figheight(figh)
make_space_above(axes, topmargin=1)
plt.show()
(left: without calling make_space_above; right: with call to make_space_above(axes, topmargin=1))
Short Answer
For those coming from Google for adjusting the title position on a scatter matrix, you can simply set the y parameter to a value slightly lower than 1:
plt.suptitle('My Title', y=0.92)
... or use constrained_layout:
import matplotlib.pyplot as plt
import numpy as np
data = np.random.random(size=100)
f, a = plt.subplots(2, 2, figsize=(10, 5), constrained_layout=True)
a[0,0].plot(data)
a[0,0].set_title("this is a really long title\n"*2)
a[0,1].plot(data)
a[1,1].plot(data)
plt.suptitle("a big long suptitle that runs into the title\n"*2);
A bit of a hacky solution, but if your plots only have 1 column, perhaps consider just add the main title to the title of the first plot, like so:
ax[0].set_title("Main Title\nFirst Plot")

How to ensure even spacing between labels on x axis of matplotlib graph?

I have been given a data for which I need to find a histogram. So I used pandas hist() function and plot it using matplotlib. The code runs on a remote server so I cannot directly see it and hence I save the image. Here is what the image looks like
Here is my code below
import matplotlib.pyplot as plt
df_hist = pd.DataFrame(np.array(raw_data)).hist(bins=5) // raw_data is the data supplied to me
plt.savefig('/path/to/file.png')
plt.close()
As you can see the x axis labels are overlapping. So I used this function plt.tight_layout() like so
import matplotlib.pyplot as plt
df_hist = pd.DataFrame(np.array(raw_data)).hist(bins=5)
plt.tight_layout()
plt.savefig('/path/to/file.png')
plt.close()
There is some improvement now
But still the labels are too close. Is there a way to ensure the labels do not touch each other and there is fair spacing between them? Also I want to resize the image to make it smaller.
I checked the documentation here https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html but not sure which parameter to use for savefig.
Since raw_data is not already a pandas dataframe there's no need to turn it into one to do the plotting. Instead you can plot directly with matplotlib.
There are many different ways to achieve what you'd like. I'll start by setting up some data which looks similar to yours:
import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import gamma
raw_data = gamma.rvs(a=1, scale=1e6, size=100)
If we go ahead and use matplotlib to create the histogram we may find the xticks too close together:
fig, ax = plt.subplots(1, 1, figsize=[5, 3])
ax.hist(raw_data, bins=5)
fig.tight_layout()
The xticks are hard to read with all the zeros, regardless of spacing. So, one thing you may wish to do would be to use scientific formatting. This makes the x-axis much easier to interpret:
ax.ticklabel_format(style='sci', axis='x', scilimits=(0,0))
Another option, without using scientific formatting would be to rotate the ticks (as mentioned in the comments):
ax.tick_params(axis='x', rotation=45)
fig.tight_layout()
Finally, you also mentioned altering the size of the image. Note that this is best done when the figure is initialised. You can set the size of the figure with the figsize argument. The following would create a figure 5" wide and 3" in height:
fig, ax = plt.subplots(1, 1, figsize=[5, 3])
I think the two best fixes were mentioned by Pam in the comments.
You can rotate the labels with
plt.xticks(rotation=45
For more information, look here: Rotate axis text in python matplotlib
The real problem is too many zeros that don't provide any extra info. Numpy arrays are pretty easy to work with, so pd.DataFrame(np.array(raw_data)/1000).hist(bins=5) should get rid of three zeros off of both axes. Then just add a 'kilo' in the axes labels.
To change the size of the graph use rcParams.
from matplotlib import rcParams
rcParams['figure.figsize'] = 7, 5.75 #the numbers are the dimensions

Save figure with clip box from another figure

Normally if you plot two different figures using the default settings in pyplot, they will be exactly the same size, and if saved can be neatly aligned in PowerPoint or the like. I'd like to generate one figure, however, which has a legend outside of the figure. The script I'm using is shown below.
import numpy as np
import matplotlib.pyplot as plt
x=np.linspace(0,1,201)
y1=x**2
y2=np.sin(x)
fig1=plt.figure(1)
plt.plot(x,y1,label='y1')
handles1,labels1=plt.gca().get_legend_handles_labels()
lgd1=plt.gca().legend(handles1,labels1,bbox_to_anchor=(1.27,1),borderaxespad=0.)
fig2=plt.figure(2)
plt.plot(x,y2)
fig1.savefig('fig1',bbox_extra_artists=(lgd1,),bbox_inches='tight')
fig2.savefig('fig2')
plt.show()
The problem is that in PowerPoint, I can no longer align the two figures left and have their axes aligned. Due to the use of the 'extra artists' and 'bbox_inches=tight' arguments for the first figure, the width of its margins becomes different from the second figure.
Is there any way to 'transfer' the clip box from the first figure to the second figure, such that they can be aligned by 'align left' in PowerPoint?
I think an easier way to achieve what you want is to just construct one figure with two subplots, and let matplotlib align everything for you.
Do you think doing something like this is a good idea?
import matplotlib.pyplot as plt
import numpy as np
x=np.linspace(0,1,201)
y1=x**2
y2=np.sin(x)
fig = plt.figure()
a = fig.add_subplot(211)
a.plot(x,y1, label='y1')
lgd1 = a.legend(bbox_to_anchor = (1.27,1), borderaxespad=0.)
a = fig.add_subplot(212)
a.plot(x,y2)
fig.savefig('fig',bbox_extra_artists=(lgd1,),bbox_inches='tight')

Subplots: tight_layout changes figure size

Changing the vertical distance between two subplot using tight_layout(h_pad=-1) changes the total figuresize. How can I define the figuresize using tight_layout?
Here is the code:
#define figure
pl.figure(figsize=(10, 6.25))
ax1=subplot(211)
img=pl.imshow(np.random.random((10,50)), interpolation='none')
ax1.set_xticklabels(()) #hides the tickslabels of the first plot
subplot(212)
x=linspace(0,50)
pl.plot(x,x,'k-')
xlim( ax1.get_xlim() ) #same x-axis for both plots
And here is the results:
If I write
pl.tight_layout(h_pad=-2)
in the last line, then I get this:
As you can see, the figure is bigger...
You can use a GridSpec object to control precisely width and height ratios, as answered on this thread and documented here.
Experimenting with your code, I could produce something like what you want, by using a height_ratio that assigns twice the space to the upper subplot, and increasing the h_pad parameter to the tight_layout call. This does not sound completely right, but maybe you can adjust this further ...
import numpy as np
from matplotlib.pyplot import *
import matplotlib.pyplot as pl
import matplotlib.gridspec as gridspec
#define figure
fig = pl.figure(figsize=(10, 6.25))
gs = gridspec.GridSpec(2, 1, height_ratios=[2,1])
ax1=subplot(gs[0])
img=pl.imshow(np.random.random((10,50)), interpolation='none')
ax1.set_xticklabels(()) #hides the tickslabels of the first plot
ax2=subplot(gs[1])
x=np.linspace(0,50)
ax2.plot(x,x,'k-')
xlim( ax1.get_xlim() ) #same x-axis for both plots
fig.tight_layout(h_pad=-5)
show()
There were other issues, like correcting the imports, adding numpy, and plotting to ax2 instead of directly with pl. The output I see is this:
This case is peculiar because of the fact that the default aspect ratios of images and plots are not the same. So it is worth noting for people looking to remove the spaces in a grid of subplots consisting of images only or of plots only that you may find an appropriate solution among the answers to this question (and those linked to it): How to remove the space between subplots in matplotlib.pyplot?.
The aspect ratios of the subplots in this particular example are as follows:
# Default aspect ratio of images:
ax1.get_aspect()
# 1.0
# Which is as it is expected based on the default settings in rcParams file:
matplotlib.rcParams['image.aspect']
# 'equal'
# Default aspect ratio of plots:
ax2.get_aspect()
# 'auto'
The size of ax1 and the space beneath it are adjusted automatically based on the number of pixels along the x-axis (i.e. width) so as to preserve the 'equal' aspect ratio while fitting both subplots within the figure. As you mentioned, using fig.tight_layout(h_pad=xxx) or the similar fig.set_constrained_layout_pads(hspace=xxx) is not a good option as this makes the figure larger.
To remove the gap while preserving the original figure size, you can use fig.subplots_adjust(hspace=xxx) or the equivalent plt.subplots(gridspec_kw=dict(hspace=xxx)), as shown in the following example:
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
np.random.seed(1)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6.25),
gridspec_kw=dict(hspace=-0.206))
# For those not using plt.subplots, you can use this instead:
# fig.subplots_adjust(hspace=-0.206)
size = 50
ax1.imshow(np.random.random((10, size)))
ax1.xaxis.set_visible(False)
# Create plot of a line that is aligned with the image above
x = np.arange(0, size)
ax2.plot(x, x, 'k-')
ax2.set_xlim(ax1.get_xlim())
plt.show()
I am not aware of any way to define the appropriate hspace automatically so that the gap can be removed for any image width. As stated in the docstring for fig.subplots_adjust(), it corresponds to the height of the padding between subplots, as a fraction of the average axes height. So I attempted to compute hspace by dividing the gap between the subplots by the average height of both subplots like this:
# Extract axes positions in figure coordinates
ax1_x0, ax1_y0, ax1_x1, ax1_y1 = np.ravel(ax1.get_position())
ax2_x0, ax2_y0, ax2_x1, ax2_y1 = np.ravel(ax2.get_position())
# Compute negative hspace to close the vertical gap between subplots
ax1_h = ax1_y1-ax1_y0
ax2_h = ax2_y1-ax2_y0
avg_h = (ax1_h+ax2_h)/2
gap = ax1_y0-ax2_y1
hspace=-(gap/avg_h) # this divided by 2 also does not work
fig.subplots_adjust(hspace=hspace)
Unfortunately, this does not work. Maybe someone else has a solution for this.
It is also worth mentioning that I tried removing the gap between subplots by editing the y positions like in this example:
# Extract axes positions in figure coordinates
ax1_x0, ax1_y0, ax1_x1, ax1_y1 = np.ravel(ax1.get_position())
ax2_x0, ax2_y0, ax2_x1, ax2_y1 = np.ravel(ax2.get_position())
# Set new y positions: shift ax1 down over gap
gap = ax1_y0-ax2_y1
ax1.set_position([ax1_x0, ax1_y0-gap, ax1_x1, ax1_y1-gap])
ax2.set_position([ax2_x0, ax2_y0, ax2_x1, ax2_y1])
Unfortunately, this (and variations of this) produces seemingly unpredictable results, including a figure resizing similar to when using fig.tight_layout(). Maybe someone else has an explanation for what is happening here behind the scenes.

Categories