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

Skip to content

Commit 2d6640d

Browse files
Rework 3d coordinates mouse hover, now displays coordinate of underlying view pane
Get 3d ortho coordinates working Get non-1 focal lengths working Docs
1 parent 2b05ace commit 2d6640d

File tree

6 files changed

+105
-108
lines changed

6 files changed

+105
-108
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
3D hover coordinates
2+
--------------------
3+
4+
The x, y, z coordinates displayed in 3D plots were previously showing
5+
nonsensical values. This has been fixed to report the coordinate on the view
6+
pane directly beneath the mouse cursor. This is likely to be most useful when
7+
viewing 3D plots along a primary axis direction when using an orthographic
8+
projection. Note that there is still no way to directly display the coordinates
9+
of the plotted data points.

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 80 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,45 +1036,81 @@ def format_zdata(self, z):
10361036
val = func(z)
10371037
return val
10381038

1039-
def format_coord(self, xd, yd):
1039+
def format_coord(self, xv, yv, renderer=None):
10401040
"""
10411041
Given the 2D view coordinates attempt to guess a 3D coordinate.
10421042
Looks for the nearest edge to the point and then assumes that
10431043
the point is at the same z location as the nearest point on the edge.
10441044
"""
1045-
10461045
if self.M is None:
1047-
return ''
1046+
coords = ''
10481047

1049-
if self.button_pressed in self._rotate_btn:
1048+
elif self.button_pressed in self._rotate_btn:
10501049
# ignore xd and yd and display angles instead
10511050
norm_elev = art3d._norm_angle(self.elev)
10521051
norm_azim = art3d._norm_angle(self.azim)
10531052
norm_roll = art3d._norm_angle(self.roll)
1054-
return (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
1055-
f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
1056-
f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
1057-
).replace("-", "\N{MINUS SIGN}")
1058-
1059-
# nearest edge
1060-
p0, p1 = min(self._tunit_edges(),
1061-
key=lambda edge: proj3d._line2d_seg_dist(
1062-
(xd, yd), edge[0][:2], edge[1][:2]))
1063-
1064-
# scale the z value to match
1065-
x0, y0, z0 = p0
1066-
x1, y1, z1 = p1
1067-
d0 = np.hypot(x0-xd, y0-yd)
1068-
d1 = np.hypot(x1-xd, y1-yd)
1069-
dt = d0+d1
1070-
z = d1/dt * z0 + d0/dt * z1
1071-
1072-
x, y, z = proj3d.inv_transform(xd, yd, z, self.M)
1073-
1074-
xs = self.format_xdata(x)
1075-
ys = self.format_ydata(y)
1076-
zs = self.format_zdata(z)
1077-
return f'x={xs}, y={ys}, z={zs}'
1053+
coords = (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
1054+
f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
1055+
f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
1056+
).replace("-", "\N{MINUS SIGN}")
1057+
1058+
else:
1059+
p1 = self._calc_coord(xv, yv, renderer)
1060+
xs = self.format_xdata(p1[0])
1061+
ys = self.format_ydata(p1[1])
1062+
zs = self.format_zdata(p1[2])
1063+
coords = f'x={xs}, y={ys}, z={zs}'
1064+
1065+
return coords
1066+
1067+
def _get_camera_loc(self):
1068+
"""
1069+
Returns the current camera location in data coordinates.
1070+
"""
1071+
cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
1072+
c = np.array([cx, cy, cz])
1073+
r = np.array([dx, dy, dz])
1074+
1075+
if self._focal_length == np.inf: # orthographic projection
1076+
eye = c + self._view_w * self._dist * r / self._box_aspect
1077+
else: # perspective projection
1078+
eye = c + self._view_w * self._dist * r / self._box_aspect * self._focal_length
1079+
return eye
1080+
1081+
def _calc_coord(self, xv, yv, renderer=None):
1082+
"""
1083+
Given the 2D view coordinates, find the point on the nearest axis pane
1084+
that lies directly below those coordinates. Returns a 3D point in data
1085+
coordinates.
1086+
"""
1087+
if self._focal_length == np.inf: # orthographic projection
1088+
zv = 1
1089+
else: # perspective projection
1090+
zv = -1 / self._focal_length
1091+
1092+
# Convert point on view plane to data coordinates
1093+
p1 = np.array(proj3d.inv_transform(xv, yv, zv, self.M))
1094+
vec = self._get_camera_loc() - p1
1095+
1096+
# Get the pane locations for each of the axes
1097+
pane_locs = []
1098+
for axis in self._axis_map.values():
1099+
xys, loc = axis.active_pane(renderer)
1100+
pane_locs.append(loc)
1101+
1102+
# Find the distance to the nearest pane
1103+
scales = np.zeros(3)
1104+
for i in range(3):
1105+
if vec[i] == 0:
1106+
scales[i] = np.inf
1107+
else:
1108+
scales[i] = (p1[i] - pane_locs[i]) / vec[i]
1109+
scale = scales[np.argmin(abs(scales))]
1110+
1111+
# Calculate the point on the closest pane
1112+
p2 = p1 - scale*vec
1113+
return p2
10781114

