Reducing axis length while maintaining equal aspect ratio in 3D plot - python

I am trying to create a 3-D plot and a 2-D plot side-by-side in python. I need equal aspect ratios for both plots, which I managed using code provided by this answer: https://stackoverflow.com/a/31364297/125507. The problem I'm having now is how to effectively "crop" the 3-D plot so it doesn't take up so much white space. That is to say, I want to reduce the length of the X and Y axes while maintaining equal scale to the (longer) Z-axis. Here is a sample code and plot:
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np
def set_axes_equal(ax):
'''Make axes of 3D plot have equal scale so that spheres appear as spheres,
cubes as cubes, etc.. This is one possible solution to Matplotlib's
ax.set_aspect('equal') and ax.axis('equal') not working for 3D.
Input
ax: a matplotlib axis, e.g., as output from plt.gca().
'''
x_limits = ax.get_xlim3d()
y_limits = ax.get_ylim3d()
z_limits = ax.get_zlim3d()
x_range = abs(x_limits[1] - x_limits[0])
x_middle = np.mean(x_limits)
y_range = abs(y_limits[1] - y_limits[0])
y_middle = np.mean(y_limits)
z_range = abs(z_limits[1] - z_limits[0])
z_middle = np.mean(z_limits)
# The plot bounding box is a sphere in the sense of the infinity
# norm, hence I call half the max range the plot radius.
plot_radius = 0.5*max([x_range, y_range, z_range])
ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
ax = [None]*2
fig = plt.figure()
ax[0] = fig.add_subplot(121, projection='3d', aspect='equal')
ax[1] = fig.add_subplot(122, aspect='equal')
nn = 30
phis = np.linspace(0,np.pi, nn).reshape(1,nn)
psis = np.linspace(0,np.pi*2,nn).reshape(nn,1)
ones = np.ones((nn,1))
el_h = np.linspace(-5, 5, nn).reshape(1,nn)
x_sph = np.sin(phis)*np.cos(psis)
y_sph = np.sin(phis)*np.sin(psis)
z_sph = np.cos(phis)*ones
x_elp = np.sin(phis)*np.cos(psis)*.25
y_elp = np.sin(phis)*np.sin(psis)*.25
z_elp = el_h*ones
ax[0].scatter(x_sph, y_sph, z_sph)
ax[0].scatter(x_elp, y_elp, z_elp)
ax[1].scatter(y_sph, z_sph)
ax[1].scatter(y_elp, z_elp)
for ii in range(2):
ax[ii].set_xlabel('X')
ax[ii].set_ylabel('Y')
ax[0].set_zlabel('Z')
set_axes_equal(ax[0])
plt.savefig('SphereElipse.png', dpi=300)
And here is its image output:
3-D and 2-D sphere and ellipse side-by-side
Clearly the 2D plot automatically modifies the length of the axes while maintaining the scale, but the 3D plot doesn't, leading to a tiny representation which does not well use the space allotted to its subplot. Is there any way to do this? This question is similar to an earlier unanswered question How do I crop an Axes3D plot with square aspect ratio?, except it adds the stipulation of multiple subplots, which means the answers provided there do not work.

Related

Cartesian zoom with polar plot in python

