Save a subplot in matplotlib - python

Is it possible to save (to a png) an individual subplot in a matplotlib figure? Let's say I have
import pyplot.matplotlib as plt
ax1 = plt.subplot(121)
ax2 = plt.subplot(122)
ax1.plot([1,2,3],[4,5,6])
ax2.plot([3,4,5],[7,8,9])
Is it possible to save each of the two subplots to different files or at least copy them separately to a new figure to save them?
I am using version 1.0.0 of matplotlib on RHEL 5.

While #Eli is quite correct that there usually isn't much of a need to do it, it is possible. savefig takes a bbox_inches argument that can be used to selectively save only a portion of a figure to an image.
Here's a quick example:
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
# Make an example plot with two subplots...
fig = plt.figure()
ax1 = fig.add_subplot(2,1,1)
ax1.plot(range(10), 'b-')
ax2 = fig.add_subplot(2,1,2)
ax2.plot(range(20), 'r^')
# Save the full figure...
fig.savefig('full_figure.png')
# Save just the portion _inside_ the second axis's boundaries
extent = ax2.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
fig.savefig('ax2_figure.png', bbox_inches=extent)
# Pad the saved area by 10% in the x-direction and 20% in the y-direction
fig.savefig('ax2_figure_expanded.png', bbox_inches=extent.expanded(1.1, 1.2))
The full figure:
Area inside the second subplot:
Area around the second subplot padded by 10% in the x-direction and 20% in the y-direction:

Applying the full_extent() function in an answer by #Joe 3 years later from here, you can get exactly what the OP was looking for. Alternatively, you can use Axes.get_tightbbox() which gives a little tighter bounding box
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
from matplotlib.transforms import Bbox
def full_extent(ax, pad=0.0):
"""Get the full extent of an axes, including axes labels, tick labels, and
titles."""
# For text objects, we need to draw the figure first, otherwise the extents
# are undefined.
ax.figure.canvas.draw()
items = ax.get_xticklabels() + ax.get_yticklabels()
# items += [ax, ax.title, ax.xaxis.label, ax.yaxis.label]
items += [ax, ax.title]
bbox = Bbox.union([item.get_window_extent() for item in items])
return bbox.expanded(1.0 + pad, 1.0 + pad)
# Make an example plot with two subplots...
fig = plt.figure()
ax1 = fig.add_subplot(2,1,1)
ax1.plot(range(10), 'b-')
ax2 = fig.add_subplot(2,1,2)
ax2.plot(range(20), 'r^')
# Save the full figure...
fig.savefig('full_figure.png')
# Save just the portion _inside_ the second axis's boundaries
extent = full_extent(ax2).transformed(fig.dpi_scale_trans.inverted())
# Alternatively,
# extent = ax.get_tightbbox(fig.canvas.renderer).transformed(fig.dpi_scale_trans.inverted())
fig.savefig('ax2_figure.png', bbox_inches=extent)
I'd post a pic but I lack the reputation points

Related

Saving with bbox_inches set to 'tight' changes the display coordinates [duplicate]