10791115
def _on_move(self, event):
10801116
"""
@@ -1296,21 +1332,27 @@ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
12961332
scale_z : float
12971333
Scale factor for the z data axis.
12981334
"""
1299-
# Get the axis limits and centers
1335+
# Get the axis centers and ranges
1336+
cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
1337+
1338+
# Set the scaled axis limits
1339+
self.set_xlim3d(cx - dx*scale_x/2, cx + dx*scale_x/2)
1340+
self.set_ylim3d(cy - dy*scale_y/2, cy + dy*scale_y/2)
1341+
self.set_zlim3d(cz - dz*scale_z/2, cz + dz*scale_z/2)
1342+
1343+
def _get_w_centers_ranges(self):
1344+
"""Get 3D world centers and axis ranges."""
1345+
# Calculate center of axis limits
13001346
minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
13011347
cx = (maxx + minx)/2
13021348
cy = (maxy + miny)/2
13031349
cz = (maxz + minz)/2
13041350

1305-
# Scale the data range
1306-
dx = (maxx - minx)*scale_x
1307-
dy = (maxy - miny)*scale_y
1308-
dz = (maxz - minz)*scale_z
1309-
1310-
# Set the scaled axis limits
1311-
self.set_xlim3d(cx - dx/2, cx + dx/2)
1312-
self.set_ylim3d(cy - dy/2, cy + dy/2)
1313-
self.set_zlim3d(cz - dz/2, cz + dz/2)
1351+
# Calculate range of axis limits
1352+
dx = (maxx - minx)
1353+
dy = (maxy - miny)
1354+
dz = (maxz - minz)
1355+
return cx, cy, cz, dx, dy, dz
13141356

