I'm currently trying to create a stackplot of graphs, of which my first two have colorbars. To do this nicely, I'm using GridSpec to define two columns, with the second being much thinner and specifically for colorbars (or other out-of-plot things like legends).
grids = gs.GridSpec(5, 2, width_ratios=[1, 0.01])
ax1 = fig.add_subplot(grids[0, 0])
cax1 = fig.add_subplot(grids[0, 1])
The problem is that for these top two plots, the ticklabels of my colorbar overlap slightly, due to the fact that I've got zero horizontal space between my plots.
I know that there are ways to control the height of the colorbar, but they seem to rely on the colorbar making it's own axes by borrowing space from the parent axes. I was wondering if there was any way to control how much space (or specifically, height) the colorbar takes up when you use the cax kwarg
fig.colorbar(im1, cax=cax1, extend='max')
or if it defaults (immutably) to take up the entire height of the axes given to it.
Thanks!
EDIT: Here's an image of the issue I'm struggling with.
If I could make the second slightly shorter, or shift the upper one slightly up then it wouldn't be an issue. Unfortunately since I've used GridSpec (which has been amazing otherwise) I'm constrained to the limits of the axes.
I don't think there is any way to ask colorbar to not fill the whole cax. However, it is fairly trivial to shrink the size of the cax before (or after actually) plotting the colorbar.
I wrote this small function:
def shrink_cbar(ax, shrink=0.9):
b = ax.get_position()
new_h = b.height*shrink
pad = (b.height-new_h)/2.
new_y0 = b.y0 + pad
new_y1 = b.y1 - pad
b.y0 = new_y0
b.y1 = new_y1
ax.set_position(b)
which can be used like so:
fig = plt.figure()
grids = gs.GridSpec(2, 2, width_ratios=[1, 0.01])
ax1 = fig.add_subplot(grids[0, 0])
cax1 = fig.add_subplot(grids[0, 1])
ax2 = fig.add_subplot(grids[1, 0])
cax2 = fig.add_subplot(grids[1, 1])
shrink_cbar(cax2, 0.75)
Related
I'm trying to plot some array data and add a colorbar to the right of the axis, matching the height and with a set width.
Starting by generating some data.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
data = np.random.rand(700, 400)
I've got the following function.
def plot_data(data, aspect, pad):
fig, ax = plt.subplots()
img = ax.imshow(data, aspect=aspect)
last_axes = plt.gca()
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size='5%', pad=pad)
cbar = fig.colorbar(im, cax=cax)
plt.sca(last_axes)
Running plot_data(data, None, 0.05) gives what I would expect - an image with colorbar taking up 5% of the width, matched to the same height and padded correctly.
However running plot_data(data, 2.5, 0) results in a figure with an image that has the correct aspect ratio, but a colorbar that's padded way too much. I can correct this by making the padding negative, finding a good value by trial and error. However I need this to be generic and to work without user monitoring.
I found this thread but the answer doesn't seem to solve this particular case.
Any suggestions are much appreciated!
I've been playing with this and it looks like the color bar is always based on the original location of the edge of the data plot. This means that, for positive aspect ratios, the height of the graph stays fixed and the width of the graph is reduced.
The image is then centered so the padding needs to be used to adjust the color bar respectively (inward) by width - height/aspect
width=last_axes.get_position().width
height=last_axes.get_position().height
cax = divider.append_axes('right', size='5%', pad=-((width/0.7)-(height/(0.7*aspect)) + pad))
The odd thing I encountered is that it's not exactly centering the data but rather the center of the data and axis labels so we have to downscale the adjustment accordingly hence the 1/0.7 in the formula. I realise that this is not perfect as the ticklables are not being reduced by aspect so a linear shift would be more suitable but I've done it now!
please note this DOES NOT work for aspect ratios LESS THAN 1 because at that point the width is fixed and the height gets altered when aspect is applied. I'm gonna keep messing around with it and see if I can generalise for landscape
edit:
Ok, I have it. the append axis function forces the vertical colorbar to be the original height of the plot for some reason. fine for portrait plots but broken for landscape where the data is shrunk vertically but the plot isn't so I had to put a switch case in here's the full code:
def plot_data(data, aspect, pad):
fig, ax = plt.subplots()
img = ax.imshow(data, aspect=aspect)
last_axes = plt.gca()
divider = make_axes_locatable(ax)
if(aspect<1):
hscale=aspect
cbar = fig.colorbar(img,shrink=hscale,pad=(-0.43+pad))
else:
hscale=1
width=last_axes.get_position().width
height=last_axes.get_position().height
padfix = -((width/0.7)-(height/(0.7*aspect)))
cax = divider.append_axes('right',size='5%', pad=padfix+ pad)
cbar = fig.colorbar(img,cax=cax)
Again there's some weirdness going on with a fixed offset (this time \approx 0.43) this was found by trial and error and may need to be adjusted if plotting really long thin plots.
A solution to the problem
A "nonlinear regression" with a few approximate values in the interval (1,2.5) gave me:
y=a*x^2+b*x+c
a=0.25
b=-1.29
c=1.09
Try using:
cax = divider.append_axes('right', size='5%', pad=0.25*aspect**2-1.29*aspect+1.09)
For now you can't use None as aspect when aspect=1 and you don't need to pass pad. Also, this formula works for aspect>=1, you may need to get another formula for values less than 1 because the behaviour is really different. For values higher than 2.5 you need to calculate new coefficientes (the explanation is below).
def plot_data(data, aspect, pad=None):
if aspect == None:
aspect = 1
fig, ax = plt.subplots()
img = ax.imshow(data, aspect=aspect)
last_axes = plt.gca()
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size='5%', pad=0.25*aspect**2-1.29*aspect+1.09)
cbar = fig.colorbar(img, cax=cax)
plt.sca(last_axes)
About the "nonlinear regression"
To get the coefficients the first thing I needed to do is get a few pairs (aspect, padding). How get them? Trial and error: the first pair is the original (1, 0.05), the others were visually good: (1.5, -0.305), (2, -0.48), (2.5, -0.58). If you see, there is no linearity in the data but lets plot it anyways
import matplotlib.pyplot as plt
data = [(1,0.05),(1.5,-0.305),(2,-0.48),(2.5,-0.58)]
plt.plot(*zip(*data))
plt.plot(*zip(*data),'or')
plt.xlabel("aspect")
plt.ylabel("padding")
plt.show()
Now, lets get the the coefficients doing a curve fitting:
fit = np.polynomial.polynomial.Polynomial.fit(*zip(*pairs),2)
c,b,a = fit.convert().coef
(Yes, I only did a curve fitting with a second-degree polynomial, sorry!) Differences between the values I initially wrote and the one python gives arise because I used other software and I did a round to 2 decimals in most cases.
Why did I use a second degree polynomial (fit(x,y,2))? I tried to keep the model as simple as possible.
pairs = [(1,0.05),(1.5,-0.305),(2,-0.48),(2.5,-0.58)]
x = np.linspace(1,2.5,100)
yp = [a*x**2+b*x+c for x in x]
plt.plot(*zip(*pairs),'or', label='pairs')
plt.plot(x,yp,'g', label='Curve fitting')
plt.xlabel("aspect")
plt.ylabel("padding")
plt.legend(loc="upper right")
plt.show()
Does this work for values outside [1,2.5]? Not really. For that you should include more points in the curve fitting, maybe change the polynomial of order 2 for, lets say, a logarithmic one.
I'm trying to produce a series of figures showing geometric shapes of different sizes (one shape in each figure) but consistent, equal-spacing axes across each figure. I can't seem to get axis('equal') to play nice with set_xlim in matplotlib.
Here's the closest I've come so far:
pts0 = np.array([[13,34], [5,1], [ 0,0], [7,36], [13,34]], dtype=np.uint8)
pts1 = np.array([[10,82], [119,64], [149,63], [136,0], [82,14], [81,18],
[26,34], [3,29], [0,34], [10,82]], dtype=np.uint8)
shapes = [pts0,pts1]
for i in range(2):
pts = shapes[i]
fig = plt.figure()
ax1 = fig.add_subplot(111)
plotShape = patches.Polygon(pts, True, fill=True)
p = PatchCollection([plotShape], cmap=cm.Greens)
color = [99]
p.set_clim([0, 100])
p.set_array(np.array(color))
ax1.add_collection(p)
ax1.axis('equal')
ax1.set_xlim(-5,200)
ax1.set_ylim(-5,200)
ax1.set_title('pts'+str(i))
plt.show()
In my system, this results in two figures with the same axes, but neither one of them shows y=0 or the lower portion of the shape. If I remove the line ax1.set_ylim(-5,200), then figure "pts1" looks correct, but the limits of figure "pts0" are such that the shape doesn't show up at all.
My ideal situation is to "anchor" the lower-left corner of the figures at (-5,-5), define xlim as 200, and allow the scaling of the x axis and the value of ymax to "float" as the figure windows are resized, but right now I'd be happy just to consistently get the shapes inside the figures.
Any help would be greatly appreciated!
You can define one of your axes independently first and then when you define the second axis use the sharex or sharey arguments
new_ax = fig.add_axes([<bounds>], sharex=old_ax)
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:
I am stuck in a rather complicated situation. I am plotting some data as an image with imshow(). Unfortunately my script is long and a little messy, so it is difficult to make a working example, but I am showing the key steps. This is how I get the data for my image from a bigger array, written in a file:
data = np.tril(np.loadtxt('IC-heatmap-20K.mtx'), 1)
#
#Here goes lot's of other stuff, where I define start and end
#
chrdata = data[start:end, start:end]
chrdata = ndimage.rotate(chrdata, 45, order=0, reshape=True,
prefilter=False, cval=0)
ax1 = host_subplot(111)
#I don't really need host_subplot() in this case, I could use something more common;
#It is just divider.append_axes("bottom", ...) is really convenient.
plt.imshow(chrdata, origin='lower', interpolation='none',
extent=[0, length*resolution, 0, length*resolution]) #resolution=20000
So the values I am interested in are all in a triangle with the top angle in the middle of the top side of a square. At the same time I plot some data (lot's of coloured lines in this case) along with the image near it's bottom.
So at first this looks OK, but is actually is not: all pixels in the image are not square, but elongated with their height being bigger, than their width. This is how they look if I zoom in:
This doesn't happen, If I don't set extent when calling imshow(), but I need it so that coordinates in the image and other plots (coloured lines at the bottom in this case), where identical (see Converting coordinates of a picture in matplotlib?).
I tried to fix it using aspect. I tried to do that and it fixed the pixels' shape, but I got a really weird picture:
The thing is, later in the code I explicitly set this:
ax1.set_ylim(0*resolution, length*resolution) #resolution=20000
But after setting aspect I get absolutely different y limits. And the worst thing: ax1 is now wider, than axes of another plot at the bottom, so that their coordinates do not match anymore! I add it in this way:
axPlotx = divider.append_axes("bottom", size=0.1, pad=0, sharex=ax1)
I would really appreciate help with getting it fixed: square pixels, identical coordinates in two (or more, in other cases) plots. As I see it, the axes of the image need to become wider (as aspect does), the ylims should apply and the width of the second axes should be identical to the image's.
Thanks for reading this probably unclear explanation, please, let me know, if I should clarify anything.
UPDATE
As suggested in the comments, I tried to use
ax1.set(adjustable='box-forced')
And it did help with the image itself, but it caused two axes to get separated by white space. Is there any way to keep them close to each other?
Re-edited my entire answer as I found the solution to your problem. I solved it using the set_adjustable("box_forced") option as suggested by the comment of tcaswell.
import numpy
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import host_subplot, make_axes_locatable
#Calculate aspect ratio
def determine_aspect(shape, extent):
dx = (extent[1] - extent[0]) / float(shape[1])
dy = (extent[3] - extent[2]) / float(shape[0])
return dx / dy
data = numpy.random.random((30,60))
shape = data.shape
extent = [-10, 10, -20, 20]
x_size, y_size = 6, 6
fig = plt.figure(figsize = (x_size, y_size))
ax = host_subplot(1, 1, 1)
ax.imshow(data, extent = extent, interpolation = "None", aspect = determine_aspect(shape, extent))
#Determine width and height of the subplot frame
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
width, height = bbox.width, bbox.height
#Calculate distance, the second plot needs to be elevated by
padding = (y_size - (height - width)) / float(1 / (2. * determine_aspect(shape, extent)))
#Create second image in subplot with shared x-axis
divider = make_axes_locatable(ax)
axPlotx = divider.append_axes("bottom", size = 0.1, pad = -padding, sharex = ax)
#Turn off yticks for axPlotx and xticks for ax
axPlotx.set_yticks([])
plt.setp(ax.get_xticklabels(), visible=False)
#Make the plot obey the frame
ax.set_adjustable("box-forced")
fig.savefig("test.png", dpi=300, bbox_inches = "tight")
plt.show()
This results in the following image where the x-axis is shared:
Hope that helps!
i create a figure with 4 subplots (2 x 2), where 3 of them are of the type imshow and the other is errorbar. Each imshow plots have in addition a colorbar at the right side of them. I would like to resize my 3rd plot, that the area of the graph would be exactly under the one above it (with out colorbar)
as example (this is what i now have):
How could i resize the 3rd plot?
Regards
To adjust the dimensions of an axes instance, you need to use the set_position() method. This applies to subplotAxes as well. To get the current position/dimensions of the axis, use the get_position() method, which returns a Bbox instance. For me, it's conceptually easier to just interact with the position, ie [left,bottom,right,top] limits. To access this information from a Bbox, the bounds property.
Here I apply these methods to something similar to your example above:
import matplotlib.pyplot as plt
import numpy as np
x,y = np.random.rand(2,10)
img = np.random.rand(10,10)
fig = plt.figure()
ax1 = fig.add_subplot(221)
im = ax1.imshow(img,extent=[0,1,0,1])
plt.colorbar(im)
ax2 = fig.add_subplot(222)
im = ax2.imshow(img,extent=[0,1,0,1])
plt.colorbar(im)
ax3 = fig.add_subplot(223)
ax3.plot(x,y)
ax3.axis([0,1,0,1])
ax4 = fig.add_subplot(224)
im = ax4.imshow(img,extent=[0,1,0,1])
plt.colorbar(im)
pos4 = ax4.get_position().bounds
pos1 = ax1.get_position().bounds
# set the x limits (left and right) to first axes limits
# set the y limits (bottom and top) to the last axes limits
newpos = [pos1[0],pos4[1],pos1[2],pos4[3]]
ax3.set_position(newpos)
plt.show()
You may feel that the two plots do not exactly look the same (in my rendering, the left or xmin position is not quite right), so feel free to adjust the position until you get the desired effect.