Plotting a surface for a robot reach bubble in Python - python

I'm trying to simulate a robot reach bubble. The goal would be to export it into a CAD file and visualize the possible workspace. My approach was to plot all potential endpoints using forward kinematics for the robot, considering linkage lengths and joint limits. This may be a brute-force way to generate the endpoints (Rx, Ry, Rz), but it comes out to be very accurate (at least for 2D examples). [This image shows the provided workspace for an IRB robot and my results when plotting the points in 2D][1]
[1]: https://i.stack.imgur.com/g3cP7.jpg
I can display a three-dimensional figure of the bubble as a scatterplot; however, to export it into a CAD file, I need to mesh it first, which requires converting it into a surface, as I understand. This is the part I'm having trouble with.
Using matplotlib's ax.surface_plot(Rx, Ry, Rz) I receive an error stating that Rz must be a 2-dimensional value. I fiddled with np.meshgrid() and np.mgrid() functions but have been unable to create a simple surface of the bubble. What can I do to convert this scatterplot into a surface? Is there another approach that I'm missing?
Another thing that dawned on me is that I'd likely want to remove some of the intermediate points inside the reach bubble. Ideally, the surface would be composed of the outer ends and the hollow points from the center radius.
Below is a code that results in 1D arrays:
# Reach bubble 3D
import NumPy as np
import matplotlib.pyplot as plt
# Initialize figure and label axes
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
dr = np.pi/180 # Degree to radian multiple
pi = np.pi
# Define important robot dimensions and axis limits for IRB 1100
z0 = 0.327 # Fixed height from base to A1
link1 = 0.28
link2 = 0.3
a1 = np.linspace(0, 2*pi, 8) # Angle limits for axes
a2 = np.linspace(-115*dr, 113*dr, 12)
a3 = np.linspace(-205*dr, 55*dr, 12)
Rx = []
Ry = []
Rz = []
for i1 in a1:
for i2 in a2:
for i3 in a3:
r = link1*np.sin(i2) + link2*np.sin(pi/2+i2+i3)
Rx.append(r*np.cos(i1))
Ry.append(r*np.sin(i1))
Rz.append(z0 + link1*np.cos(i2) + link2*np.cos(pi/2+i2+i3))
# Plot reach points
ax.scatter(Rx, Ry, Rz, c='r', marker='o', alpha = 0.2)
plt.show()
Below is a code that results in 3D arrays but doesn't use for loops:
# 3D Reach bubble for robot
import numpy as np
import matplotlib.pyplot as plt
# Initialize figure and label axes
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
pi = np.pi
dr = pi/180 # Degree to radian multiple
# Define important robot dimensions and axis limits for GP8
z0 = 0.327 # Fixed height from base to A1
link1 = 0.28
link2 = 0.3
link3 = 0.064
a1, a2, a3 = np.mgrid[0:2*pi:15j, -115*dr:113*dr:13j, -205*dr:55*dr:17j]
r = link1*np.sin(a2) + link2*np.sin(pi/2+a2+a3)
Rx = r*np.cos(a1)
Ry = r*np.sin(a1)
Rz = z0 + link1*np.cos(a2) + link2*np.cos(pi/2+a2+a3)
# Plot reach points
ax.plot_surface(Rx, Ry, Rz, color = 'Red', alpha = 0.2)
ax.scatter(Rx, Ry, Rz, c='r', marker='o', alpha = 0.2)

Related

How can I rotate a graph that contains an imshow in matplotlib?

