Setting aspect ratio of 3D plot - python

I am trying to plot a 3D image of the seafloor from the data of a sonar run over a 500m by 40m portion of the seafloor. I am using matplotlib/mplot3d with Axes3D and I want to be able to change the aspect ratio of the axes so that the x & y axis are to scale. An example script with generated data rather than the real data is:
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
# Create figure.
fig = plt.figure()
ax = fig.gca(projection = '3d')
# Generate example data.
R, Y = np.meshgrid(np.arange(0, 500, 0.5), np.arange(0, 40, 0.5))
z = 0.1 * np.abs(np.sin(R/40) * np.sin(Y/6))
# Plot the data.
surf = ax.plot_surface(R, Y, z, cmap=cm.jet, linewidth=0)
fig.colorbar(surf)
# Set viewpoint.
ax.azim = -160
ax.elev = 30
# Label axes.
ax.set_xlabel('Along track (m)')
ax.set_ylabel('Range (m)')
ax.set_zlabel('Height (m)')
# Save image.
fig.savefig('data.png')
And the output image from this script:
Now I would like to change it so that 1 metre in the along-track (x) axis is the same as 1 metre in the range (y) axis (or maybe a different ratio depending on the relative sizes involved). I would also like to set the ratio of the z-axis, again not neccessarily to 1:1 due to the relative sizes in the data, but so the axis is smaller than the current plot.
I have tried building and using this branch of matplotlib, following the example script in this message from the mailing list, but adding the ax.pbaspect = [1.0, 1.0, 0.25] line to my script (having uninstalled the 'standard' version of matplotlib to ensure the custom version was being used) didn't make any difference in the generated image.
Edit: So the desired output would be something like the following (crudely edited with Inkscape) image. In this case I haven't set a 1:1 ratio on the x/y axes because that looks ridiculously thin, but I have spread it out so it isn't square as on the original output.

Add following code before savefig:
ax.auto_scale_xyz([0, 500], [0, 500], [0, 0.15])
If you want no square axis:
edit the get_proj function inside site-packages\mpl_toolkits\mplot3d\axes3d.py:
xmin, xmax = np.divide(self.get_xlim3d(), self.pbaspect[0])
ymin, ymax = np.divide(self.get_ylim3d(), self.pbaspect[1])
zmin, zmax = np.divide(self.get_zlim3d(), self.pbaspect[2])
then add one line to set pbaspect:
ax = fig.gca(projection = '3d')
ax.pbaspect = [2.0, 0.6, 0.25]

The answer to this question works perfectly for me. And you do not need to set up any ratio, it does everything automatically.

An issue has been opened over at github: https://github.com/matplotlib/matplotlib/issues/8593
The above solutions don't seem to work any more. One has to edit the get_proj function inside site-packages\mpl_toolkits\mplot3d\axes3d.py in the following way now:
try:
self.localPbAspect = self.pbaspect
except AttributeError:
self.localPbAspect = [1,1,1]
xmin, xmax = ( lim / self.localPbAspect[0] for lim in self.get_xlim3d() )
ymin, ymax = ( lim / self.localPbAspect[1] for lim in self.get_ylim3d() )
zmin, zmax = ( lim / self.localPbAspect[2] for lim in self.get_zlim3d() )

That how i solved the wasted space problem:
try:
self.localPbAspect=self.pbaspect
zoom_out = (self.localPbAspect[0]+self.localPbAspect[1]+self.localPbAspect[2])
except AttributeError:
self.localPbAspect=[1,1,1]
zoom_out = 0
xmin, xmax = self.get_xlim3d() / self.localPbAspect[0]
ymin, ymax = self.get_ylim3d() / self.localPbAspect[1]
zmin, zmax = self.get_zlim3d() / self.localPbAspect[2]
# transform to uniform world coordinates 0-1.0,0-1.0,0-1.0
worldM = proj3d.world_transformation(xmin, xmax,
ymin, ymax,
zmin, zmax)
# look into the middle of the new coordinates
R = np.array([0.5*self.localPbAspect[0], 0.5*self.localPbAspect[1], 0.5*self.localPbAspect[2]])
xp = R[0] + np.cos(razim) * np.cos(relev) * (self.dist+zoom_out)
yp = R[1] + np.sin(razim) * np.cos(relev) * (self.dist+zoom_out)
zp = R[2] + np.sin(relev) * (self.dist+zoom_out)
E = np.array((xp, yp, zp))

Related

