Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 761f1ed

Browse files
Add a roll angle for viewing 3D plots
Add roll angle for 3d plot examples Align rotation of 3D plot by mouse with rolled axes 3D plot roll angle flake8, tests, and whats new Fix plot flipping at elev>270 Test for 3d plot with elev>270 Normalize the displayed angles for 3d plots when rotating with mouse Make elev, azim, roll ordering consistent everywhere Rename roll angle to tilt Speed up calculations for default tilt=0 Code review updates Switch tilt naming back to roll, flip rotation handedness Switch tilt naming back to roll, flip rotation handedness
1 parent d2c7b16 commit 761f1ed

File tree

9 files changed

+117
-63
lines changed

9 files changed

+117
-63
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
3D plots gained a 3rd "roll" viewing angle
2+
------------------------------------------
3+
4+
3D plots can now be viewed from any orientation with the addition of a 3rd roll
5+
angle, which rotates the plot about the viewing axis. Interactive rotation
6+
using the mouse still only controls elevation and azimuth, meaning that this
7+
feature is relevant to users who create more complex camera angles
8+
programmatically. The default roll angle of 0 is backwards-compatible with
9+
existing 3D plots.
10+
11+
.. plot::
12+
:include-source: true
13+
14+
from mpl_toolkits.mplot3d import axes3d
15+
import matplotlib.pyplot as plt
16+
fig = plt.figure()
17+
ax = fig.add_subplot(projection='3d')
18+
X, Y, Z = axes3d.get_test_data(0.05)
19+
ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
20+
ax.view_init(elev=0, azim=0, roll=30)
21+
plt.show()

examples/mplot3d/2dcollections3d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@
4343

4444
# Customize the view angle so it's easier to see that the scatter points lie
4545
# on the plane y=0
46-
ax.view_init(elev=20., azim=-35)
46+
ax.view_init(elev=20., azim=-35, roll=0)
4747

4848
plt.show()

examples/mplot3d/box3d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
)
6969

7070
# Set distance and angle view
71-
ax.view_init(40, -30)
71+
ax.view_init(40, -30, 0)
7272
ax.dist = 11
7373

7474
# Colorbar

examples/mplot3d/rotate_axes3d_sgskip.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@
2323

2424
# rotate the axes and update
2525
for angle in range(0, 360):
26-
ax.view_init(30, angle)
26+
ax.view_init(30, angle, 0)
2727
plt.draw()
2828
plt.pause(.001)

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Axes3D(Axes):
5454