I have some microscopic scan data in a rectangular grid that was scanned by an X/Y scanner. The object I'm scanning contains markers in a nice, rectangular, orthogonal pattern on its surface.
Due to practicalities, we can only align the object to the X/Y scanner to within a few degrees when fixing it to the X/Y scanner table. We compensate for this by some calibration measurement, and then performing scans that are nicely aligned with the object we're trying to scan (by moving the X and Y axes simultaneously).
This works well, but now I want to display this data in a coordinate system that corresponds to the X/Y scanner's axes. So the scanned, rectangular dataset will be rotated a bit in that coordinate system. (See the second image below for what I mean).
For presentation purposes however, I would prefer to have the scanned image nicely horizontal, and the axes rotated. I am trying to accomplish this in Matplotlib but I'm failing miserably.
I can get a rotated Axes (see the third image), but I am unable to figure out what kind of transforms I should do to get the ax3.imshow() to show its data at its intended position.
Here's where I am so far:
The program to generate the test data and the image I have so far is shown below.
I'd appreciate any help I can get. I'm afraid to say the documentation of matplotlib leaves a lot to be desired, so I hope somebody who knows matplotlib inside out may chime in.
#! /usr/bin/env python3
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import Affine2D
import mpl_toolkits.axisartist.floating_axes as floating_axes
import random
angle_deg = 5.0 # degrees CCW.
# Test data
NX = 5
NY = 3
x = np.zeros(shape=(NY, NX))
y = np.zeros(shape=(NY, NX))
c = np.zeros(shape=(NY, NX))
angle_rad = np.deg2rad(angle_deg)
for iy in range(NY):
for ix in range(NX):
icx = 0.5 * (NX - 1)
icy = 0.5 * (NY - 1)
# To centered coordinates, in physical units
xcp = (ix - icx) * 0.1
ycp = (iy - icy) * 0.1
# Rotate
xcpr = xcp * np.cos(angle_rad) - ycp * np.sin(angle_rad)
ycpr = xcp * np.sin(angle_rad) + ycp * np.cos(angle_rad)
# Rotate, and translated to the physical center
x[iy, ix] = xcpr + 40.0
y[iy, ix] = ycpr + 30.0
c[iy, ix] = 100 + ix + iy
fig = plt.figure()
fig.set_size_inches(15, 9)
# Add axis #1.
ax1 = fig.add_subplot(131)
ax1.set_title("imshow()")
ax1.imshow(c, origin='lower')
ax1.grid()
# Add axis #2.
ax2 = fig.add_subplot(132)
ax2.set_title("pcolormesh()")
ax2.pcolormesh(x, y, c, shading='nearest')
ax2.axis('equal')
ax2.grid()
# Add axis #3.
min_x = x.min()
max_x = x.max()
min_y = y.min()
max_y = y.max()
cx = 0.5 * (min_x + max_x)
cy = 0.5 * (min_y + max_y)
transform = Affine2D().rotate_deg(-angle_deg)
grid_helper = floating_axes.GridHelperCurveLinear(transform, extremes=(min_x, max_x, min_y, max_y))
ax3 = floating_axes.FloatingSubplot(fig, 133, grid_helper=grid_helper)
ax3.grid()
ax3.set_title("rotate the entire graph.\nHow do I do get the data to show up here,\nhorizontally like in the leftmost image!?\n")
ax3.imshow(c, origin='lower')
fig.add_subplot(ax3)
plt.show()

Using a linear equation to create a radially interpolated circle

Given a linear equation, I want to use the slope to create a circle of values around a given point, defined by the slope of the linear equation if possible
Im currently a bit far away - can only make the radial plot but do not know how to connect this with an input equation. My first thought would be to change the opacity using import matplotlib.animation as animation and looping matplotlib's alpha argument to become gradually more and more opaque. However the alpha doesnt seem to change opacity.
Code:
# lenth of radius
distance = 200
# create radius
radialVals = np.linspace(0,distance)
# 2 pi radians = full circle
azm = np.linspace(0, 2 * np.pi)
r, th = np.meshgrid(radialVals, azm)
z = (r ** 2.0) / 4.0
# creates circle
plt.subplot(projection="polar")
# add color gradient
plt.pcolormesh(th, r, z)
plt.plot(azm, r,alpha=1, ls='', drawstyle = 'steps')
#gridlines
# plt.grid()
plt.show()
Here is one way to solve it, the idea is to create a mesh, calculate the colors with a function then use imshow to visualize the mesh.
from matplotlib import pyplot as plt
import numpy as np
def create_mesh(slope,center,radius,t_x,t_y,ax,xlim,ylim):
"""
slope: the slope of the linear function
center: the center of the circle
raadius: the radius of the circle
t_x: the number of grids in x direction
t_y: the number of grids in y direction
ax: the canvas
xlim,ylim: the lims of the ax
"""
def cart2pol(x,y):
rho = np.sqrt(x**2 + y**2)
phi = np.arctan2(y,x)
return rho,phi
def linear_func(slope):
# initialize a patch and grids
patch = np.empty((t_x,t_y))
patch[:,:] = np.nan
x = np.linspace(xlim[0],xlim[1],t_x)
y = np.linspace(ylim[0],ylim[1],t_y)
x_grid,y_grid = np.meshgrid(x, y)
# centered grid
xc = np.linspace(xlim[0]-center[0],xlim[1]-center[0],t_x)
yc = np.linspace(ylim[0]-center[1],ylim[1]-center[1],t_y)
xc_grid,yc_grid = np.meshgrid(xc, yc)
rho,phi = cart2pol(xc_grid,yc_grid)
linear_values = slope * rho
# threshold controls the size of the gaussian
circle_mask = (x_grid-center[0])**2 + (y_grid-center[1])**2 < radius
patch[circle_mask] = linear_values[circle_mask]
return patch
# modify the patch
patch = linear_func(slope)
extent = xlim[0],xlim[1],ylim[0],ylim[1]
ax.imshow(patch,alpha=.6,interpolation='bilinear',extent=extent,
cmap=plt.cm.YlGn,vmin=v_min,vmax=v_max)
fig,ax = plt.subplots(nrows=1,ncols=2,figsize=(12,6))
slopes = [40,30]
centroids = [[2,2],[4,3]]
radii = [1,4]
for item in ax:item.set_xlim(0,8);item.set_ylim(0,8)
v_max,v_min = max(slopes),0
create_mesh(slopes[0],centroids[0],radii[0],t_x=300,t_y=300,ax=ax[0],xlim=(0,8),ylim=(0,8))
create_mesh(slopes[1],centroids[1],radii[1],t_x=300,t_y=300,ax=ax[1],xlim=(0,8),ylim=(0,8))
plt.show()
The output of this code is
As you can see, the color gradient of the figure on the left is not as sharp as the figure on the right because of the different slopes ([40,30]).
Also note that, these two lines of code
v_max,v_min = max(slopes),0
ax.imshow(patch,alpha=.6,interpolation='bilinear',extent=extent,
cmap=plt.cm.YlGn,vmin=v_min,vmax=v_max)
are added in order to let the two subplots share the same colormap.

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.