polar pcolormesh plot projected onto cartopy map

To simplify, as much as possible, a question I already asked, how would you OVERLAY or PROJECT a polar plot onto a cartopy map.
phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar'))
im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.grid(True)
that results in
Over a cartopy map like this...
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
gc.collect()
I'd like to project this polar plot over an arbitrary lon/lat... I can convert the polar theta/phi into lon/lat, but lon/lat coords (used on the map) are more 'cartesian like' than polar, hence you cannot just substitute lon/lat for theta/phi ... This is a conceptual problem. How would you tackle it?
Firstly, the data must be prepared/transformed into certain projection coordinates for use as input. And the instruction/option of the data's CRS must be specified correctly when used in the plot statement.
In your specific case, you need to transform your data into (long,lat) values.
XX = X/np.pi*180 # wrap around data in EW direction
YY = Y*9 # spread across N hemisphere
And plot it with an instruction transform=ccrs.PlateCarree().
ax.pcolormesh(XX,YY,Z, cmap=mpl.cm.jet_r,shading='auto',
transform=ccrs.PlateCarree())
The same (XX,YY,Z) data set can be plotted on orthographic projection.
Edit1
Update of the code and plots.
Part 1 (Data)
import matplotlib.colors
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
import matplotlib.pyplot as mpl
import cartopy.feature as cfeature
#
# Part 1
#
phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar'))
im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.grid(True)
Part 2 The required code and output.
#
# Part 2
#
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land',
resolution, edgecolor='black', alpha=0.7,
facecolor=cfeature.COLORS['land']))
ax.set_extent([-180, 180, -90, 90], crs=ccrs.PlateCarree())
def scale_position(lat_deg, lon_deg, rad_deg):
# Two operations:
# 1. manipulates X,Y data and get (XX,YY)
# 2. create proper projection of (XX,YY), `rotpole_proj`
# Returns: XX,YY,rotpole_proj
# For X data
XX = X/np.pi*180 #always wrap around EW direction
# For Y data
# The cone data: min=0, max=10 --> (90-rad),90
# rad_deg = radius of the display area
top = 90
btm = top-rad_deg
YY = btm + (Y/Y.max())*rad_deg
# The proper coordinate system
rotpole_proj = ccrs.RotatedPole(pole_latitude=lat_deg, pole_longitude=lon_deg)
# Finally,
return XX,YY,rotpole_proj
# Location 1 (Asia)
XX1, YY1, rotpole_proj1 = scale_position(20, 100, 20)
ax.pcolormesh(XX1, YY1, Z, cmap=mpl.cm.jet_r,
transform=rotpole_proj1)
# Location 2 (Europe)
XX2, YY2, rotpole_proj2 = scale_position(62, -6, 8)
ax.pcolormesh(XX2, YY2, Z, cmap=mpl.cm.jet_r,
transform=rotpole_proj2)
# Location 3 (N America)
XX3, YY3, rotpole_proj3 = scale_position(29, -75, 30)
ax.pcolormesh(XX3, YY3, Z, cmap=mpl.cm.jet_r,shading='auto',
transform=rotpole_proj3)
#gc.collect()
plt.show()
This solution does NOT account for the projection point being at some altitude above the globe... I can do that part, so I really have trouble mapping the meshgrid to lon/lat so the work with the PREVIOUSLY GENERATES values of Z.
Here's a simple mapping directly from polar to cart:
X_cart = np.array([[p*np.sin(t) for p in phis] for t in thetas]).T
Y_cart = np.array([[p*np.cos(t) for p in phis] for t in thetas]).T
# Need to map cartesian XY to Z that is compatbile with above...
Z_cart = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X) # This Z does NOT map to cartesian X,Y
print(X_cart.shape,Y_cart.shape,Z_cart.shape)
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
im = ax.pcolormesh(X_cart*2,Y_cart*2, Z_cart, cmap=mpl.cm.jet_r, shading='auto') # c=mapper.to_rgba(Z_cart), cmap=mpl.cm.jet_r)
gc.collect()
Which maps the polar plot center to lon/lat (0,0):
I'm close... I somehow need to move my cartesian coords to the proper lon/lat (the satellite sub-point) and then scale it appropriately. Have the set of lon/lat but I'm screwing up the meshgrid somehow... ???
The sphere_intersect() routine returns lon/lat for projection of theta/phi on the globe (that works)... The bit that doesn't work is building the meshgrid that replaces X,Y:
lons = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg,
lonlat=True)[0] for az in thetas for el in phis], dtype='object')
lats = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg,
lonlat=True)[1] for az in thetas for el in phis], dtype='object')
long, latg = np.meshgrid(lons,lats) # THIS IS A PROBLEM I BELIEVE...
and the pcolormesh makes a mess...