Instead of words or numbers being the tick labels of the x axis, I want to draw a simple drawing (made of lines and circles) as the label for each x tick. Is this possible? If so, what is the best way to go about it in matplotlib?
I would remove the tick labels and replace the text with patches. Here is a brief example of performing this task:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define where to put symbols vertically
TICKYPOS = -.6
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(range(10))
# set ticks where your images will be
ax.get_xaxis().set_ticks([2,4,6,8])
# remove tick labels
ax.get_xaxis().set_ticklabels([])
# add a series of patches to serve as tick labels
ax.add_patch(patches.Circle((2,TICKYPOS),radius=.2,
fill=True,clip_on=False))
ax.add_patch(patches.Circle((4,TICKYPOS),radius=.2,
fill=False,clip_on=False))
ax.add_patch(patches.Rectangle((6-.1,TICKYPOS-.05),.2,.2,
fill=True,clip_on=False))
ax.add_patch(patches.Rectangle((8-.1,TICKYPOS-.05),.2,.2,
fill=False,clip_on=False))
This results in the following figure:
It is key to set clip_on to False, otherwise patches outside the axes will not be shown. The coordinates and sizes (radius, width, height, etc.) of the patches will depend on where your axes is in the figure. For example, if you are considering doing this with subplots, you will need to be sensitive of the patches placement so as to not overlap any other axes. It may be worth your time investigating Transformations, and defining the positions and sizes in an other unit (Axes, Figure or display).
If you have specific image files that you want to use for the symbols, you can use the BboxImage class to create artists to be added to the axes instead of patches. For example I made a simple icon with the following script:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(1,1),dpi=400)
ax = fig.add_axes([0,0,1,1],frameon=False)
ax.set_axis_off()
ax.plot(range(10),linewidth=32)
ax.plot(range(9,-1,-1),linewidth=32)
fig.savefig('thumb.png')
producing this image:
Then I created a BboxImage at the location I want the tick label and of the size I want:
lowerCorner = ax.transData.transform((.8,TICKYPOS-.2))
upperCorner = ax.transData.transform((1.2,TICKYPOS+.2))
bbox_image = BboxImage(Bbox([lowerCorner[0],
lowerCorner[1],
upperCorner[0],
upperCorner[1],
]),
norm = None,
origin=None,
clip_on=False,
)
Noticed how I used the transData transformation to convert from data units to display units, which are required in the definition of the Bbox.
Now I read in the image using the imread routine, and set it's results (a numpy array) to the data of bbox_image and add the artist to the axes:
bbox_image.set_data(imread('thumb.png'))
ax.add_artist(bbox_image)
This results in an updated figure:
If you do directly use images, make sure to import the required classes and methods:
from matplotlib.image import BboxImage,imread
from matplotlib.transforms import Bbox
The other answer has some drawbacks because it uses static coordinates. It will hence not work when changing the figure size or zooming and panning the plot.
A better option is to directly define the positions in the coordinate systems of choice. For the xaxis it makes sense to use data coordinates for the x position and axes coordinates for y position.
Using matplotlib.offsetboxes makes this rather simple. The following would position a box with a circle and a box with an image at coordinates (-5,0) and (5,0) respectively and offsets them a bit to the lower such that they'll look as if they were ticklabels.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.offsetbox import (DrawingArea, OffsetImage,AnnotationBbox)
fig, ax = plt.subplots()
ax.plot([-10,10], [1,3])
# Annotate the 1st position with a circle patch
da = DrawingArea(20, 20, 10, 10)
p = mpatches.Circle((0, 0), 10)
da.add_artist(p)
ab = AnnotationBbox(da, (-5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
box_alignment=(.5, 1),
boxcoords="offset points",
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
# Annotate the 2nd position with an image
arr_img = plt.imread("https://i.stack.imgur.com/FmX9n.png", format='png')
imagebox = OffsetImage(arr_img, zoom=0.2)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, (5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
boxcoords="offset points",
box_alignment=(.5, 1),
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
plt.show()
Note that many shapes exist as unicode symbols, such that one can simply set the ticklabels with those symbols. For such a solution, see How to use a colored shape as yticks in matplotlib or seaborn?

Controlling legend across multiple subplots with windrose axes

I cannot figure out how to make the legends not overlap with my figures (see below figure) in subplots. The problem is my axes are complicated because they are from a windrose. To get the axes:
1) I have downloaded the windrose.py from https://github.com/akrherz/windrose/tree/darylchanges
2) I copied the windrose.py into the same path with my python script, example.py
3) I changed windrose.py so that it is able to do subplots, according to the steps from Subplot of Windrose in matplotlib . Those steps were to make WindroseAxes as a projection into matplotlib. I edited the file windrose.py:
3a) Include an
import from matplotlib.projections import register_projection
at the beginning of the file.
3b) Then add a name variable :
class WindroseAxes(PolarAxes):
name = 'windrose'
...
3c) Finally, at the end of windrose.py, you add:
register_projection(WindroseAxes)
Once that is done, you can easily create your windrose axes using the projection argument to the matplotlib axes.
4) Now I ran my script below (example of my real script)
from windrose import WindroseAxes
import numpy as np
import matplotlib.pyplot as plt
from windrose_subplot import WindroseAxes
wind_speeds1 = np.array([12,10,13,15])
wind_dirs1 = np.array([60,76,32,80]) # in degrees
wind_speeds2 = np.array([23,12,10,8])
wind_dirs2 = np.array([23,45,29,13])
fig = plt.figure()
ax1 = fig.add_subplot(231,projection='windrose')
ax1.bar(wind_dirs1,wind_speeds1,normed=True,opening=0.8,edgecolor='white')
ax2 = fig.add_subplot(232,projection='windrose')
ax2.bar(wind_dirs2,wind_speeds2,normed=True,opening=0.8,edgecolor='white')
ax1.legend()
ax2.legend()
plt.tight_layout()
plt.show()
Ideally, I would like to create one legend with the max/min of all the subplots because they are all the same units . This legend will have to be the corresponding colors for each subplot for the same values across subplots (eg, a single normal legend relevant to all subplots). There will be 6 subplots in the real script but 2 here for now shows the point.
This is simple to fix. In order to only plot one legend, comment out or delete where you plot the first legend. In order to move the legend off of the plot, use bbox_to_anchor=() with some logical location. See below for an example that works for this example.
import numpy as np
import matplotlib.pyplot as plt
from windrose_subplot import WindroseAxes
wind_speeds1 = np.array([12,10,13,15])
wind_dirs1 = np.array([60,76,32,80]) # in degrees
wind_speeds2 = np.array([23,12,10,8])
wind_dirs2 = np.array([23,45,29,13])
fig = plt.figure()
ax1 = fig.add_subplot(231,projection='windrose')
ax1.bar(wind_dirs1,wind_speeds1,normed=True,opening=0.8,edgecolor='white')
ax2 = fig.add_subplot(232,projection='windrose')
ax2.bar(wind_dirs2,wind_speeds2,normed=True,opening=0.8,edgecolor='white')
# ax1.legend()
ax2.legend(bbox_to_anchor=(1.2 , -0.1))
plt.tight_layout()
plt.show()
However, note the bbox_to_anchor is reliant on the axis that the legend comes from, so
ax1.legend(bbox_to_anchor=1.2, -0.1))
#ax2.legend()
would display the legend underneath the second axis:
Thank you Hazard11, I found your answer very useful :) There is an issue with the answer though is the legend does not represent the first subplot because the bins are generated when creating the second subplot.
I just solved this issue by calculating the bins using numpy.histogram first and then passing that to windrose.WindroseAxes.bar() when creating each wind rose. Doing it this way means you need to pick which one you want to use to generate the bins. Another way to do it would be to define the bins manually or to create a function which generates some efficient binning for both which could then be used.
wind_speeds1 = np.array([12,10,13,15])
wind_dirs1 = np.array([60,76,32,80]) # in degrees
wind_speeds2 = np.array([23,12,10,8])
wind_dirs2 = np.array([23,45,29,13])
wind_speeds_bins = np.histogram(wind_speeds2, 5)[1]
fig = plt.figure()
ax1 = fig.add_subplot(231, projection='windrose')
ax1.bar(wind_dirs1 ,wind_speeds1, normed=True, opening=0.8, edgecolor='white', bins=wind_speeds_bins)
ax2 = fig.add_subplot(232, projection='windrose')
ax2.bar(wind_dirs2, wind_speeds2, normed=True, opening=0.8, edgecolor='white', bins=wind_speeds_bins)
# ax1.legend()
ax2.legend(bbox_to_anchor=(1.2 , -0.1))
plt.tight_layout()
plt.show()