5555
def __init__(
5656
self, fig, rect=None, *args,
57-
azim=-60, elev=30, sharez=None, proj_type='persp',
57+
elev=30, azim=-60, roll=0, sharez=None, proj_type='persp',
5858
box_aspect=None, computed_zorder=True,
5959
**kwargs):
6060
"""
@@ -64,10 +64,12 @@ def __init__(
6464
The parent figure.
6565
rect : (float, float, float, float)
6666
The ``(left, bottom, width, height)`` axes position.
67-
azim : float, default: -60
68-
Azimuthal viewing angle.
6967
elev : float, default: 30
7068
Elevation viewing angle.
69+
azim : float, default: -60
70+
Azimuthal viewing angle.
71+
roll : float, default: 0
72+
Roll viewing angle.
7173
sharez : Axes3D, optional
7274
Other axes to share z-limits with.
7375
proj_type : {'persp', 'ortho'}
@@ -101,6 +103,7 @@ def __init__(
101103

102104
self.initial_azim = azim
103105
self.initial_elev = elev
106+
self.initial_roll = roll
104107
self.set_proj_type(proj_type)
105108
self.computed_zorder = computed_zorder
106109

@@ -112,7 +115,7 @@ def __init__(
112115

113116
# inhibit autoscale_view until the axes are defined
114117
# they can't be defined until Axes.__init__ has been called
115-
self.view_init(self.initial_elev, self.initial_azim)
118+
self.view_init(self.initial_elev, self.initial_azim, self.initial_roll)
116119

117120
self._sharez = sharez
118121
if sharez is not None:
@@ -976,7 +979,7 @@ def clabel(self, *args, **kwargs):
976979
"""Currently not implemented for 3D axes, and returns *None*."""
977980
return None
978981

979-
def view_init(self, elev=None, azim=None, vertical_axis="z"):
982+
def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"):
980983
"""
981984
Set the elevation and azimuth of the axes in degrees (not radians).
982985
@@ -992,6 +995,10 @@ def view_init(self, elev=None, azim=None, vertical_axis="z"):
992995
The azimuth angle in the horizontal plane in degrees.
993996
If None then the initial value as specified in the `Axes3D`
994997
constructor is used.
998+
roll : float, default: None
999+
The roll angle about the viewing direction in degrees.
1000+
If None then the initial value as specified in the `Axes3D`
1001+
constructor is used.
9951002
vertical_axis : {"z", "x", "y"}, default: "z"
9961003
The axis to align vertically. *azim* rotates about this axis.
9971004
"""
@@ -1008,6 +1015,11 @@ def view_init(self, elev=None, azim=None, vertical_axis="z"):
10081015
else:
10091016
self.azim = azim
10101017

1018+
if roll is None:
1019+
self.roll = self.initial_roll
1020+
else:
1021+
self.roll = roll
1022+
10111023
self._vertical_axis = _api.check_getitem(
10121024
dict(x=0, y=1, z=2), vertical_axis=vertical_axis
10131025
)
@@ -1046,8 +1058,10 @@ def get_proj(self):
10461058

10471059
# elev stores the elevation angle in the z plane
10481060
# azim stores the azimuth angle in the x,y plane
1049-
elev_rad = np.deg2rad(self.elev)
1050-
azim_rad = np.deg2rad(self.azim)
1061+
# roll stores the roll angle about the view axis
1062+
elev_rad = np.deg2rad(art3d._norm_angle(self.elev))
1063+
azim_rad = np.deg2rad(art3d._norm_angle(self.azim))
1064+
roll_rad = np.deg2rad(art3d._norm_angle(self.roll))
10511065

10521066
# Coordinates for a point that rotates around the box of data.
10531067
# p0, p1 corresponds to rotating the box only around the
@@ -1077,7 +1091,7 @@ def get_proj(self):
10771091
V = np.zeros(3)
10781092
V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1
10791093

1080-
viewM = proj3d.view_transformation(eye, R, V)
1094+
viewM = proj3d.view_transformation(eye, R, V, roll_rad)
10811095
projM = self._projection(-self.dist, self.dist)
10821096
M0 = np.dot(viewM, worldM)
10831097
M = np.dot(projM, M0)
@@ -1165,14 +1179,15 @@ def _button_release(self, event):
11651179
def _get_view(self):
11661180
# docstring inherited
11671181
return (self.get_xlim(), self.get_ylim(), self.get_zlim(),
1168-
self.elev, self.azim)
1182+
self.elev, self.azim, self.roll)
11691183

11701184
def _set_view(self, view):
11711185
# docstring inherited
1172-
xlim, ylim, zlim, elev, azim = view
1186+
xlim, ylim, zlim, elev, azim, roll = view
11731187
self.set(xlim=xlim, ylim=ylim, zlim=zlim)
11741188
self.elev = elev
11751189
self.azim = azim
1190+
self.roll = roll
11761191

11771192
def format_zdata(self, z):
11781193
"""
@@ -1199,8 +1214,12 @@ def format_coord(self, xd, yd):
11991214