I am trying to plot some data in polar coordinates (I am currently using the polar projection):
The code I am using is the following:
import matplotlib.pyplot as plt
import numpy as np
# Create radial and angular array
r = np.linspace(1.0,10,11)
t = np.linspace(0.0,0.5*np.pi,101)
# Define the quantity that I want to plot
z = np.zeros((len(t),len(r)))
for yval in range(len(r)):
z[:,yval] = np.cos(16.0*t)/r[yval]
#Create the figure
f = plt.figure(figsize=(13,8))
ax = plt.subplot(111, projection='polar')
ax.set_rorigin(-1)
#Plot the data
pcm = ax.pcolormesh(t,r,z.T,cmap = 'hot',shading='gouraud')
ax.set_xlim([0.0,0.5*np.pi])
ax.set_ylim([1.0,10.0])
#Add colorbar and show
bar = f.colorbar(pcm)
plt.show()
So far I have no problem, but I would like to zoom on a particular region of this plot.
However, when I set the axes range the axes is still polar, therefore I cannot zoom on a "cartesian" region of the domain (i.e. a square box).
A possible option would be to transform the data into cartesian coordinates, but when I do it I lose a lot of resolution in the inner part of the domain, which is something that I should absolutely avoid.
How can I select a rectangular zone of a plot in polar coordinates without transforming by hand the data? And in case I have to switch to cartesian coordinates, is there any matplotlib or python function that does it while taking care of the resolution in the inner regions of the domain?
Thanks in advance
You can create an X, Y mesh yourself that is has a higher resolution on the inner part of the domain and use that with ax.pcolormesh()
# Create radial and angular array
r = np.linspace(1.0,10,11)
t = np.linspace(0.0,0.5*np.pi,101)
# Define the quantity that I want to plot
z = np.zeros((len(t),len(r)))
for yval in range(len(r)):
z[:,yval] = np.cos(16.0*t)/r[yval]
#Create the figure, bigger figsize to make the resulting plot square
f = plt.figure(figsize=(13,10))
ax = plt.subplot(111) # Drop back to XY coordinates
# Generate the XY corners of the colormesh
X = np.array([[ri*np.cos(j) for j in t] for ri in r])
Y = np.array([[ri*np.sin(j) for j in t] for ri in r])
#Plot the data
pcm = ax.pcolormesh(X,Y,z.T,cmap = 'hot',shading='gouraud')
#Add colorbar and show
bar = f.colorbar(pcm)
plt.show()
The figure from the question
The figure generated by code above
A way to do this is to create an expanded polar plot and then clip a rectangle of it. A picture is worth a thousand words:
Here is a function that allows you to do so. The arguments are the original axes, the xlims and ylims of the region to be zoomed and the inset axes bounds (x0, y0, width, height) in the original axes coordinates. The function outputs a cartesian ax with the specified limits, a polar axes where you can plot and the rmax value you need to set AFTER plotting (if you do it before, it will change after plotting).
def create_polar_zoom_inset(ax, xlims, ylims, inset_bounds):
# Create cartesian axes for inset
ax_inset_cart = ax.inset_axes(inset_bounds)
ax_inset_cart.set_xlim(xlims)
ax_inset_cart.set_ylim(ylims)
# Calculate location of expanded polar inset
# Scale factor from data to axes coordinates
xscalefactor = inset_bounds[2]/(xlims[1] - xlims[0])
yscalefactor = inset_bounds[3]/(ylims[1] - ylims[0])
# Center of expanded polar inset
center_inset_polar = [
inset_bounds[0] - xlims[0]*xscalefactor,
inset_bounds[1] - ylims[0]*yscalefactor
]
# Max value of r in the inset
rmax_inset = 2*np.sqrt(np.power(xlims, 2).max() + np.power(ylims, 2).max())
# Size of the expanded polar inset
size_inset_polar = [2*rmax_inset*xscalefactor, 2*rmax_inset*yscalefactor]
# Create expanded polar inset
polar_inset_bounds = [
center_inset_polar[0] - 0.5*size_inset_polar[0],
center_inset_polar[1] - 0.5*size_inset_polar[1],
size_inset_polar[0],
size_inset_polar[1]
]
ax_inset_polar = ax.inset_axes(polar_inset_bounds, projection="polar")
ax_inset_polar.set_facecolor("None")
# Remove tick labels from expanded polar inset
ax_inset_polar.xaxis.set_ticklabels([])
ax_inset_polar.yaxis.set_ticklabels([])
# Clip elements of the expanded inset outside the cartesian inset
ax_inset_polar.patch = ax_inset_cart.patch
for axis in [ax_inset_polar.xaxis, ax_inset_polar.yaxis]:
axis.set_clip_path(ax_inset_cart.patch)
ax_inset_polar.spines['polar'].set_clip_path(ax_inset_cart.patch)
return ax_inset_cart, ax_inset_polar, rmax_inset
The code in your example is especially hard since the origin of the axes is not (0,0) but (-1,-1). That would need additional tinkering. But if we set rorigin to 0 (as it will be usually the case), the code would look as follows
# Create radial and angular array
r = np.linspace(1.0,10,11)
t = np.linspace(0.0,0.5*np.pi,101)
# Define the quantity that I want to plot
z = np.zeros((len(t),len(r)))
for yval in range(len(r)):
z[:,yval] = np.cos(16.0*t)/r[yval]
#Create the figure
f = plt.figure(figsize=(13,8))
ax = plt.subplot(111, projection='polar')
ax.set_rorigin(0)
#Plot the data
pcm = ax.pcolormesh(t,r,z.T,cmap = 'hot',shading='gouraud')
ax.set_xlim([0.0,0.5*np.pi])
ax.set_ylim([1.0,10.0])
#Add colorbar and show
bar = f.colorbar(pcm)
#Create inset
ax_c, ax_p, rmax_inset = create_polar_zoom_inset(
ax, xlims=[0., 2.], ylims=[1, 2], inset_bounds=[0.4, 0.3, 0.6, 0.3])
#Plot on inset
ax_p.pcolormesh(t,r,z.T,cmap = 'hot',shading='gouraud')
#Make rorigin and rmin coincide with the original plot
ax_p.set_rorigin(0)
ax_p.set_rmin(1)
#Set rmax
ax_p.set_rmax(rmax_inset)
plt.show()

How to fix overlapping matplotlib y-axis tick labels or autoscale the plot? [duplicate]