adjust matplotlib subplot spacing after tight_layout

I would like to minimize white space in my figure. I have a row of sub plots where four plots share their y-axis and the last plot has a separate axis.
There are no ylabels or ticklabels for the shared axis middle panels.
tight_layout creates a lot of white space between the the middle plots as if leaving space for tick labels and ylabels but I would rather stretch the sub plots. Is this possible?
import matplotlib.gridspec as gridspec
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
fig = plt.figure()
gs = gridspec.GridSpec(1, 5, width_ratios=[4,1,4,1,2])
ax = fig.add_subplot(gs[0])
axes = [ax] + [fig.add_subplot(gs[i], sharey=ax) for i in range(1, 4)]
axes[0].plot(np.random.randint(0,100,100))
barlist=axes[1].bar([1,2],[1,20])
axes[2].plot(np.random.randint(0,100,100))
barlist=axes[3].bar([1,2],[1,20])
axes[0].set_ylabel('data')
axes.append(fig.add_subplot(gs[4]))
axes[4].plot(np.random.randint(0,5,100))
axes[4].set_ylabel('other data')
for ax in axes[1:4]:
plt.setp(ax.get_yticklabels(), visible=False)
sns.despine();
plt.tight_layout(pad=0, w_pad=0, h_pad=0);
Setting w_pad = 0 is not changing the default settings of tight_layout. You need to set something like w_pad = -2. Which produces the following figure:
You could go further, to say -3 but then you would start to get some overlap with your last plot.
Another way could be to remove plt.tight_layout() and set the boundaries yourself using
plt.subplots_adjust(left=0.065, right=0.97, top=0.96, bottom=0.065, wspace=0.14)
Though this can be a bit of a trial and error process.
Edit
A nice looking graph can be achieved by moving the ticks and the labels of the last plot to the right hand side. This answer shows you can do this by using:
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
So for your example:
axes[4].yaxis.tick_right()
axes[4].yaxis.set_label_position("right")
In addition, you need to remove sns.despine(). Finally, there is now no need to set w_pad = -2, just use plt.tight_layout(pad=0, w_pad=0, h_pad=0)
Using this creates the following figure:

Python: subplots with different total sizes

Original Post
I need to make several subplots with different sizes.
I have simulation areas of the size (x y) 35x6µm to 39x2µm and I want to plot them in one figure. All subplots have the same x-ticklabels (there is a grid line every 5µm on the x-axis).
When I plot the subplots into one figure, then the graphs with the small x-area are streched, so that the x-figuresize is completely used. Therefore, the x-gridlines do not match together anymore.
How can I achieve that the subplots aren't streched anymore and are aligned on the left?
Edit: Here is some code:
size=array([[3983,229],[3933,350],[3854,454],[3750,533],[3500,600]], dtype=np.float)
resolution=array([[1024,256],[1024,320],[1024,448],[1024,512],[1024,640]], dtype=np.float)
aspect_ratios=(resolution[:,0]/resolution[:,1])*(size[:,1]/size[:,0])
number_of_graphs=len(data)
fig, ax=plt.subplots(nrows=number_of_graphs, sharex=xshare)
fig.set_size_inches(12,figheight)
for i in range(number_of_graphs):
temp=np.rot90(np.loadtxt(path+'/'+data[i]))
img=ax[i].imshow(temp,
interpolation="none",
cmap=mapping,
norm=specific_norm,
aspect=aspect_ratios[i]
)
ax[i].set_adjustable('box-forced')
#Here I have to set some ticks and labels....
ax[i].xaxis.set_ticks(np.arange(0,int(size[i,0]),stepwidth_width)*resolution[i,0]/size[i,0])
ax[i].set_xticklabels((np.arange(0, int(size[i,0]), stepwidth_width)))
ax[i].yaxis.set_ticks(np.arange(0,int(size[i,1]),stepwidth_height)*resolution[i,1]/size[i,1])
ax[i].set_yticklabels((np.arange(0, int(size[i,1]), stepwidth_height)))
ax[i].set_title(str(mag[i]))
grid(True)
savefig(path+'/'+name+'all.pdf', bbox_inches='tight', pad_inches=0.05) #saves graph
Here are some examples:
If I plot different matrices in a for loop, the iPhython generates an output which is pretty much what I want. The y-distande between each subplot is constant, and the size of each figure is correct. You can see, that the x-labels match to each other:
When I plot the matrices in one figure using subplots, then this is not the case: The x-ticks do not fit together, and every subplot has the same size on the canvas (which means, that for thin subplots there is more white space reservated on the canvas...).
I simply want the first result from iPython in one output file using subplots.
Using GridSpec
After the community told me to use GridSpec to determine the size of my subplots directly I wrote a code for automatic plotting:
size=array([[3983,229],[3933,350],[3854,454],[3750,533],[3500,600]], dtype=np.float)
#total size of the figure
total_height=int(sum(size[:,1]))
total_width=int(size.max())
#determines steps of ticks
stepwidth_width=500
stepwidth_height=200
fig, ax=plt.subplots(nrows=len(size))
fig.set_size_inches(size.max()/300., total_height/200)
gs = GridSpec(total_height, total_width)
gs.update(left=0, right=0.91, hspace=0.2)
height=0
for i in range (len(size)):
ax[i] = plt.subplot(gs[int(height):int(height+size[i,1]), 0:int(size[i,0])])
temp=np.rot90(np.loadtxt(path+'/'+FFTs[i]))
img=ax[i].imshow(temp,
interpolation="none",
vmin=-100,
vmax=+100,
aspect=aspect_ratios[i],
)
#Some rescaling
ax[i].xaxis.set_ticks(np.arange(0,int(size[i,0]),stepwidth_width)*resolution[i,0]/size[i,0])
ax[i].set_xticklabels((np.arange(0, int(size[i,0]), stepwidth_width)))
ax[i].yaxis.set_ticks(np.arange(0,int(size[i,1]),stepwidth_height)*resolution[i,1]/size[i,1])
ax[i].set_yticklabels((np.arange(0, int(size[i,1]), stepwidth_height)))
ax[i].axvline(antenna[i]) #at the antenna position a vertical line is plotted
grid(True)
#colorbar
cbaxes = fig.add_axes([0.93, 0.2, 0.01, 0.6]) #[left, bottom, width, height]
cbar = plt.colorbar(img, cax = cbaxes, orientation='vertical')
tick_locator = ticker.MaxNLocator(nbins=3)
cbar.locator = tick_locator
cbar.ax.yaxis.set_major_locator(matplotlib.ticker.AutoLocator())
cbar.set_label('Intensity',
#fontsize=12
)
cbar.update_ticks()
height=height+size[i,1]
plt.show()
And here is the result....
Do you have any ideas?
What about using matplotlib.gridspec.GridSpec? Docs.
You could try something like
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
gs = GridSpec(8, 39)
ax1 = plt.subplot(gs[:6, :35])
ax2 = plt.subplot(gs[6:, :])
data1 = np.random.rand(6, 35)
data2 = np.random.rand(2, 39)
ax1.imshow(data1)
ax2.imshow(data2)
plt.show()