12001215
if self.button_pressed in self._rotate_btn:
12011216
# ignore xd and yd and display angles instead
1202-
return (f"azimuth={self.azim:.0f}\N{DEGREE SIGN}, "
1203-
f"elevation={self.elev:.0f}\N{DEGREE SIGN}"
1217+
norm_elev = art3d._norm_angle(self.elev)
1218+
norm_azim = art3d._norm_angle(self.azim)
1219+
norm_roll = art3d._norm_angle(self.roll)
1220+
return (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
1221+
f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
1222+
f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
12041223
).replace("-", "\N{MINUS SIGN}")
12051224

12061225
# nearest edge
@@ -1253,8 +1272,12 @@ def _on_move(self, event):
12531272
# get the x and y pixel coords
12541273
if dx == 0 and dy == 0:
12551274
return
1256-
self.elev = art3d._norm_angle(self.elev - (dy/h)*180)
1257-
self.azim = art3d._norm_angle(self.azim - (dx/w)*180)
1275+
1276+
roll = np.deg2rad(self.roll)
1277+
delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
1278+
dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
1279+
self.elev = self.elev + delev
1280+
self.azim = self.azim + dazim
12581281
self.get_proj()
12591282
self.stale = True
12601283
self.figure.canvas.draw_idle()
@@ -1267,7 +1290,8 @@ def _on_move(self, event):
12671290
minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
12681291
dx = 1-((w - dx)/w)
12691292
dy = 1-((h - dy)/h)
1270-
elev, azim = np.deg2rad(self.elev), np.deg2rad(self.azim)
1293+
elev = np.deg2rad(self.elev)
1294+
azim = np.deg2rad(self.azim)
12711295
# project xv, yv, zv -> xw, yw, zw
12721296
dxx = (maxx-minx)*(dy*np.sin(elev)*np.cos(azim) + dx*np.sin(azim))
12731297
dyy = (maxy-miny)*(-dx*np.cos(azim) + dy*np.sin(elev)*np.sin(azim))
@@ -3249,11 +3273,11 @@ def _extract_errs(err, data, lomask, himask):
32493273
quiversize = np.mean(np.diff(quiversize, axis=0))
32503274
# quiversize is now in Axes coordinates, and to convert back to data
32513275
# coordinates, we need to run it through the inverse 3D transform. For
3252-
# consistency, this uses a fixed azimuth and elevation.
3253-
with cbook._setattr_cm(self, azim=0, elev=0):
3276+
# consistency, this uses a fixed elevation, azimuth, and roll.
3277+
with cbook._setattr_cm(self, elev=0, azim=0, roll=0):
32543278
invM = np.linalg.inv(self.get_proj())
3255-
# azim=elev=0 produces the Y-Z plane, so quiversize in 2D 'x' is 'y' in
3256-
# 3D, hence the 1 index.
3279+
# elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
3280+
# 'y' in 3D, hence the 1 index.
32573281
quiversize = np.dot(invM, np.array([quiversize, 0, 0, 0]))[1]
32583282
# Quivers use a fixed 15-degree arrow head, so scale up the length so
32593283
# that the size corresponds to the base. In other words, this constant

lib/mpl_toolkits/mplot3d/proj3d.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,34 +51,41 @@ def world_transformation(xmin, xmax,
5151
[0, 0, 0, 1]])
5252

5353

54-
def view_transformation(E, R, V):
54+
def rotation_about_vector(v, angle):
55+
"""
56+
Produce a rotation matrix for an angle in radians about a vector.
57+
"""
58+
vx, vy, vz = v / np.linalg.norm(v)
59+
s = np.sin(angle)
60+
c = np.cos(angle)
61+
t = 2*np.sin(angle/2)**2 # more numerically stable than t = 1-c
62+
63+
R = np.array([
64+
[t*vx*vx + c, t*vx*vy - vz*s, t*vx*vz + vy*s],
65+
[t*vy*vx + vz*s, t*vy*vy + c, t*vy*vz - vx*s],
66+
[t*vz*vx - vy*s, t*vz*vy + vx*s, t*vz*vz + c]])
67+
68+
return R
69+
70+
71+
def view_transformation(E, R, V, roll):
5572
n = (E - R)
56-
## new
57-
# n /= np.linalg.norm(n)
58-
# u = np.cross(V, n)
59-
# u /= np.linalg.norm(u)
60-
# v = np.cross(n, u)
61-
# Mr = np.diag([1.] * 4)
62-
# Mt = np.diag([1.] * 4)
63-
# Mr[:3,:3] = u, v, n
64-
# Mt[:3,-1] = -E
65-
## end new
66-
67-
## old
68-
n = n / np.linalg.norm(n)
73+
n = n/np.linalg.norm(n)
6974
u = np.cross(V, n)
70-
u = u / np.linalg.norm(u)
71-
v = np.cross(n, u)
72-
Mr = [[u[0], u[1], u[2], 0],
73-
[v[0], v[1], v[2], 0],
74-
[n[0], n[1], n[2], 0],
75-
[0, 0, 0, 1]]
76-
#
77-
Mt = [[1, 0, 0, -E[0]],
78-
[0, 1, 0, -E[1]],
79-
[0, 0, 1, -E[2]],
80-
[0, 0, 0, 1]]
81-
## end old
75+
u = u/np.linalg.norm(u)
76+
v = np.cross(n, u) # Will be a unit vector
77+
78+
# Save some computation for the default roll=0
79+
if roll != 0:
80+
# A positive rotation of the camera is a negative rotation of the world
81+
Rroll = rotation_about_vector(n, -roll)
82+
u = np.dot(Rroll, u)
83+
v = np.dot(Rroll, v)
84+
85+
Mr = np.eye(4)
86+
Mt = np.eye(4)
87+
Mr[:3, :3] = [u, v, n]
88+
Mt[:3, -1] = -E
8289

8390
return np.dot(Mr, Mt)
8491

0 commit comments

Comments
 (0)