I am trying to make a series of matplotlib plots that plot timespans for different classes of objects. Each plot has an identical x-axis and plot elements like a title and a legend. However, which classes appear in each plot differs; each plot represents a different sampling unit, each of which only contains only a subset of all the possible classes.
I am having a lot of trouble determining how to set the figure and axis dimensions. The horizontal size should always remain the same, but the vertical dimensions need to be scaled to the number of classes represented in that sampling unit. The distance between each entry on the y-axis should be equal for every plot.
It seems that my difficulties lie in the fact that I can set the absolute size (in inches) of the figure with plt.figure(figsize=(w,h)), but I can only set the size of the axis with relative dimensions (e.g., fig.add_axes([0.3,0.05,0.6,0.85]) which leads to my x-axis labels getting cut off when the number of classes is small.
Here is an MSPaint version of what I'd like to get vs. what I'm getting.
Here is a simplified version of the code I have used. Hopefully it is enough to identify the problem/solution.
import pandas as pd
import matplotlib.pyplot as plt
import pylab as pl
from matplotlib import collections as mc
from matplotlib.lines import Line2D
import seaborn as sns
# elements for x-axis
start = 1
end = 6
interval = 1 # x-axis tick interval
xticks = [x for x in range(start, end, interval)] # create x ticks
# items needed for legend construction
lw_bins = [0,10,25,50,75,90,100] # bins for line width
lw_labels = [3,6,9,12,15,18] # line widths
def make_proxy(zvalue, scalar_mappable, **kwargs):
color = 'black'
return Line2D([0, 1], [0, 1], color=color, solid_capstyle='butt', **kwargs)
for line_subset in data:
# create line collection for this run through loop
lc = mc.LineCollection(line_subset)
# create plot and set properties
sns.set(style="ticks")
sns.set_context("notebook")
############################################################
# I think the problem lies here
fig = plt.figure(figsize=(11, len(line_subset.index)*0.25))
ax = fig.add_axes([0.3,0.05,0.6,0.85])
############################################################
ax.add_collection(lc)
ax.set_xlim(left=start, right=end)
ax.set_xticks(xticks)
ax.xaxis.set_ticks_position('bottom')
ax.margins(0.05)
sns.despine(left=True)
ax.set_yticks(line_subset['order_y'])
ax.set(yticklabels=line_subset['ylabel'])
ax.tick_params(axis='y', length=0)
# legend
proxies = [make_proxy(item, lc, linewidth=item) for item in lw_labels]
leg = ax.legend(proxies, ['0-10%', '10-25%', '25-50%', '50-75%', '75-90%', '90-100%'], bbox_to_anchor=(1.0, 0.9),
loc='best', ncol=1, labelspacing=3.0, handlelength=4.0, handletextpad=0.5, markerfirst=True,
columnspacing=1.0)
for txt in leg.get_texts():
txt.set_ha("center") # horizontal alignment of text item
txt.set_x(-23) # x-position
txt.set_y(15) # y-position
You can start by defining the margins on top and bottom in units of inches. Having a fixed unit of one data unit in inches allows to calculate how large the final figure should be.
Then dividing the margin in inches by the figure height gives the relative margin in units of figure size, this can be supplied to the figure using subplots_adjust, given the subplots has been added with add_subplot.
A minimal example:
import numpy as np
import matplotlib.pyplot as plt
data = [np.random.rand(i,2) for i in [2,5,8,4,3]]
height_unit = 0.25 #inch
t = 0.15; b = 0.4 #inch
for d in data:
height = height_unit*(len(d)+1)+t+b
fig = plt.figure(figsize=(5, height))
ax = fig.add_subplot(111)
ax.set_ylim(-1, len(d))
fig.subplots_adjust(bottom=b/height, top=1-t/height, left=0.2, right=0.9)
ax.barh(range(len(d)),d[:,1], left=d[:,0], ec="k")
ax.set_yticks(range(len(d)))
plt.show()

matplotlib separating scatterplot points and creating a divisionary curve

I'm attempting to create a divisionary curve on a scatter plot in matplotlib that would divide my scatterplot according to marker size.
The (x,y) are phi0 and phi0dot and I'm coloring/sizing according a to third variable 'e-folds'. I'd like to draw an 'S' shaped curve that divides the plot into small, black markers and large, cyan markers.
Here is a sample scatterplot run with a very few number of points for an example. Ultimately I will run with tens of thousands of points of data such that the divisionary would be much finer and more obviously 'S' shaped. This is roughly what I have in mind.
My code thus far looks like this:
# Set up the PDF
pdf_pages = PdfPages(outfile)
plt.rcParams["font.family"] = "serif"
# Create the canvas
canvas = plt.figure(figsize=(14.0, 14.0), dpi=100)
plt.subplot(1, 1, 1)
for a, phi0, phi0dot, efolds in datastore:
if efolds[-1] > 65:
plt.scatter(phi0[0], phi0dot[0], s=200, color='aqua')
else:
plt.scatter(phi0[0], phi0dot[0], s=30, color='black')
# Apply labels
plt.xlabel(r"$\phi_0$")
plt.ylabel(r"$\dot{\phi}_0$")
# Finish the file
pdf_pages.savefig(canvas)
pdf_pages.close()
print("Finished!")
This type of separation is very akin to what I'd like to do, but don't see immediately how I would extend this to my problem. Any advice would be much appreciated.
I would assume that the separation line between the differently classified points is a simple contour line along the threshold value.
Here I'm assuming classification takes values of 0 or 1, hence one can draw a contour along 0.5,
ax.contour(x,y,clas, [0.5])
Example:
import numpy as np
import matplotlib.pyplot as plt
# Some data on a grid
x,y = np.meshgrid(np.arange(20), np.arange(10))
z = np.sin(y+1) + 2*np.cos(x/5) + 2
fig, ax = plt.subplots()
# Threshold; values above the threshold belong to another class as those below.
thresh = 2.5
clas = z > thresh
size = 100*clas + 30*~clas
# scatter plot
ax.scatter(x.flatten(), y.flatten(), s = size.flatten(), c=clas.flatten(), cmap="bwr")
# threshold line
ax.contour(x,y,clas, [.5], colors="k", linewidths=2)
plt.show()

Matplotlib plot has slanted lines

I'm trying to plot projections of coordinates onto a line, but for some reason, Matplotlib is plotting the projections in a slightly slanted manner. Ideally, I would like the (blue) projections to be perpendicular to the (green) line. Here's an image of how it looks with sample data:
As you can see, the angles between the blue lines and the green line are slightly obtuse instead of right. I tried playing around with the rotation parameter to the annotate function, but this did not help. The code for this plot is below, although the data might look a bit different since the random generator is not seeded:
import numpy as np
import matplotlib.pyplot as plt
prefs = {'color':'purple','edgecolors':'black'}
X = np.dot(np.random.rand(2,2), np.random.rand(2,50)).T
pts = np.linspace(-1,1)
v1_m = 0.8076549717643662
plt.scatter(X[:,0],X[:,1],**prefs)
plt.plot(pts, [v1_m*x for x in pts], color='lightgreen')
for x,y in X:
# slope of connecting line
# y = mx+b
m = -np.reciprocal(v1_m)
b = y-m*x
# find intersecting point
zx = b/(v1_m-m)
zy = v1_m*zx
# draw line
plt.annotate('',(zx,zy),(x,y),arrowprops=dict(linewidth=2,arrowstyle='-',color='lightblue'))
plt.show()
The problem lies in the unequal axes which makes it look like they are not at a right angle. Use plt.axis('equal') to have equal axis spans on x- and y-axis and a square figure with equal height and width. plt.axis('scaled') works the same way. As pointed out by #CedricZoppolo, you should set the equal aspect ratios before plt.show(). As per docs, setting the aspect ratio to "equal" means
same scaling from data to plot units for x and y
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,8))
# Your code here
plt.axis('equal')
plt.show()
Choosing a square figure is not necessary as it works also with rectangular figures as
fig = plt.figure(figsize=(8,6))
# Your code here
plt.axis('equal')
plt.show()
The blue lines not being perpendicular is due to axis not being equal.
You just need to add below line before plt.show()
plt.gca().set_aspect('equal')
Below you can see the resulted graph:

