python - Is there a way to tell where a user has clicked a 3D surface plot in matplotlib? - Stack Overflow

admin2025-04-17  3

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'
Share Improve this question edited Feb 3 at 14:57 pippo1980 3,2013 gold badges17 silver badges40 bronze badges asked Jan 31 at 21:45 PetSvenPetSven 3824 silver badges13 bronze badges 4
  • Voted to close, code is missing – pippo1980 Commented Feb 2 at 20:58
  • @pippo1980 I have added my code – PetSven Commented Feb 3 at 13:50
  • retracted vote to close – pippo1980 Commented Feb 3 at 14:39
  • matplotlib: getting coordinates in 3D plots by a mouseevent Could it be a duplicate ??? Not sure. But it is driving me crazy that my matplotlib backend (plt.show() ---> window that opens and show the plot) nicely displays the right 3d projections coords in x y and z too !!!!!!! – pippo1980 Commented Feb 3 at 18:09
Add a comment  | 

1 Answer 1

Reset to default -1

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

转载请注明原文地址:http://anycun.com/QandA/1744845358a88424.html