How to draw colored rectangles around grouped clusters in dendogram?

I try to add colored rectangle to dendrogram results like as follow:
this is my dendrogram codes:
from scipy.cluster.hierarchy import dendrogram
...
plt.figure(figsize=(250, 100))
labelsize=20
ticksize=15
plt.title(file_name.split(".")[0], fontsize=labelsize)
plt.xlabel('stock', fontsize=labelsize)
plt.ylabel('distance', fontsize=labelsize)
dendrogram(
Z,
leaf_rotation=90., # rotates the x axis labels
leaf_font_size=8., # font size for the x axis labels
labels = corr.columns
)
pylab.yticks(fontsize=ticksize)
pylab.xticks(rotation=-90, fontsize=ticksize)
However, this is only add colorful line not a rectangle like in the above image. How can I create image like this?
Thanks
You can loop through the generated path collections and draw a bounding box.
Optionally, you could set the height to the color_threshold= parameter, which defaults to Z[:, 2].max() * 0.7.
The last collection is are the unclassified lines, so the example code below loops through all earlier collections.
import matplotlib.pyplot as plt
from scipy.cluster import hierarchy
import numpy as np
N = 15
ytdist = np.random.randint(10, 1000, N * (N + 1) // 2)
Z = hierarchy.linkage(ytdist)
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
dn1 = hierarchy.dendrogram(Z, ax=ax)
for coll in ax.collections[:-1]: # the last collection is the ungrouped level
xmin, xmax = np.inf, -np.inf
ymax = -np.inf
for p in coll.get_paths():
box = p.get_extents()
(x0, _), (x1, y1) = p.get_extents().get_points()
xmin = min(xmin, x0)
xmax = max(xmax, x1)
ymax = max(ymax, y1)
rec = plt.Rectangle((xmin - 4, 0), xmax - xmin + 8, ymax*1.05,
facecolor=coll.get_color()[0], alpha=0.2, edgecolor="none")
ax.add_patch(rec)
plt.show()

Changing aspect ratio for 3D plots on Matplotlib

I am trying to set up the aspect ratio for 3D plots using Matplotlib.
Following the answer for this question: Setting aspect ratio of 3D plot
I kind of applied the solution as:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
def get_proj(self):
"""
Create the projection matrix from the current viewing position.
elev stores the elevation angle in the z plane
azim stores the azimuth angle in the (x, y) plane
dist is the distance of the eye viewing point from the object point.
"""
# chosen for similarity with the initial view before gh-8896
relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
#EDITED TO HAVE SCALED AXIS
xmin, xmax = np.divide(self.get_xlim3d(), self.pbaspect[0])
ymin, ymax = np.divide(self.get_ylim3d(), self.pbaspect[1])
zmin, zmax = np.divide(self.get_zlim3d(), self.pbaspect[2])
# transform to uniform world coordinates 0-1, 0-1, 0-1
worldM = proj3d.world_transformation(xmin, xmax,
ymin, ymax,
zmin, zmax)
# look into the middle of the new coordinates
R = self.pbaspect / 2
xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
zp = R[2] + np.sin(relev) * self.dist
E = np.array((xp, yp, zp))
self.eye = E
self.vvec = R - E
self.vvec = self.vvec / np.linalg.norm(self.vvec)
if abs(relev) > np.pi/2:
# upside down
V = np.array((0, 0, -1))
else:
V = np.array((0, 0, 1))
zfront, zback = -self.dist, self.dist
viewM = proj3d.view_transformation(E, R, V)
projM = self._projection(zfront, zback)
M0 = np.dot(viewM, worldM)
M = np.dot(projM, M0)
return M
Axes3D.get_proj = get_proj
Then I'm creating a sample data and plotting as:
y,z,x = np.meshgrid(range(10),-np.arange(5)[::-1],range(20))
d = np.sin(x)+np.sin(y)+np.sin(z)
iy,ix = 0,-1
iz = -1
fig,ax = plt.subplots(figsize=(5,5),subplot_kw={'projection':'3d'})
ax.pbaspect = np.array([1, 1, 1])#np.array([2.0, 1.0, 0.5])
ax.contourf(x[iz], y[iz], d[iz],zdir='z',offset=0)
ax.contourf(x[:,iy,:],d[:,iy,:],z[:,iy,:],zdir='y',offset=y.min())
ax.contourf(d[:,:,ix],y[:,:,ix],z[:,:,ix],zdir='x',offset=x.max())
color = '0.3'
ax.plot(x[0,iy,:],y[0,iy,:],y[0,iy,:]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,iy,0]*0+x.max(),y[:,iy,0],z[:,iy,0],color,linewidth=1,zorder=1e4)
ax.plot(x[0,:,ix],y[0,:,ix],y[0,:,ix]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,0,ix],y[:,0,ix]*0+y.min(),z[:,0,ix],color,linewidth=1,zorder=1e4)
ax.set(xlim=[x.min(),x.max()],ylim=[y.min(),y.max()],zlim=[z.min(),z.max()])
fig.tight_layout()
If I set the pbaspect parameter as (1,1,1) I obtain:
But If I set it for (2,1,0.5) the axis seems to be correct, but it crops the data somehow:
Even if I let the xlim,ylim and zlim automatic. There is something strange with the aspect too.
Something tells me that the the axis as not orthogonal.
Does someone know how to correct the aspect ratio for this?
I would also like to know how to avoid the axis being cropped by the figure limits.
I searched on the web so long, but I could not find a better solution for this.
Update:
I tried using less than 1 values for pbaspect as suggested and it gets beter, but still crops the data:
You can try to change figsize and adjust subplots margins like below:
fig = plt.figure(figsize=(10,6))
fig.subplots_adjust(left=0.2, right=0.8, top=0.8, bottom=0.2)
ax = fig.gca(projection='3d')
ax.pbaspect = np.array([2, 1, 0.5])
ax.contourf(x[iz], y[iz], d[iz],zdir='z',offset=0)
ax.contourf(x[:,iy,:],d[:,iy,:],z[:,iy,:],zdir='y',offset=y.min())
ax.contourf(d[:,:,ix],y[:,:,ix],z[:,:,ix],zdir='x',offset=x.max())
ax.plot(x[0,iy,:],y[0,iy,:],y[0,iy,:]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,iy,0]*0+x.max(),y[:,iy,0],z[:,iy,0],color,linewidth=1,zorder=1e4)
ax.plot(x[0,:,ix],y[0,:,ix],y[0,:,ix]*0,color,linewidth=1,zorder=1e4)
ax.plot(x[:,0,ix],y[:,0,ix]*0+y.min(),z[:,0,ix],color,linewidth=1,zorder=1e4)
plt.show()
Output for (2,1,0.5):
Regarding disappearing graphics there is Matplotlib issue:
The problem occurs due to the reduction of 3D data down to 2D + z-order scalar. A single value represents the 3rd dimension for all parts of 3D objects in a collection. Therefore, when the bounding boxes of two collections intersect, it becomes possible for this artifact to occur. Furthermore, the intersection of two 3D objects (such as polygons or patches) can not be rendered properly in matplotlib’s 2D rendering engine.