plotting coordinate as a matrix matplotlib python

I have a set of coordinates, say [(2,3),(45,4),(3,65)]
I need to plot them as a matrix is there anyway I can do this in matplotlib so I want it to have this sort of look http://imgur.com/Q6LLhmk
Edit: My original answer used ax.scatter. There is a problem with this: If two points are side-by-side, ax.scatter may draw them with a bit of space in between, depending on the scale:
For example, with
data = np.array([(2,3),(3,3)])
Here is a zoomed-in detail:
So here is a alternative solution that fixes this problem:
import matplotlib.pyplot as plt
import numpy as np
data = np.array([(2,3),(3,3),(45,4),(3,65)])
N = data.max() + 5
# color the background white (1 is white)
arr = np.ones((N,N), dtype = 'bool')
# color the dots black (0)
arr[data[:,1], data[:,0]] = 0
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.imshow(arr, interpolation='nearest', cmap = 'gray')
ax.invert_yaxis()
# ax.axis('off')
plt.show()
No matter how much you zoom in, the adjacent squares at (2,3) and (3,3) will remain side-by-side.
Unfortunately, unlike ax.scatter, using ax.imshow requires building an N x N array, so it could be more memory-intensive than using ax.scatter. That should not be a problem unless data contains very large numbers, however.

Categories