I am creating a 3D plot using Matplotlib. The plot contains points, lines and surfaces displayed in 3D space. Using the “picker=true” option for the lines and points I can make them clickable. And when the user clicks on them I can return the location of their pointer in 3D space using “get_data_3d”. I can’t get this to work with the 3d surfaces though. They are poly3dcollections instead of line3d. They don’t have a “get_data_3d” function. Any idea how I can return where the user clicks on a 3d surface?
# Imports
import matplotlib.pyplot as plt
import numpy
# If a point is selected, print its location
def onPick(event):
points = event.artist
print(points.get_data_3d())
# Create a 3D plot
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# Create a plane in 3D space
x = numpy.arange(-50, 50, 1)
y = numpy.arange(-50, 50, 1)
z = numpy.array([[5 for _ in x] for _ in y])
x, y = numpy.meshgrid(x, y)
# Plot the plane
ax.plot_surface(x, y, z, alpha=0.2, color="y", picker=True, pickradius=5)
# Call a function if the plane is clicked on
fig.canvas.mpl_connect('pick_event', onPick)
# Show the plot
plt.show()
clicking mouse on one point of the surface I get:
AttributeError: 'Poly3DCollection' object has no attribute 'get_data_3d'
I am creating a 3D plot using Matplotlib. The plot contains points, lines and surfaces displayed in 3D space. Using the “picker=true” option for the lines and points I can make them clickable. And when the user clicks on them I can return the location of their pointer in 3D space using “get_data_3d”. I can’t get this to work with the 3d surfaces though. They are poly3dcollections instead of line3d. They don’t have a “get_data_3d” function. Any idea how I can return where the user clicks on a 3d surface?
# Imports
import matplotlib.pyplot as plt
import numpy
# If a point is selected, print its location
def onPick(event):
points = event.artist
print(points.get_data_3d())
# Create a 3D plot
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# Create a plane in 3D space
x = numpy.arange(-50, 50, 1)
y = numpy.arange(-50, 50, 1)
z = numpy.array([[5 for _ in x] for _ in y])
x, y = numpy.meshgrid(x, y)
# Plot the plane
ax.plot_surface(x, y, z, alpha=0.2, color="y", picker=True, pickradius=5)
# Call a function if the plane is clicked on
fig.canvas.mpl_connect('pick_event', onPick)
# Show the plot
plt.show()
clicking mouse on one point of the surface I get:
AttributeError: 'Poly3DCollection' object has no attribute 'get_data_3d'
DISCLAIMER: Valid for Matplotlib 3.7
Ok, reverted to google plus copying and getting from here:
matplotlib: getting coordinates in 3D plots by a mouseevent --> Answer
and here:
With what to replace the deprecated line2d_seg_dist function of matplotlib? --> Answer
keeping in mind that :
Axes3D.tunit_edges(vals=None, M=None) is Deprecated since version 3.7:
and Matplotlib stable is at 3.10.0
but I am lucky being outdated, with:
simple_pick_info_modded.py
:
# import matplotlib
# print('matplotlib version : ', matplotlib.__version__)
import numpy as np
import matplotlib.transforms as mtransforms
from mpl_toolkits import mplot3d
import time
def line2d_seg_dist(p1, p2, p0):
"""distance(s) from line defined by p1 - p2 to point(s) p0
p0[0] = x(s)
p0[1] = y(s)
intersection point p = p1 + u*(p2-p1)
and intersection point lies within segment if u is between 0 and 1
"""
x21 = p2[0] - p1[0]
y21 = p2[1] - p1[1]
# print('p0[0]' , p0[0] , type(p0[0]))
# print('p1[0]' , p1[0] , type(p1[0]))
# time.sleep(5)
try :
x01 = np.asarray(p0[0]) - p1[0]
y01 = np.asarray(p0[1]) - p1[1]
u = (x01*x21 + y01*y21) / (x21**2 + y21**2)
u = np.clip(u, 0, 1)
d = np.hypot(x01 - u*x21, y01 - u*y21)
return d
except:
return 'out of ax'
def get_xyz_mouse_click(event, ax):
"""
Get coordinates clicked by user
"""
if ax.M is None:
return {}
xd, yd = event.xdata, event.ydata
p = (xd, yd)
### https://matplotlib.org/3.8.2/api/_as_gen/mpl_toolkits.mplot3d.axes3d.Axes3D.tunit_edges.html --> Deprecated since version 3.7:
edges = ax.tunit_edges() ####
#### https://matplotlib.org/3.2.2/api/_as_gen/mpl_toolkits.mplot3d.proj3d.line2d_seg_dist.html --> Deprecated since version 3.1.
# ldists = [(mplot3d.proj3d.line2d_seg_dist(p0, p1, p), i) for \
# i, (p0, p1) in enumerate(edges)]
ldists = [(line2d_seg_dist(p0, p1, p), i) for \
i, (p0, p1) in enumerate(edges)]
if type(ldists) == str:
return ('point out of ax','point out of ax','point out of ax')
else:
ldists.sort()
# nearest edge
edgei = ldists[0][1]
p0, p1 = edges[edgei]
# scale the z value to match
x0, y0, z0 = p0
x1, y1, z1 = p1
try :
d0 = np.hypot(x0-xd, y0-yd)
d1 = np.hypot(x1-xd, y1-yd)
dt = d0+d1
z = d1/dt * z0 + d0/dt * z1
x, y, z = mplot3d.proj3d.inv_transform(xd, yd, z, ax.M)
return x, y, z
except:
return ('point out of ax','point out of ax','point out of ax')
and your modified code main.py
:
# Imports
import matplotlib
print('matplotlib version : ', matplotlib.__version__)
import matplotlib.pyplot as plt
import numpy
import simple_pick_info_modded
# print(simple_pick_info_modded.get_xyz_mouse_click)
from mpl_toolkits.mplot3d import Axes3D
is_a_pick_event = {'status' : False}
print('is_a_pick_event : ', is_a_pick_event , type(is_a_pick_event))
# If a point is selected, print its location
def onPick(event):
is_a_pick_event['status'] = True
print('\n\nevent : ', event)
print('is_a_pick_event on Pick: ', is_a_pick_event)
# Create a 3D plot
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
Axes3D.disable_mouse_rotation(ax) #disable rotation & zoom of graph
# Create a plane in 3D space
x = numpy.arange(-50, 50, 1)
y = numpy.arange(-50, 50, 1)
z = numpy.array([[5 for _ in x] for _ in y])
x, y = numpy.meshgrid(x, y)
# Plot the plane
plane = ax.plot_surface(x, y, z, alpha=0.2, color="y", picker=True, pickradius=0.1)
# Call a function if the plane is clicked on
fig.canvas.mpl_connect('pick_event', onPick)
def on_click(event):
print('is_a_pick_event on_click: ', is_a_pick_event)
if is_a_pick_event['status'] == True:
x,y,z = simple_pick_info_modded.get_xyz_mouse_click(event, ax)
print(f'Clicked at: x={x}, y={y}, z={z}')
is_a_pick_event['status'] = False
print('is_a_pick_event on click after print : ', is_a_pick_event)
else:
pass
fig.canvas.mpl_connect('button_press_event', on_click)
# Show the plot
plt.show()
I can get:
The x,y,z coordinates printed are the same displayed by my matplotlib backend on the plt.show()
window, but with my great regrets adding to the click event when is a pick event too a
ax.scatter(x,y,z)
creates points that are far from the surface.
This last bit was solved in code by adding Axes3D.disable_mouse_rotation(ax)
to disable rotation & zoom of camera and setting axis limits by:
ax.set_xlim([-50, 50])
ax.set_ylim([-50, 50])
ax.set_zlim([4.8, 5.2])
Now all the points belong (are seen) on the surface(plane in this example) ...
EDITED
Works both on Matplotlib 3.7 and 3.10:
code main.py
:
# Imports
import matplotlib
print('matplotlib version : ', matplotlib.__version__)
import matplotlib.pyplot as plt
import numpy
import simple_pick_info_modded_310
# print(simple_pick_info_modded_310.get_xyz_mouse_click)
from mpl_toolkits.mplot3d import Axes3D
is_a_pick_event = {'status' : False}
print('is_a_pick_event : ', is_a_pick_event , type(is_a_pick_event))
# If a point is selected, print its location
def onPick(event):
is_a_pick_event['status'] = True
print('\n\nevent : ', event)
print('is_a_pick_event on Pick: ', is_a_pick_event)
# Create a 3D plot
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
Axes3D.disable_mouse_rotation(ax)
ax.set_xlim([-50, 50])
ax.set_ylim([-50, 50])
ax.set_zlim([4.8, 5.2])
# Create a plane in 3D space
x = numpy.arange(-50, 50, 1)
y = numpy.arange(-50, 50, 1)
z = numpy.array([[5 for _ in x] for _ in y])
x, y = numpy.meshgrid(x, y)
# Plot the plane
plane = ax.plot_surface(x, y, z, alpha=0.2, color="y", picker=True, pickradius=0)
# Call a function if the plane is clicked on
fig.canvas.mpl_connect('pick_event', onPick)
def on_click(event):
print('is_a_pick_event on_click: ', is_a_pick_event)
if is_a_pick_event['status'] == True:
x,y,z = simple_pick_info_modded_310.get_xyz_mouse_click(event, ax)
print('\nx :', x , type(x))
print('\ny :', y , type(y))
print('\nz :', z, type(z))
print(f'\n\nClicked at: x={x}, y={y}, z={z}')
point = ax.scatter3D(x,y,z)
print('\n\npoint : ', point, type(point))
plt.draw()
is_a_pick_event['status'] = False
print('is_a_pick_event on click after print : ', is_a_pick_event)
else:
pass
fig.canvas.mpl_connect('button_press_event', on_click)
# Show the plot
plt.show()
with:
simple_pick_info_modded_310.py
:
# import matplotlib
# print('matplotlib version : ', matplotlib.__version__)
import numpy as np
import matplotlib.transforms as mtransforms
from mpl_toolkits import mplot3d
def get_xyz_mouse_click(event, ax):
"""
Get coordinates clicked by user
"""
if ax.M is None:
return {}
xd, yd = event.xdata, event.ydata
#p = (xd, yd)
print('\nevent : ', event , type(event))
print('event.xdata and event.ydata : ', xd,yd)
print('\nevent.inaxes.format_coord(event.xdata,event.ydata) : ',event.inaxes.format_coord(event.xdata,event.ydata))
p=event.inaxes.format_coord(event.xdata,event.ydata)
print('\np : ',p , type(p))
#print([float(i.replace('−','-').split('=')[1]) for i in p.split(',')])
px = [float(i.replace('−','-').split('=')[1]) for i in p.split(',')]
print('px : ', px)
x,y,z = px
print('x : ',x,' y : ' , y ,' z : ',z)
return x, y, z