Skip to content

Commit a0b665d

Browse files
Rework 3d coordinates mouse hover, now displays coordinate of underlying view pane
1 parent 7100e9e commit a0b665d

File tree

5 files changed

+90
-108
lines changed

5 files changed

+90
-108
lines changed

lib/mpl_toolkits/mplot3d/axes3d.py

+74-38
Original file line numberDiff line numberDiff line change
@@ -1036,45 +1036,75 @@ 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, xd, yd, 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(xd, yd, 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+
eye = c + self._view_w * self._dist * r / self._box_aspect
1075+
return eye
1076+
1077+
def _calc_coord(self, xd, yd, renderer=None):
1078+
"""
1079+
Given the 2D view coordinates, find the point on the nearest axis pane
1080+
that lies directly below those coordinates.
1081+
"""
1082+
# convert to data coordinates
1083+
p1 = np.array(proj3d.inv_transform(xd, yd, -1, self.M))
1084+
1085+
vec = self._get_camera_loc() - p1
1086+
1087+
if self._focal_length == np.inf:
1088+
vec = self._view_w
1089+
1090+
# Get the pane locations for each of the axes
1091+
pane_locs = []
1092+
for axis in self._axis_map.values():
1093+
xys, loc = axis.active_pane(renderer)
1094+
pane_locs.append(loc)
1095+
1096+
# Find the distance to the nearest pane
1097+
scales = np.zeros(3)
1098+
for i in range(3):
1099+
if vec[i] == 0:
1100+
scales[i] = np.inf
1101+
else:
1102+
scales[i] = (p1[i] - pane_locs[i]) / vec[i]
1103+
scale = scales[np.argmin(abs(scales))]
1104+
1105+
# Calculate the point on the closest pane
1106+
p2 = p1 - scale*vec
1107+
return p2
10781108

10791109
def _on_move(self, event):
10801110
"""
@@ -1296,21 +1326,27 @@ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
12961326
scale_z : float
12971327
Scale factor for the z data axis.
12981328
"""
1299-
# Get the axis limits and centers
1329+
# Get the axis centers and ranges
1330+
cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
1331+
1332+
# Set the scaled axis limits
1333+
self.set_xlim3d(cx - dx*scale_x/2, cx + dx*scale_x/2)
1334+
self.set_ylim3d(cy - dy*scale_y/2, cy + dy*scale_y/2)
1335+
self.set_zlim3d(cz - dz*scale_z/2, cz + dz*scale_z/2)
1336+
1337+
def _get_w_centers_ranges(self):
1338+
"""Get 3D world centers and axis ranges."""
1339+
# Calculate center of axis limits
13001340
minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
13011341
cx = (maxx + minx)/2
13021342
cy = (maxy + miny)/2
13031343
cz = (maxz + minz)/2
13041344

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)
1345+
# Calculate range of axis limits
1346+
dx = (maxx - minx)
1347+
dy = (maxy - miny)
1348+
dz = (maxz - minz)
1349+
return cx, cy, cz, dx, dy, dz
13141350

13151351
def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
13161352
"""

lib/mpl_toolkits/mplot3d/axis3d.py

+15-11
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

+1-26
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

-33
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)