Matplotlib subplot alignment and equal spacing - python

I am trying to make an image with multiple subplots using matplotlib. That part is no problem. What I have been struggling with is alignment. Specifically, having the outside edges of the plots aligned and having equal spacing between the plots (the same space horizontally and vertically).
I have been able to get it working with no spacing between plots:
import numpy as np
import matplotlib.pyplot as plt
plt.set_cmap('jet')
plt.close()
dim1 = 81
dim2 = 445
rad = 10
arr1 = np.random.rand(dim1, dim2) # Representative data, first plot
arr1[round(dim1/2 - rad):round(dim1/2 + rad), round(dim1/2 - rad):round(dim1/2 + rad)] += 6
arr1[round(dim1/2 - rad):round(dim1/2 + rad), round(-dim1/2 - rad):round(-dim1/2 + rad)] += 3
arr2 = arr1[:, :dim1] # Second plot
arr3 = arr1[:, -dim1:] # Third plot
xpix = dim2 # sudo x pixels in the image
ypix = xpix/2 + dim1 # sudo y pixels for the image
ratio = xpix/ypix # ratio of the width to height for the image
size = 7
fig = plt.figure(figsize = (size*ratio, size))
grid = plt.GridSpec(2, 2, wspace = 0, hspace = 0, left = 0, right = 1, top = 1, bottom = 0, height_ratios = [dim1, xpix/2])
ax1=fig.add_subplot(grid[0,:]) # Top plot
ax1.axis(False)
#ax1.set_anchor('N')
ax1.imshow(arr1, aspect = 'equal')
ax2=fig.add_subplot(grid[1,0]) # Bottom left plot
ax2.axis(False)
ax2.set_anchor('SW')
ax2.imshow(arr2, aspect = 'equal')
ax3=fig.add_subplot(grid[1,1]) # Bottom right plot
ax3.axis(False)
ax3.set_anchor('SE')
ax3.imshow(arr3, aspect = 'equal')
plt.show()
Which gives me the attached output.
No spacing plot
However, after many attempts at adding in spaces, I have had no luck. One way that I tried to do it was replacing the GridSpec line above with:
space = 0.01
grid = plt.GridSpec(2, 2, wspace = space, hspace = space*ratio, left = 0, right = 1, top = 1, bottom = 0, height_ratios=[dim1, xpix*(1-space)/2])
Sometimes depending on the space, image size, or number of pixels this seems to work, but a lot of the time it does not. I have tried several other things including using a 3*3 GridSpec with wspace = 0, hspace = 0 and using the middle row/column for spacing. I also tried using multiple GridSpec's and specifying the top, bottom, left, and right for each to get equal spacing and aligned edges. All of these attempts yielded similar results. One other important thing is that the I do need to have square pixels for each of the arrays (I noticed that a lot of the examples/other questions regarding GridSpec use subplots with nothing in them and it looks like the aspect ratio changes to get alignment?).
My ultimate goal is to be able to set an arbitrary space, size and input arrays and not have to worry about fiddling with things in order to get equal spacing with the edges aligned. Thank you in advance for any help!
Edit:
I am hoping that in the end the output will look like this: Desired Output and that I will be able to choose the amount of space between the images.

Related

Image plot in bokeh with tight axes and matching aspect ratio