How to change the length of axes for 3D plots in matplotlib [duplicate]

I have this so far:
x,y,z = data.nonzero()
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(x, y, z, zdir='z', c= 'red')
plt.savefig("plot.png")
Which creates:
What I'd like to do is stretch this out to make the Z axis 9 times taller and keep X and Y the same. I'd like to keep the same coordinates though.
So far I tried this guy:
fig = plt.figure(figsize=(4.,35.))
But that just stretches out the plot.png image.
The code example below provides a way to scale each axis relative to the others. However, to do so you need to modify the Axes3D.get_proj function. Below is an example based on the example provided by matplot lib: http://matplotlib.org/1.4.0/mpl_toolkits/mplot3d/tutorial.html#line-plots
(There is a shorter version at the end of this answer)
from mpl_toolkits.mplot3d.axes3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
import matplotlib as mpl
import numpy as np
import matplotlib.pyplot as plt
#Make sure these are floating point values:
scale_x = 1.0
scale_y = 2.0
scale_z = 3.0
#Axes are scaled down to fit in scene
max_scale=max(scale_x, scale_y, scale_z)
scale_x=scale_x/max_scale
scale_y=scale_y/max_scale
scale_z=scale_z/max_scale
#Create scaling matrix
scale = np.array([[scale_x,0,0,0],
[0,scale_y,0,0],
[0,0,scale_z,0],
[0,0,0,1]])
print scale
def get_proj_scale(self):
"""
Create the projection matrix from the current viewing position.
elev stores the elevation angle in the z plane
azim stores the azimuth angle in the x,y plane
dist is the distance of the eye viewing point from the object
point.
"""
relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
xmin, xmax = self.get_xlim3d()
ymin, ymax = self.get_ylim3d()
zmin, zmax = self.get_zlim3d()
# transform to uniform world coordinates 0-1.0,0-1.0,0-1.0
worldM = proj3d.world_transformation(
xmin, xmax,
ymin, ymax,
zmin, zmax)
# look into the middle of the new coordinates
R = np.array([0.5, 0.5, 0.5])
xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
zp = R[2] + np.sin(relev) * self.dist
E = np.array((xp, yp, zp))
self.eye = E
self.vvec = R - E
self.vvec = self.vvec / proj3d.mod(self.vvec)
if abs(relev) > np.pi/2:
# upside down
V = np.array((0, 0, -1))
else:
V = np.array((0, 0, 1))
zfront, zback = -self.dist, self.dist
viewM = proj3d.view_transformation(E, R, V)
perspM = proj3d.persp_transformation(zfront, zback)
M0 = np.dot(viewM, worldM)
M = np.dot(perspM, M0)
return np.dot(M, scale);
Axes3D.get_proj=get_proj_scale
"""
You need to include all the code above.
From here on you should be able to plot as usual.
"""
mpl.rcParams['legend.fontsize'] = 10
fig = plt.figure(figsize=(5,5))
ax = fig.gca(projection='3d')
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
ax.plot(x, y, z, label='parametric curve')
ax.legend()
plt.show()
Standard output:
Scaled by (1, 2, 3):
Scaled by (1, 1, 3):
The reason I particularly like this method,
Swap z and x, scale by (3, 1, 1):
Below is a shorter version of the code.
from mpl_toolkits.mplot3d.axes3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
import matplotlib as mpl
import numpy as np
import matplotlib.pyplot as plt
mpl.rcParams['legend.fontsize'] = 10
fig = plt.figure(figsize=(5,5))
ax = fig.gca(projection='3d')
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
"""
Scaling is done from here...
"""
x_scale=1
y_scale=1
z_scale=2
scale=np.diag([x_scale, y_scale, z_scale, 1.0])
scale=scale*(1.0/scale.max())
scale[3,3]=1.0
def short_proj():
return np.dot(Axes3D.get_proj(ax), scale)
ax.get_proj=short_proj
"""
to here
"""
ax.plot(z, y, x, label='parametric curve')
ax.legend()
plt.show()
Please note that the answer below simplifies the patch, but uses the same underlying principle as the answer by #ChristianSarofeen.
Solution
As already indicated in other answers, it is not a feature that is currently implemented in matplotlib. However, since what you are requesting is simply a 3D transformation that can be applied to the existing projection matrix used by matplotlib, and thanks to the wonderful features of Python, this problem can be solved with a simple oneliner:
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([scale_x, scale_y, scale_z, 1]))
where scale_x, scale_y and scale_z are values from 0 to 1 that will re-scale your plot along each of the axes accordingly. ax is simply the 3D axes which can be obtained with ax = fig.gca(projection='3d')
Explanation
To explain, the function get_proj of Axes3D generates the projection matrix from the current viewing position. Multiplying it by a scaling matrix:
scale_x, 0, 0
0, scale_y, 0
0, 0, scale_z
0, 0, 1
includes the scaling into the projection used by the renderer. So, what we are doing here is substituting the original get_proj function with an expression taking the result of the original get_proj and multiplying it by the scaling matrix.
Example
To illustrate the result with the standard parametric function example:
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca(projection='3d')
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z ** 2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
# OUR ONE LINER ADDED HERE:
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([0.5, 0.5, 1, 1]))
ax.plot(x, y, z)
plt.show()
for values 0.5, 0.5, 1, we get:
while for values 0.2, 1.0, 0.2, we get:
In my case I wanted to stretch z-axis 2 times for better point visibility
from mpl_toolkits import mplot3d
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# plt.rcParams["figure.figsize"] = (10,200)
# plt.rcParams["figure.autolayout"] = True
ax = plt.axes(projection='3d')
ax.set_box_aspect(aspect = (1,1,2))
ax.plot(dataX,dataY,dataZ)
I looks like by default, mplot3d will leave quite a bit of room at the top and bottom of a very tall plot. But, you can trick it into filling that space using fig.subplots_adjust, and extending the top and bottom out of the normal plotting area (i.e. top > 1 and bottom < 0). Some trial and error here is probably needed for your particular plot.
I've created some random arrays for x, y, and z with limits similar to your plot, and have found the parameters below (bottom=-0.15, top = 1.2) seem to work ok.
You might also want to change ax.view_init to set a nice viewing angle.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
from numpy import random
# Make some random data with similar limits to the OP's example
x,y,z=random.rand(3,100)
z*=250
y*=800
y+=900
x*=350
x+=1200
fig=plt.figure(figsize=(4,35))
# Set the bottom and top outside the actual figure limits,
# to stretch the 3D axis
fig.subplots_adjust(bottom=-0.15,top=1.2)
ax = fig.add_subplot(111, projection='3d')
# Change the viewing angle to an agreeable one
ax.view_init(2,None)
ax.scatter(x, y, z, zdir='z', c= 'red')
plt.savefig("plot.png")
Sounds like you're trying to adjust the scale of the plot. I don't think there's a way to stretch a linear scale to user specifications, but you can use set_yscale(), set_xscale(), set_zscale() to alter the scales with respect to each other.
Intuitively, set_yscale(log), set_xscale(log), set_zscale(linear) might solve your problems.
A likely better option: specify a stretch, set them all to symlog with the same log base and then specify the Z-axis's symlog scale with the linscalex/linscaley kwargs to your specifications.
More here:
http://matplotlib.org/mpl_toolkits/mplot3d/api.html
I found this while searching on a similar problem. After experimenting a bit, perhaps I can share some of my prelim findings here..matplotlib library is VAST!! (am a newcomer). Note that quite akin to this question, all i wanted was to 'visually' stretch the chart without distorting it.
Background story (only key code snippets are shown to avoid unnecessary clutter for those who know the library, and if you want a run-able code please drop a comment):
I have three 1-d ndarrays representing the X,Y and Z data points respectively. Clearly I can't use plot_surface (as it requires 2d ndarrays for each dim) so I went for the extremely useful plot_trisurf:
fig = plt.figure()
ax = Axes3D(fig)
3d_surf_obj = ax.plot_trisurf(X, Y, Z_defl, cmap=cm.jet,linewidth=0,antialiased=True)
You can think of the plot like a floating barge deforming in waves...As you can see, the axes stretch make it pretty deceiving visually (note that x is supposed to be at x6 times longer than y and >>>>> z). While the plot points are correct, I wanted something more visually 'stretched' at the very least. Was looking for A QUICK FIX, if I may. Long story cut short, I found a bit of success with...'figure.figsize' general setting (see snippet below).
matplotlib.rcParams.update({'font.serif': 'Times New Roman',
'font.size': 10.0,
'axes.labelsize': 'Medium',
'axes.labelweight': 'normal',
'axes.linewidth': 0.8,
###########################################
# THIS IS THE IMPORTANT ONE FOR STRETCHING
# default is [6,4] but...i changed it to
'figure.figsize':[15,5] # THIS ONE #
})
For [15,5] I got something like...
Pretty neat!!
So I started to push it.... and got up to [20,6] before deciding to settle there..
If you want to try for visually stretching the vertical axis, try with ratios like... [7,10], which in this case gives me ...
Not too shabby !
Should do it for visual prowess.
Multiply all your z values by 9,
ax.scatter(x, y, 9*z, zdir='z', c= 'red')
And then give the z-axis custom plot labels and spacing.
ax.ZTick = [0,-9*50, -9*100, -9*150, -9*200];
ax.ZTickLabel = {'0','-50','-100','-150','-200'};