automatically position text box in matplotlib

Is there a way of telling pyplot.text() a location like you can with pyplot.legend()?
Something like the legend argument would be excellent:
plt.legend(loc="upper left")
I am trying to label subplots with different axes using letters (e.g. "A","B"). I figure there's got to be a better way than manually estimating the position.
Thanks
Just use annotate and specify axis coordinates. For example, "upper left" would be:
plt.annotate('Something', xy=(0.05, 0.95), xycoords='axes fraction')
You could also get fancier and specify a constant offset in points:
plt.annotate('Something', xy=(0, 1), xytext=(12, -12), va='top'
xycoords='axes fraction', textcoords='offset points')
For more explanation see the examples here and the more detailed examples here.
I'm not sure if this was available when I originally posted the question but using the loc parameter can now actually be used. Below is an example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import AnchoredText
# make some data
x = np.arange(10)
y = x
# set up figure and axes
f, ax = plt.subplots(1,1)
# loc works the same as it does with figures (though best doesn't work)
# pad=5 will increase the size of padding between the border and text
# borderpad=5 will increase the distance between the border and the axes
# frameon=False will remove the box around the text
anchored_text = AnchoredText("Test", loc=2)
ax.plot(x,y)
ax.add_artist(anchored_text)
plt.show()
The question is quite old but as there is no general solution to the problem till now (2019) according to Add loc=best kwarg to pyplot.text(), I'm using legend() and the following workaround to obtain auto-placement for simple text boxes:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpl_patches
x = np.linspace(-1,1)
fig, ax = plt.subplots()
ax.plot(x, x*x)
# create a list with two empty handles (or more if needed)
handles = [mpl_patches.Rectangle((0, 0), 1, 1, fc="white", ec="white",
lw=0, alpha=0)] * 2
# create the corresponding number of labels (= the text you want to display)
labels = []
labels.append("pi = {0:.4g}".format(np.pi))
labels.append("root(2) = {0:.4g}".format(np.sqrt(2)))
# create the legend, supressing the blank space of the empty line symbol and the
# padding between symbol and label by setting handlelenght and handletextpad
ax.legend(handles, labels, loc='best', fontsize='small',
fancybox=True, framealpha=0.7,
handlelength=0, handletextpad=0)
plt.show()
The general idea is to create a legend with a blank line symbol and to remove the resulting empty space afterwards. How to adjust the size of matplotlib legend box? helped me with the legend formatting.

Categories

Resources