I'm using bokeh 1.0.1 version inside a Django application and I would like to display microscopic surface images as zoomable image plots with a color-encoded height and colorbar. In principle this works, but I have problems to get plots with the correct aspect ratio only showing the image without space around.
Here is an example for what I want to achieve: The resulting plot should
show an image of random data having a width of sx=10 and a height of sy=5 in data space (image size)
have axes limited to (0,sx) and (0,sy), on initial view
and when zooming
a square on the screen should match a square in data space, at least in initial view
For the image I just use random data with nx=100 points in x direction and ny=100 points in y direction.
Here is my first approach:
Attempt 1
from bokeh.models.ranges import Range1d
from bokeh.plotting import figure, show
from bokeh.models import LinearColorMapper, ColorBar
import numpy as np
sx = 10
sy = 5
nx = 100
ny = 100
arr = np.random.rand(nx, ny)
x_range = Range1d(start=0, end=sx, bounds=(0,sx))
y_range = Range1d(start=0, end=sy, bounds=(0,sy))
# Attempt 1
plot = figure(x_range=x_range, y_range=y_range, match_aspect=True)
# Attempt 2
# plot = figure(match_aspect=True)
# Attempt 3
# pw = 400
# ph = int(400/sx*sy)
# plot = figure(plot_width=pw, plot_height=ph,
# x_range=x_range, y_range=y_range, match_aspect=True)
color_mapper = LinearColorMapper(palette="Viridis256",
low=arr.min(), high=arr.max())
colorbar = ColorBar(color_mapper=color_mapper, location=(0,0))
plot.image([arr], x=[0], y=[0], dw=[sx], dh=[sy],
color_mapper=color_mapper)
plot.rect(x=[0,sx,sx,0,sx/2], y=[0,0,sy,sy,sy/2],
height=1, width=1, color='blue')
plot.add_layout(colorbar, 'right')
show(plot)
I've also added blue squares to the plot in order to see, when the
aspect ratio requirement fails.
Unfortunately, in the resulting picture, the square is no square any more, it's twice as high as wide. Zooming and panning works as expected.
Attempt 2
When leaving out the ranges by using
plot = figure(match_aspect=True)
I'll get this picture. The square is a square on the screen,
this is fine, but the axis ranges changed, so there is
now space around it. I would like to have only the data area covered by the
image.
Attempt 3
Alternatively, when providing a plot_height and plot_width to the figure,
with a pre-defined aspect ratio e.g. by
pw = 800 # plot width
ph = int(pw/sx*sy)
plot = figure(plot_width=pw, plot_height=ph,
x_range=x_range, y_range=y_range,
match_aspect=True)
I'll get this picture. The square is also not a square any more. It can be done almost, but it's difficult, because the plot_width also comprises the colorbar and the toolbar.
I've read this corresponding blog post
and the corresponding bokeh documentation, but I cannot get it working.
Does anybody know how to achieve what I want or whether it is impossible?
Responsive behaviour would also be nice, but we can neglect that for now.
Thanks for any hint.
Update
After a conversation with a Bokeh developer on Gitter (thanks Bryan!) it seems that it is nearly impossible what I want.
The reason is, how match_aspect=True works in order to make a square in data space look like a square in pixel space: Given a canvas size, which may result from applying different sizing_mode settings for responsive behaviour, the data range is then changed in order to have the matching aspect ratio. So there is no other way to make the pixel aspect ratio to match the data aspect ratio without adding extra space around the image, i.e. to extend the axes over the given bounds. Also see the comment of this issue.
Going without responsive behaviour and then fixing the canvas size beforehand with respect to the aspect ratio could be done, but currently not perfectly because of all the other elements around the inner plot frame which also take space. There is a PR which may allow a direct control of inner frame dimensions, but I'm not sure how to do it.
Okay, what if I give up the goal to have tight axes?
This is done in "Attempt 2" above, but there is too much empty space around the image, the same space that the image plot takes.
I've tried to use various range_padding* attributes, e.g.
x_range = DataRange1d(range_padding=10, range_padding_units='percent')
y_range = DataRange1d(range_padding=10, range_padding_units='percent')
but it doesn't reduce the amount of space around the plot, but increases it only. The padding in percent should be relative to the image dimensions given by dh and dw.
Does anybody know how to use the range_padding parameters to have smaller axis ranges or another way to have smaller paddings around the image plot in the example above (using match_aspect=True)?
I've opened another question on this.
Can you accept this solution (works with Bokeh v1.0.4) ?
from bokeh.models.ranges import Range1d
from bokeh.plotting import figure, show
from bokeh.layouts import Row
from bokeh.models import LinearColorMapper, ColorBar
import numpy as np
sx = 10
sy = 5
nx = 100
ny = 100
arr = np.random.rand(nx, ny)
x_range = Range1d(start = 0, end = sx, bounds = (0, sx))
y_range = Range1d(start = 0, end = sy, bounds = (0, sy))
pw = 400
ph = pw * sy / sx
plot = figure(plot_width = pw, plot_height = ph,
x_range = x_range, y_range = y_range, match_aspect = True)
color_mapper = LinearColorMapper(palette = "Viridis256",
low = arr.min(), high = arr.max())
plot.image([arr], x = [0], y = [0], dw = [sx], dh = [sy], color_mapper = color_mapper)
plot.rect(x = [0, sx, sx, 0, sx / 2], y = [0, 0, sy, sy, sy / 2], height = 1, width = 1, color = 'blue')
colorbar_plot = figure(plot_height = ph, plot_width = 69, x_axis_location = None, y_axis_location = None, title = None, tools = '', toolbar_location = None)
colorbar = ColorBar(color_mapper = color_mapper, location = (0, 0))
colorbar_plot.add_layout(colorbar, 'left')
show(Row(plot, colorbar_plot))
Result:

Matplotlib: Adjust legend location/position

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:

Extent and aspect; square pixels in an image with shared axis in matplotlib

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!

overlay matplotlib imshow with line plots that are arranged in a grid