matplotlib (mplot3d) - how to increase the size of an axis (stretch) in a 3D Plot?

I have this so far:
x,y,z = data.nonzero()
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(x, y, z, zdir='z', c= 'red')
plt.savefig("plot.png")
Which creates:
What I'd like to do is stretch this out to make the Z axis 9 times taller and keep X and Y the same. I'd like to keep the same coordinates though.
So far I tried this guy:
fig = plt.figure(figsize=(4.,35.))
But that just stretches out the plot.png image.
The code example below provides a way to scale each axis relative to the others. However, to do so you need to modify the Axes3D.get_proj function. Below is an example based on the example provided by matplot lib: http://matplotlib.org/1.4.0/mpl_toolkits/mplot3d/tutorial.html#line-plots
(There is a shorter version at the end of this answer)
from mpl_toolkits.mplot3d.axes3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
import matplotlib as mpl
import numpy as np
import matplotlib.pyplot as plt
#Make sure these are floating point values:
scale_x = 1.0
scale_y = 2.0
scale_z = 3.0
#Axes are scaled down to fit in scene
max_scale=max(scale_x, scale_y, scale_z)
scale_x=scale_x/max_scale
scale_y=scale_y/max_scale
scale_z=scale_z/max_scale
#Create scaling matrix
scale = np.array([[scale_x,0,0,0],
[0,scale_y,0,0],
[0,0,scale_z,0],
[0,0,0,1]])
print scale
def get_proj_scale(self):
"""
Create the projection matrix from the current viewing position.
elev stores the elevation angle in the z plane
azim stores the azimuth angle in the x,y plane
dist is the distance of the eye viewing point from the object
point.
"""
relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
xmin, xmax = self.get_xlim3d()
ymin, ymax = self.get_ylim3d()
zmin, zmax = self.get_zlim3d()
# transform to uniform world coordinates 0-1.0,0-1.0,0-1.0
worldM = proj3d.world_transformation(
xmin, xmax,
ymin, ymax,
zmin, zmax)
# look into the middle of the new coordinates
R = np.array([0.5, 0.5, 0.5])
xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
zp = R[2] + np.sin(relev) * self.dist
E = np.array((xp, yp, zp))
self.eye = E
self.vvec = R - E
self.vvec = self.vvec / proj3d.mod(self.vvec)
if abs(relev) > np.pi/2:
# upside down
V = np.array((0, 0, -1))
else:
V = np.array((0, 0, 1))
zfront, zback = -self.dist, self.dist
viewM = proj3d.view_transformation(E, R, V)
perspM = proj3d.persp_transformation(zfront, zback)
M0 = np.dot(viewM, worldM)
M = np.dot(perspM, M0)
return np.dot(M, scale);
Axes3D.get_proj=get_proj_scale
"""
You need to include all the code above.
From here on you should be able to plot as usual.
"""
mpl.rcParams['legend.fontsize'] = 10
fig = plt.figure(figsize=(5,5))
ax = fig.gca(projection='3d')
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
ax.plot(x, y, z, label='parametric curve')
ax.legend()
plt.show()
Standard output:
Scaled by (1, 2, 3):
Scaled by (1, 1, 3):
The reason I particularly like this method,
Swap z and x, scale by (3, 1, 1):
Below is a shorter version of the code.
from mpl_toolkits.mplot3d.axes3d import Axes3D
from mpl_toolkits.mplot3d import proj3d
import matplotlib as mpl
import numpy as np
import matplotlib.pyplot as plt
mpl.rcParams['legend.fontsize'] = 10
fig = plt.figure(figsize=(5,5))
ax = fig.gca(projection='3d')
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
"""
Scaling is done from here...
"""
x_scale=1
y_scale=1
z_scale=2
scale=np.diag([x_scale, y_scale, z_scale, 1.0])
scale=scale*(1.0/scale.max())
scale[3,3]=1.0
def short_proj():
return np.dot(Axes3D.get_proj(ax), scale)
ax.get_proj=short_proj
"""
to here
"""
ax.plot(z, y, x, label='parametric curve')
ax.legend()
plt.show()
Please note that the answer below simplifies the patch, but uses the same underlying principle as the answer by #ChristianSarofeen.
Solution
As already indicated in other answers, it is not a feature that is currently implemented in matplotlib. However, since what you are requesting is simply a 3D transformation that can be applied to the existing projection matrix used by matplotlib, and thanks to the wonderful features of Python, this problem can be solved with a simple oneliner:
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([scale_x, scale_y, scale_z, 1]))
where scale_x, scale_y and scale_z are values from 0 to 1 that will re-scale your plot along each of the axes accordingly. ax is simply the 3D axes which can be obtained with ax = fig.gca(projection='3d')
Explanation
To explain, the function get_proj of Axes3D generates the projection matrix from the current viewing position. Multiplying it by a scaling matrix:
scale_x, 0, 0
0, scale_y, 0
0, 0, scale_z
0, 0, 1
includes the scaling into the projection used by the renderer. So, what we are doing here is substituting the original get_proj function with an expression taking the result of the original get_proj and multiplying it by the scaling matrix.
Example
To illustrate the result with the standard parametric function example:
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.gca(projection='3d')
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z ** 2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
# OUR ONE LINER ADDED HERE:
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([0.5, 0.5, 1, 1]))
ax.plot(x, y, z)
plt.show()
for values 0.5, 0.5, 1, we get:
while for values 0.2, 1.0, 0.2, we get:
In my case I wanted to stretch z-axis 2 times for better point visibility
from mpl_toolkits import mplot3d
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
# plt.rcParams["figure.figsize"] = (10,200)
# plt.rcParams["figure.autolayout"] = True
ax = plt.axes(projection='3d')
ax.set_box_aspect(aspect = (1,1,2))
ax.plot(dataX,dataY,dataZ)
I looks like by default, mplot3d will leave quite a bit of room at the top and bottom of a very tall plot. But, you can trick it into filling that space using fig.subplots_adjust, and extending the top and bottom out of the normal plotting area (i.e. top > 1 and bottom < 0). Some trial and error here is probably needed for your particular plot.
I've created some random arrays for x, y, and z with limits similar to your plot, and have found the parameters below (bottom=-0.15, top = 1.2) seem to work ok.
You might also want to change ax.view_init to set a nice viewing angle.
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
from numpy import random
# Make some random data with similar limits to the OP's example
x,y,z=random.rand(3,100)
z*=250
y*=800
y+=900
x*=350
x+=1200
fig=plt.figure(figsize=(4,35))
# Set the bottom and top outside the actual figure limits,
# to stretch the 3D axis
fig.subplots_adjust(bottom=-0.15,top=1.2)
ax = fig.add_subplot(111, projection='3d')
# Change the viewing angle to an agreeable one
ax.view_init(2,None)
ax.scatter(x, y, z, zdir='z', c= 'red')
plt.savefig("plot.png")
Sounds like you're trying to adjust the scale of the plot. I don't think there's a way to stretch a linear scale to user specifications, but you can use set_yscale(), set_xscale(), set_zscale() to alter the scales with respect to each other.
Intuitively, set_yscale(log), set_xscale(log), set_zscale(linear) might solve your problems.
A likely better option: specify a stretch, set them all to symlog with the same log base and then specify the Z-axis's symlog scale with the linscalex/linscaley kwargs to your specifications.
More here:
http://matplotlib.org/mpl_toolkits/mplot3d/api.html
I found this while searching on a similar problem. After experimenting a bit, perhaps I can share some of my prelim findings here..matplotlib library is VAST!! (am a newcomer). Note that quite akin to this question, all i wanted was to 'visually' stretch the chart without distorting it.
Background story (only key code snippets are shown to avoid unnecessary clutter for those who know the library, and if you want a run-able code please drop a comment):
I have three 1-d ndarrays representing the X,Y and Z data points respectively. Clearly I can't use plot_surface (as it requires 2d ndarrays for each dim) so I went for the extremely useful plot_trisurf:
fig = plt.figure()
ax = Axes3D(fig)
3d_surf_obj = ax.plot_trisurf(X, Y, Z_defl, cmap=cm.jet,linewidth=0,antialiased=True)
You can think of the plot like a floating barge deforming in waves...As you can see, the axes stretch make it pretty deceiving visually (note that x is supposed to be at x6 times longer than y and >>>>> z). While the plot points are correct, I wanted something more visually 'stretched' at the very least. Was looking for A QUICK FIX, if I may. Long story cut short, I found a bit of success with...'figure.figsize' general setting (see snippet below).
matplotlib.rcParams.update({'font.serif': 'Times New Roman',
'font.size': 10.0,
'axes.labelsize': 'Medium',
'axes.labelweight': 'normal',
'axes.linewidth': 0.8,
###########################################
# THIS IS THE IMPORTANT ONE FOR STRETCHING
# default is [6,4] but...i changed it to
'figure.figsize':[15,5] # THIS ONE #
})
For [15,5] I got something like...
Pretty neat!!
So I started to push it.... and got up to [20,6] before deciding to settle there..
If you want to try for visually stretching the vertical axis, try with ratios like... [7,10], which in this case gives me ...
Not too shabby !
Should do it for visual prowess.
Multiply all your z values by 9,
ax.scatter(x, y, 9*z, zdir='z', c= 'red')
And then give the z-axis custom plot labels and spacing.
ax.ZTick = [0,-9*50, -9*100, -9*150, -9*200];
ax.ZTickLabel = {'0','-50','-100','-150','-200'};

Categories