Related
I want to make a plot with a grid of thumbnails on the left and a line plot on the right. Here is a minimal example
import numpy as np
from matplotlib import pyplot as plt
### This can change at runtime
n_grid = 4
### Grid of thumbnails
fig = plt.figure(figsize=(20,10.2))
for i in range(n_grid):
for j in range(n_grid):
ax = plt.subplot2grid(shape=(n_grid, 2*n_grid), loc=(i,j))
plt.imshow(np.random.random((16,16)))
ax.set_axis_off()
### Line plot
ax = plt.subplot2grid(shape=(n_grid, 2*n_grid), loc=(0,n_grid), rowspan=n_grid-1, colspan=n_grid)
plt.plot(np.cumsum(np.random.random(100)), label='Random Sum')
plt.xlim([0, 100])
plt.ylim(0,50)
plt.xlabel('Number', fontsize=12)
plt.ylabel('Sum', fontsize=12)
plt.figtext(0.5, 0.01, f'Unique identifier', ha='center', va='baseline')
#plt.tight_layout()
plt.subplots_adjust(left=0.01, bottom=0.03, right=0.99, top=0.99, wspace = 0.06, hspace=0.06)
plt.savefig('plot_1.png', dpi=96)
The problem is that the yticklabels and ylabel stick over the center into the area of the thumbnails. The lineplot on the right is too wide.
One common solution found on the internet is using automatic resizing with tight_layout(), so I change the last three lines to
plt.tight_layout()
#plt.subplots_adjust(left=0.01, bottom=0.03, right=0.99, top=0.99, wspace = 0.06, hspace=0.06)
plt.savefig('plot_2.png', dpi=96)
This does not rescale the lineplot, but instead makes the wspace and hspace attributes so big I get way too much whitespace between the thumbnails.
I am looking for a solution to either
Set wspace and hspace of only the right subplot, not all of them together, or
resize the lineplot to fit into the designated area, without the labels sticking out
It would seem that this is an easy problem, but despite searching for about 2 hours and digging around in the object properties with iPython I found nothing suitable. All solutions seem to change the size and padding of the subplots, not fitting a plot into the area defined with subplot2grid. The only other solution I can think of is a hack that calculates a modified aspect from the value ranges to make the lineplot always a given percentage thinner.
You can play around with subfigures. For example, if you do:
import numpy as np
from matplotlib import pyplot as plt
### This can change at runtime
n_grid = 4
### Grid of thumbnails
fig = plt.figure(figsize=(20,10.2))
# add 2 subfigures
subfigs = fig.subfigures(1, 2, wspace=0)
# add thumbnail grid into left subfig
gsLeft = subfigs[0].add_gridspec(n_grid, n_grid)
axLeft = []
for i in range(n_grid):
for j in range(n_grid):
axLeft.append(subfigs[0].add_subplot(gsLeft[i, j]))
axLeft[-1].imshow(np.random.random((16,16)))
axLeft[-1].set_axis_off()
### Line plot
gsRight = subfigs[1].add_gridspec(3, 1)
axRight = subfigs[1].add_subplot(gsRight[:2, 0])
axRight.plot(np.cumsum(np.random.random(100)), label='Random Sum')
axRight.set_xlim([0, 100])
axRight.set_ylim(0,50)
axRight.set_xlabel('Number', fontsize=12)
axRight.set_ylabel('Sum', fontsize=12)
# adjust subfigures here (play around with these to get the desired effect)
subfigs[0].subplots_adjust(wspace=0.03, hspace=0.03, bottom=0.05, top=0.95, left=0.05, right=0.95)
subfigs[1].subplots_adjust(left=0.01)
# add title (here I've had to add it to the left figure, so it's not centred,
# in my test adding it to the figure itself meant it was not visible, although
# the example in the Matplotlib docs suggests it should work!)
# fig.suptitle(f'Unique identifier', x=0.5, y=0.025, ha='center', va='baseline')
subfigs[0].suptitle(f'Unique identifier', x=0.5, y=0.025, ha='center', va='baseline')
fig.savefig("plot_1.png", dpi=150)
This gives:
but you can play around with the values to adjust it as you like.
I'm creating a figure with multiple subplots. One of these subplots is giving me some trouble, as none of the axes corners or centers are free (or can be freed up) for placing the legend. What I'd like to do is to have the legend placed somewhere in between the 'upper left' and 'center left' locations, while keeping the padding between it and the y-axis equal to the legends in the other subplots (that are placed using one of the predefined legend location keywords).
I know I can specify a custom position by using loc=(x,y), but then I can't figure out how to get the padding between the legend and the y-axis to be equal to that used by the other legends. Would it be possible to somehow use the borderaxespad property of the first legend? Though I'm not succeeding at getting that to work.
Any suggestions would be most welcome!
Edit: Here is a (very simplified) illustration of the problem:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, sharex=False, sharey=False)
ax[0].axhline(y=1, label='one')
ax[0].axhline(y=2, label='two')
ax[0].set_ylim([0.8,3.2])
ax[0].legend(loc=2)
ax[1].axhline(y=1, label='one')
ax[1].axhline(y=2, label='two')
ax[1].axhline(y=3, label='three')
ax[1].set_ylim([0.8,3.2])
ax[1].legend(loc=2)
plt.show()
What I'd like is that the legend in the right plot is moved down somewhat so it no longer overlaps with the line.
As a last resort I could change the axis limits, but I would very much like to avoid that.
I saw the answer you posted and tried it out. The problem however is that it is also depended on the figure size.
Here's a new try:
import numpy
import matplotlib.pyplot as plt
x = numpy.linspace(0, 10, 10000)
y = numpy.cos(x) + 2.
x_value = .014 #Offset by eye
y_value = .55
fig, ax = plt.subplots(1, 2, sharex = False, sharey = False)
fig.set_size_inches(50,30)
ax[0].plot(x, y, label = "cos")
ax[0].set_ylim([0.8,3.2])
ax[0].legend(loc=2)
line1 ,= ax[1].plot(x,y)
ax[1].set_ylim([0.8,3.2])
axbox = ax[1].get_position()
fig.legend([line1], ["cos"], loc = (axbox.x0 + x_value, axbox.y0 + y_value))
plt.show()
So what I am now doing is basically getting the coordinates from the subplot. I then create the legend based on the dimensions of the entire figure. Hence, the figure size does not change anything to the legend positioning anymore.
With the values for x_value and y_value the legend can be positioned in the subplot. x_value has been eyeballed for a good correspondence with the "normal" legend. This value can be changed at your desire. y_value determines the height of the legend.
Good luck!
After spending way too much time on this, I've come up with the following satisfactory solution (the Transformations Tutorial definitely helped):
bapad = plt.rcParams['legend.borderaxespad']
fontsize = plt.rcParams['font.size']
axline = plt.rcParams['axes.linewidth'] #need this, otherwise the result will be off by a few pixels
pad_points = bapad*fontsize + axline #padding is defined in relative to font size
pad_inches = pad_points/72.0 #convert from points to inches
pad_pixels = pad_inches*fig.dpi #convert from inches to pixels using the figure's dpi
Then, I found that both of the following work and give the same value for the padding:
# Define inverse transform, transforms display coordinates (pixels) to axes coordinates
inv = ax[1].transAxes.inverted()
# Inverse transform two points on the display and find the relative distance
pad_axes = inv.transform((pad_pixels, 0)) - inv.transform((0,0))
pad_xaxis = pad_axes[0]
or
# Find how may pixels there are on the x-axis
x_pixels = ax[1].transAxes.transform((1,0)) - ax[1].transAxes.transform((0,0))
# Compute the ratio between the pixel offset and the total amount of pixels
pad_xaxis = pad_pixels/x_pixels[0]
And then set the legend with:
ax[1].legend(loc=(pad_xaxis,0.6))
Plot:
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()
As it says in the title, I am trying to fix the values of the colorbar (vmin=-3 and vmax=+3) of a polar contour plot. I am going to generate several dozens of such graphs, and the auto scaling of the colorbar makes comparison very difficult.
The plot itself is generated by the following code:
fig, ax = subplots(subplot_kw=dict(projection='polar'))
cax = ax.contourf(thetas, r, values, 130)
cb1 = fig.colorbar(cax)
I have been going through http://matplotlib.sourceforge.org for hours and still haven't found the solution. I would point me in the right direction.
You can do this by passing in the contour levels yourself.
Instead of just trying to set vmin=3, vmax=3, pick 130 values between vmin and vmax so they will be the same for all the graphs, independent of the data range.
Try:
contour_levels = arange(-3, 3, 0.05)
fig, ax = subplots(subplot_kw=dict(projection='polar'))
cax = ax.contourf(thetas, r, values, contour_levels)
cb1 = fig.colorbar(cax)
An alternative solution might be to follow the logic used in this response to a similar question on setting the min and max of a colorbar. The main takeaway is the use of set_clim(self, vmin=None, vmax=None). In the context of this question, one of the following might work:
fig, ax = subplots(subplot_kw=dict(projection='polar'))
cax = ax.contourf(thetas, r, values, vmin=-3, vmax=3)
cb1 = fig.colorbar(cax)
OR
cb1.set_clim(vmin=-3, vmax=3)
This answer is in the same vein but addresses the requisite of using the same colorbar min/max for multiple graphs.
I'm familiar with the following questions:
Matplotlib savefig with a legend outside the plot
How to put the legend out of the plot
It seems that the answers in these questions have the luxury of being able to fiddle with the exact shrinking of the axis so that the legend fits.
Shrinking the axes, however, is not an ideal solution because it makes the data smaller making it actually more difficult to interpret; particularly when its complex and there are lots of things going on ... hence needing a large legend
The example of a complex legend in the documentation demonstrates the need for this because the legend in their plot actually completely obscures multiple data points.
http://matplotlib.sourceforge.net/users/legend_guide.html#legend-of-complex-plots
What I would like to be able to do is dynamically expand the size of the figure box to accommodate the expanding figure legend.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))
ax.grid('on')
Notice how the final label 'Inverse tan' is actually outside the figure box (and looks badly cutoff - not publication quality!)
Finally, I've been told that this is normal behaviour in R and LaTeX, so I'm a little confused why this is so difficult in python... Is there a historical reason? Is Matlab equally poor on this matter?
I have the (only slightly) longer version of this code on pastebin http://pastebin.com/grVjc007
Sorry EMS, but I actually just got another response from the matplotlib mailling list (Thanks goes out to Benjamin Root).
The code I am looking for is adjusting the savefig call to:
fig.savefig('samplefigure', bbox_extra_artists=(lgd,), bbox_inches='tight')
#Note that the bbox_extra_artists must be an iterable
This is apparently similar to calling tight_layout, but instead you allow savefig to consider extra artists in the calculation. This did in fact resize the figure box as desired.
import matplotlib.pyplot as plt
import numpy as np
plt.gcf().clear()
x = np.arange(-2*np.pi, 2*np.pi, 0.1)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x, np.sin(x), label='Sine')
ax.plot(x, np.cos(x), label='Cosine')
ax.plot(x, np.arctan(x), label='Inverse tan')
handles, labels = ax.get_legend_handles_labels()
lgd = ax.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5,-0.1))
text = ax.text(-0.2,1.05, "Aribitrary text", transform=ax.transAxes)
ax.set_title("Trigonometry")
ax.grid('on')
fig.savefig('samplefigure', bbox_extra_artists=(lgd,text), bbox_inches='tight')
This produces:
[edit] The intent of this question was to completely avoid the use of arbitrary coordinate placements of arbitrary text as was the traditional solution to these problems. Despite this, numerous edits recently have insisted on putting these in, often in ways that led to the code raising an error. I have now fixed the issues and tidied the arbitrary text to show how these are also considered within the bbox_extra_artists algorithm.
Added: I found something that should do the trick right away, but the rest of the code below also offers an alternative.
Use the subplots_adjust() function to move the bottom of the subplot up:
fig.subplots_adjust(bottom=0.2) # <-- Change the 0.02 to work for your plot.
Then play with the offset in the legend bbox_to_anchor part of the legend command, to get the legend box where you want it. Some combination of setting the figsize and using the subplots_adjust(bottom=...) should produce a quality plot for you.
Alternative:
I simply changed the line:
fig = plt.figure(1)
to:
fig = plt.figure(num=1, figsize=(13, 13), dpi=80, facecolor='w', edgecolor='k')
and changed
lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,0))
to
lgd = ax.legend(loc=9, bbox_to_anchor=(0.5,-0.02))
and it shows up fine on my screen (a 24-inch CRT monitor).
Here figsize=(M,N) sets the figure window to be M inches by N inches. Just play with this until it looks right for you. Convert it to a more scalable image format and use GIMP to edit if necessary, or just crop with the LaTeX viewport option when including graphics.
Here is another, very manual solution. You can define the size of the axis and paddings are considered accordingly (including legend and tickmarks). Hope it is of use to somebody.
Example (axes size are the same!):
Code:
#==================================================
# Plot table
colmap = [(0,0,1) #blue
,(1,0,0) #red
,(0,1,0) #green
,(1,1,0) #yellow
,(1,0,1) #magenta
,(1,0.5,0.5) #pink
,(0.5,0.5,0.5) #gray
,(0.5,0,0) #brown
,(1,0.5,0) #orange
]
import matplotlib.pyplot as plt
import numpy as np
import collections
df = collections.OrderedDict()
df['labels'] = ['GWP100a\n[kgCO2eq]\n\nasedf\nasdf\nadfs','human\n[pts]','ressource\n[pts]']
df['all-petroleum long name'] = [3,5,2]
df['all-electric'] = [5.5, 1, 3]
df['HEV'] = [3.5, 2, 1]
df['PHEV'] = [3.5, 2, 1]
numLabels = len(df.values()[0])
numItems = len(df)-1
posX = np.arange(numLabels)+1
width = 1.0/(numItems+1)
fig = plt.figure(figsize=(2,2))
ax = fig.add_subplot(111)
for iiItem in range(1,numItems+1):
ax.bar(posX+(iiItem-1)*width, df.values()[iiItem], width, color=colmap[iiItem-1], label=df.keys()[iiItem])
ax.set(xticks=posX+width*(0.5*numItems), xticklabels=df['labels'])
#--------------------------------------------------
# Change padding and margins, insert legend
fig.tight_layout() #tight margins
leg = ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1), borderaxespad=0)
plt.draw() #to know size of legend
padLeft = ax.get_position().x0 * fig.get_size_inches()[0]
padBottom = ax.get_position().y0 * fig.get_size_inches()[1]
padTop = ( 1 - ax.get_position().y0 - ax.get_position().height ) * fig.get_size_inches()[1]
padRight = ( 1 - ax.get_position().x0 - ax.get_position().width ) * fig.get_size_inches()[0]
dpi = fig.get_dpi()
padLegend = ax.get_legend().get_frame().get_width() / dpi
widthAx = 3 #inches
heightAx = 3 #inches
widthTot = widthAx+padLeft+padRight+padLegend
heightTot = heightAx+padTop+padBottom
# resize ipython window (optional)
posScreenX = 1366/2-10 #pixel
posScreenY = 0 #pixel
canvasPadding = 6 #pixel
canvasBottom = 40 #pixel
ipythonWindowSize = '{0}x{1}+{2}+{3}'.format(int(round(widthTot*dpi))+2*canvasPadding
,int(round(heightTot*dpi))+2*canvasPadding+canvasBottom
,posScreenX,posScreenY)
fig.canvas._tkcanvas.master.geometry(ipythonWindowSize)
plt.draw() #to resize ipython window. Has to be done BEFORE figure resizing!
# set figure size and ax position
fig.set_size_inches(widthTot,heightTot)
ax.set_position([padLeft/widthTot, padBottom/heightTot, widthAx/widthTot, heightAx/heightTot])
plt.draw()
plt.show()
#--------------------------------------------------
#==================================================
I tried a very simple way, just make the figure a bit wider:
fig, ax = plt.subplots(1, 1, figsize=(a, b))
adjust a and b to a proper value such that the legend is included in the figure