I would like to plot a number of curves over an image
Using this code I am reasonably close:
G=plt.matplotlib.gridspec.GridSpec(64,1)
fig = plt.figure()
plt.imshow(img.data[:,:],cmap='gray')
plt.axis('off')
plt.axis([0,128,0,64])
for i in arange(64):
fig.add_subplot(G[i,0])
plt.axis('off')
# note that vtc.data.shape = (64, 128*400=51200)
# so every trace for each image pixel is 400 points long
plt.plot(vtc.data[i,:])
plt.axis([0, 51200, 0, 5])
The result that I am getting looks like this:
The problem is that while I seem to be able to get rid of all the padding in the horizontal (x) direction, there is different amount of padding in the image and the stacked plots in the vertical direction.
I tried using
ax = plt.gca()
ax.autoscale_view('tight')
but that didn't reduce the margin either.
How can I get a grid of m-by-n line plots to line up precisely with a blown up (by factor f) version of an image with dimensions (fm)-by-(fn)?
UPDATE and Solution:
The answer by #RutgerKassies works quite well. I achieved it using his code like so:
fig, axs = plt.subplots(1,1,figsize=(8,4))
axs.imshow(img.data[:,:],cmap='gray', interpolation='none')
nplots = 64
fig.canvas.draw()
box = axs._position.bounds
height = box[3] / nplots
for i in arange(nplots):
tmpax = fig.add_axes([box[0], box[1] + i * height, box[2], height])
tmpax.set_axis_off()
# make sure to get image orientation right and
tmpax.plot(vtc.data[nplots-i-1,:],alpha=.3)
tmpax.set_ylim(0,5)
tmpax.set_xlim(0, 51200)
I think the easiest way is to use the boundaries from your 'imshow axes' to manually calculate the boundaries of all your 'lineplot axes':
import matplotlib.pyplot as plt
import numpy as np
fig, axs = plt.subplots(1,1,figsize=(15,10))
axs.imshow(np.random.rand(50,100) ,cmap='gray', interpolation='none', alpha=0.3)
nplots = 50
fig.canvas.draw()
box = axs._position.bounds
height = box[3] / nplots
for i in arange(nplots):
tmpax = fig.add_axes([box[0], box[1] + i * height, box[2], height])
tmpax.set_axis_off()
tmpax.plot(np.sin(np.linspace(0,np.random.randint(20,1000),1000))*0.4)
tmpax.set_ylim(-1,1)
The above code seems nice, but i do have some issues with the autoscale chopping off part of the plot. Try removing the last line to see the effect, im not sure why thats happening.

blank space in the top of the plot matplotlib django

I've a question about matplotlib bars.
I've already made some bar charts but I don't know why, this one left a huge blank space in the top.
the code is similar to other graphics I've made and they don't have this problem.
If anyone has any idea, I appreciate the help.
x = matplotlib.numpy.arange(0, max(total))
ind = matplotlib.numpy.arange(len(age_list))
ax.barh(ind, total)
ax.set_yticks(ind)
ax.set_yticklabels(age_list)
By "blank space in the top" do you mean that the y-limits are set too large?
By default, matplotlib will choose the x and y axis limits so that they're rounded to the closest "even" number (e.g. 1, 2, 12, 5, 50, -0.5 etc...).
If you want the axis limits to be set so that they're "tight" around the plot (i.e. the min and max of the data) use ax.axis('tight') (or equivalently, plt.axis('tight') which will use the current axis).
Another very useful method is plt.margins(...)/ax.margins(). It will act similar to axis('tight'), but will leave a bit of padding around the limits.
As an example of your problem:
import numpy as np
import matplotlib.pyplot as plt
# Make some data...
age_list = range(10,31)
total = np.random.random(len(age_list))
ind = np.arange(len(age_list))
plt.barh(ind, total)
# Set the y-ticks centered on each bar
# The default height (thickness) of each bar is 0.8
# Therefore, adding 0.4 to the tick positions will
# center the ticks on the bars...
plt.yticks(ind + 0.4, age_list)
plt.show()
If I wanted the limits to be tighter, I could call plt.axis('tight') after the call to plt.barh, which would give:
However, you might not want things to be too tight, so you could use plt.margins(0.02) to add 2% padding in all directions. You can then set the left-hand limit back to 0 with plt.xlim(xmin=0):
import numpy as np
import matplotlib.pyplot as plt
# Make some data...
age_list = range(10,31)
total = np.random.random(len(age_list))
ind = np.arange(len(age_list))
height = 0.8
plt.barh(ind, total, height=height)
plt.yticks(ind + height / 2.0, age_list)
plt.margins(0.05)
plt.xlim(xmin=0)
plt.show()
Which produces a bit nicer of a plot:
Hopefully that points you in the right direction, at any rate!

Categories

Resources