Make a Rotating Sphere in Python

I have made this code that applies the spherical harmonics in a spherical manner as I am trying to model stellar pulsation modes. Ideally, I'd like to be able to have an image that rotates that can be saved as a gif image. I have found a few examples of code for doing this but none of it seems to apply to my code or uses python packages that aren't available to me. I'm not sure if this is too far out of my range of skills in python as I'm very much a beginner.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm, colors
from mpl_toolkits.mplot3d import Axes3D
from scipy.special import sph_harm #import package to calculate spherical harmonics
theta = np.linspace(0, 2*np.pi, 100) #setting range for theta
phi = np.linspace(0, np.pi, 100) #setting range for phi
phi, theta = np.meshgrid(phi, theta) #setting the grid for phi and theta
#Setting the cartesian coordinates of the unit sphere
#Converting phi, theta, z to cartesian coordinates
x = np.sin(phi)*np.cos(theta)
y = np.sin(phi)*np.sin(theta)
z = np.cos(phi)
m, l = 4, 4 #m and l control the mode of pulsation and overall appearance of the figure
#Calculating the spherical harmonic Y(l,m) and normalizing it
figcolors = sph_harm(m, l, theta, phi).real
figmax, figmin = figcolors.max(), figcolors.min()
figcolors = (figcolors-figmin)/(figmax-figmin)
#Setting the aspect ratio to 1 which makes the sphere look spherical and not elongated
fig = plt.figure(figsize=plt.figaspect(1.)) #aspect ratio
axes = fig.add_subplot(111, projection='3d') #sets figure to 3d
#Sets the plot surface and colors of the figure where seismic is the color scheme
axes.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=cm.autumn(figcolors))
#yellow zones are cooler and compressed, red zones are warmer and expanded
#Turn off the axis planes so only the sphere is visible
axes.set_axis_off()
fig.suptitle('m=4 l=4', fontsize=18, x=0.52, y=.85)
plt.savefig('m4_l4.png') #saves a .png file of my figure
plt.show() #Plots the figure
#figure saved for m=1, 2, 3, 4 and l=2, 3, 5, 6 respectively then all 6 were put together to form a single figure
I've also got an image showing what my code outputs currently. It's just a still sphere, of course. Thank you in advance! sphere4_4
Change the last part of your code to generate a set of figures (see below). In this case I create num = 10 frames, you can change this number if you want. Then open a terminal and type
convert m4_l4*.png m4_l4.gif
And this is the result
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm, colors
from mpl_toolkits.mplot3d import Axes3D
from scipy.special import sph_harm #import package to calculate spherical harmonics
theta = np.linspace(0, 2*np.pi, 100) #setting range for theta
phi = np.linspace(0, np.pi, 100) #setting range for phi
phi, theta = np.meshgrid(phi, theta) #setting the grid for phi and theta
#Setting the cartesian coordinates of the unit sphere
#Converting phi, theta, z to cartesian coordinates
x = np.sin(phi)*np.cos(theta)
y = np.sin(phi)*np.sin(theta)
z = np.cos(phi)
m, l = 4, 4 #m and l control the mode of pulsation and overall appearance of the figure
#Calculating the spherical harmonic Y(l,m) and normalizing it
figcolors = sph_harm(m, l, theta, phi).real
figmax, figmin = figcolors.max(), figcolors.min()
figcolors = (figcolors-figmin)/(figmax-figmin)
#Setting the aspect ratio to 1 which makes the sphere look spherical and not elongated
fig = plt.figure(figsize=plt.figaspect(1.)) #aspect ratio
axes = fig.add_subplot(111, projection='3d') #sets figure to 3d
#Sets the plot surface and colors of the figure where seismic is the color scheme
axes.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=cm.autumn(figcolors))
#yellow zones are cooler and compressed, red zones are warmer and expanded
axes.set_axis_off()
fig.suptitle('m=4 l=4', fontsize=18, x=0.52, y=.85)
for idx, angle in enumerate(np.linspace(0, 360, 10)):
axes.view_init(30, angle)
plt.draw()
#Turn off the axis planes so only the sphere is visible
plt.savefig('m4_l4-%04d.png' % idx) #saves a .png file of my figure
plt.show()

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'};

Categories