13151357
def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
13161358
"""

lib/mpl_toolkits/mplot3d/axis3d.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,20 @@ def _get_tickdir(self):
321321
tickdir = np.roll(info_i, -j)[np.roll(tickdirs_base, j)][i]
322322
return tickdir
323323

324+
def active_pane(self, renderer):
325+
mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer)
326+
info = self._axinfo
327+
index = info['i']
328+
if not highs[index]:
329+
loc = mins[index]
330+
plane = self._PLANES[2 * index]
331+
else:
332+
loc = maxs[index]
333+
plane = self._PLANES[2 * index + 1]
334+
xys = [tc[p] for p in plane]
335+
self._set_pane_pos(xys)
336+
return xys, loc
337+
324338
def draw_pane(self, renderer):
325339
"""
326340
Draw pane.
@@ -330,19 +344,9 @@ def draw_pane(self, renderer):
330344
renderer : `~matplotlib.backend_bases.RendererBase` subclass
331345
"""
332346
renderer.open_group('pane3d', gid=self.get_gid())
333-
334-
mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer)
335-
336-
info = self._axinfo
337-
index = info['i']
338-
if not highs[index]:
339-
plane = self._PLANES[2 * index]
340-
else:
341-
plane = self._PLANES[2 * index + 1]
342-
xys = [tc[p] for p in plane]
347+
xys, loc = self.active_pane(renderer)
343348
self._set_pane_pos(xys)
344349
self.pane.draw(renderer)
345-
346350
renderer.close_group('pane3d')
347351

348352
@artist.allow_rasterization

lib/mpl_toolkits/mplot3d/proj3d.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,6 @@
88
from matplotlib import _api
99

1010

11-
def _line2d_seg_dist(p, s0, s1):
12-
"""
13-
Return the distance(s) from point(s) *p* to segment(s) (*s0*, *s1*).
14-
15-
Parameters
16-
----------
17-
p : (ndim,) or (N, ndim) array-like
18-
The points from which the distances are computed.
19-
s0, s1 : (ndim,) or (N, ndim) array-like
20-
The xy(z...) coordinates of the segment endpoints.
21-
"""
22-
s0 = np.asarray(s0)
23-
s01 = s1 - s0 # shape (ndim,) or (N, ndim)
24-
s0p = p - s0 # shape (ndim,) or (N, ndim)
25-
l2 = s01 @ s01 # squared segment length
26-
# Avoid div. by zero for degenerate segments (for them, s01 = (0, 0, ...)
27-
# so the value of l2 doesn't matter; this just replaces 0/0 by 0/1).
28-
l2 = np.where(l2, l2, 1)
29-
# Project onto segment, without going past segment ends.
30-
p1 = s0 + np.multiply.outer(np.clip(s0p @ s01 / l2, 0, 1), s01)
31-
return ((p - p1) ** 2).sum(axis=-1) ** (1/2)
32-
33-
3411
def world_transformation(xmin, xmax,
3512
ymin, ymax,
3613
zmin, zmax, pb_aspect=None):
@@ -220,10 +197,8 @@ def inv_transform(xs, ys, zs, M):
220197
iM = linalg.inv(M)
221198
vec = _vec_pad_ones(xs, ys, zs)
222199
vecr = np.dot(iM, vec)
223-
try:
200+
if vecr[3] != 0:
224201
vecr = vecr / vecr[3]
225-
except OverflowError:
226-
pass
227202
return vecr[0], vecr[1], vecr[2]
228203

229204

Binary file not shown.

lib/mpl_toolkits/mplot3d/tests/test_axes3d.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,39 +1116,6 @@ def test_world():
11161116
[0, 0, 0, 1]])
11171117

11181118

1119-
@mpl3d_image_comparison(['proj3d_lines_dists.png'])
1120-
def test_lines_dists():
1121-
fig, ax = plt.subplots(figsize=(4, 6), subplot_kw=dict(aspect='equal'))
1122-
1123-
xs = (0, 30)
1124-
ys = (20, 150)
1125-
ax.plot(xs, ys)
1126-
p0, p1 = zip(xs, ys)
1127-
1128-
xs = (0, 0, 20, 30)
1129-
ys = (100, 150, 30, 200)
1130-
ax.scatter(xs, ys)
1131-
1132-
dist0 = proj3d._line2d_seg_dist((xs[0], ys[0]), p0, p1)
1133-
dist = proj3d._line2d_seg_dist(np.array((xs, ys)).T, p0, p1)
1134-
assert dist0 == dist[0]
1135-
1136-
for x, y, d in zip(xs, ys, dist):
1137-
c = Circle((x, y), d, fill=0)
1138-
ax.add_patch(c)
1139-
1140-
ax.set_xlim(-50, 150)
1141-
ax.set_ylim(0, 300)
1142-
1143-
1144-
def test_lines_dists_nowarning():
1145-
# No RuntimeWarning must be emitted for degenerate segments, see GH#22624.
1146-
s0 = (10, 30, 50)
1147-
p = (20, 150, 180)
1148-
proj3d._line2d_seg_dist(p, s0, s0)
1149-
proj3d._line2d_seg_dist(np.array(p), s0, s0)
1150-
1151-
11521119
def test_autoscale():
11531120
fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
11541121
assert ax.get_zscale() == 'linear'

0 commit comments

Comments
 (0)