# Toggle labels in 3D graph by clicking on corresponding point

Dear Community,

I am trying to make it so that it is possible to click on a plot to toggle specific text labels for points. Below is an example code. I took some help from chatGPT to construct this but I am stuck at the fact that apparently the indices of the 3D scatter point artists and of the text artists are not aligned and I can’t go further than that. So my question is: how to make it so that by clicking on a given point, the associated text label gets toggled? (if it is displayed, it is removed, and if it is not there, then it is displayed?)

Below is the code.

import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import mpl_toolkits.mplot3d as mpl3d

class IndexedText:
def __init__(self, text, index):
self.text = text
self.index = index
self.visible = True

# Sample data
d_a = 10
d_b = 20
d_d = 15
solute = 'Sample Solute'
d_a_solvent_eff = np.random.rand(5) * 30
d_b_solvent_eff = np.random.rand(5) * 30
d_d_solvent = np.random.rand(5) * 10 + 12
solvents = [f'Solvent {i}' for i in range(1, 6)]

fig, ax = plt.subplots(figsize=(4.5, 3), dpi=80, subplot_kw={'projection': '3d'})
ax.set_xlabel(r"$\delta$$_{{A}} (effective) (MPa^{{1/2}})") ax.set_ylabel(r"\delta$$_{{B}}$ (effective) (MPa$^{{1/2}}$)")
ax.set_zlabel(r"$\delta$$_{{D}}$ (MPa$^{{1/2}}$)")

# plot molecules to figure
scatter_solute = ax.scatter([d_a], [d_b], [d_d], color='blue', picker=True)
text_solute = ax.text(d_a, d_b, d_d, 'SOLUTE: %s' % (solute), size=8, zorder=1, color='blue', picker=True)
scatter_solvents = ax.scatter(d_a_solvent_eff, d_b_solvent_eff, d_d_solvent, label=solvents, color='black', picker=True)
text_solvents = [IndexedText(text, i) for i, text in enumerate(solvents)]
for i in range(len(text_solvents)):
text = ax.text(d_a_solvent_eff[i], d_b_solvent_eff[i], d_d_solvent[i], '%s' % (text_solvents[i].text), size=5, zorder=1, color='black', picker=True)
text_solvents[i].text_artist = text

ax.set_xlim([0, 30])
ax.set_ylim([0, 30])
ax.set_zlim([12, 22])
ax.mouse_init()  # make sure 3D plot is rotatable

# set up plot title
ax.set_title(solute)

# Embed figure in PySimpleGUI Canvas
layout = [[sg.Canvas(size=(450, 300), background_color='white', key='-CANVAS-', pad=(0, 0))]]
window = sg.Window('Matplotlib Plot in PySimpleGUI Canvas', layout, finalize=True)

# Embed figure in canvas and update interface to account for the change
canvas = FigureCanvasTkAgg(fig, master=window['-CANVAS-'].TKCanvas)
canvas.draw()
canvas.get_tk_widget().pack(side='top', fill='both', expand=1)

def on_pick(event):
print(f'Type of the artist: {type(event.artist)}')
print(f'Indices of picked points: {event.ind}')
if isinstance(event.artist, mpl3d.art3d.Path3DCollection):
# Access the data associated with the picked point using the index
for index in event.ind:
# Access the corresponding Text object using the loop index
text_obj = text_solvents[index]

# Get the 3D coordinates of the point
x_points, y_points, z_points = event.artist._offsets3d

x_point = x_points[index]
y_point = y_points[index]
z_point = z_points[index]

# Convert 3D coordinates to screen space
point_screen = ax.transData.transform_point((x_point, y_point))

# Get the pixel coordinates
x_screen, y_screen = point_screen[:2]

# Toggle visibility of the text artist
text_artist = text_obj.text_artist
text_artist.set_visible(not text_artist.get_visible())

print(f'Solvent {index + 1} is {text_obj.text}')
print(f'2D Coordinates: ({x_point}, {y_point})')
print(f'Screen Coordinates: ({x_screen}, {y_screen})')
print("mouse coordinates", event.mouseevent.xdata, event.mouseevent.ydata, event.mouseevent.x, event.mouseevent.y)

# Redraw the canvas to reflect the changes
canvas.draw()

# Get the pixel coordinates of all text labels
labels_screen = [text_obj.text_artist.get_position()
for text_obj in text_solvents]

print('Screen Coordinates of Text Labels:')
for i, label_screen in enumerate(labels_screen):
print(f'{text_solvents[i].text}: {label_screen}')

# Redraw the canvas to reflect the changes
canvas.draw()
fig.canvas.mpl_connect('pick_event', on_pick)

while True:
event, values = window.read()

if event == sg.WIN_CLOSED:
break

window.close()



After lots of trying, I solved my issue. Here is the general solution I found for introducing toggling of labels for a Matplotlib plot embedded in FigureCanvasTkAgg:

import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d as mpl3d
import numpy as np
import PySimpleGUI as sg

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from mpl_toolkits.mplot3d              import proj3d

def toggle_3Dplot_labels_on_pick(event, ax, canvas):                                    #function that toggles points in matplotlib plots (with data stored in ax) embedded in a FigureCanvasTkAgg canvas

#to use this function, you'll need to bind it to the figure, in the main code, using something like:
# fig.canvas.mpl_connect('pick_event', lambda event: toggle_3Dplot_labels_on_pick(event, ax, canvas))

if isinstance(event.artist, mpl3d.art3d.Path3DCollection):

# Get the scatter plot data from the event artist
scatter_data = event.artist._offsets3d

# Extract x, y, and z coordinates
x_coordinates = scatter_data[0].data
y_coordinates = scatter_data[1].data
z_coordinates = scatter_data[2]

# Combine them into a NumPy array
points = np.column_stack((x_coordinates, y_coordinates, z_coordinates))

#for each point we check if it is closer to the mouse pointer
min_distance = float('inf')
closest_point = None
for i in range(len(points)):
# Convert 3D coordinates to screen space
x_screen, y_screen, _ = proj3d.proj_transform(points[i][0], points[i][1], points[i][2], ax.get_proj())

# Calculate Euclidean distance
distance = np.sqrt((event.mouseevent.xdata - x_screen)**2 + (event.mouseevent.ydata - y_screen)**2)

# Update closest point if the current point is closer
if distance < min_distance:
min_distance = distance
closest_point = (points[i][0], points[i][1])

#get text objects from the ax object
text_objects = [artist for artist in ax.texts if isinstance(artist, mpl3d.art3d.Text3D)]

# Get the coordinates of all text labels
labels_screen = [text_obj.get_position()
for text_obj in text_objects]

#for each label we check if it is closer to the clicked point
min_distance_label = float('inf')
closest_label = None
closest_index = None

for i, label_screen in enumerate(labels_screen):
# Calculate Euclidean distance
distance = np.sqrt((closest_point[0] - label_screen[0])**2 + (closest_point[1] - label_screen[1])**2)

# Update closest point if the current point is closer
if distance < min_distance_label:
min_distance_label = distance
closest_label = label_screen
closest_index = i

text_obj = text_objects[closest_index]

# Toggle visibility of the text artist
text_obj.set_visible(not text_obj.get_visible())

# Redraw the canvas to reflect the changes
canvas.draw()


Of course if you wish to use this, you’ll need to have the option picker=True when scattering the points, such as in the line:

ax.scatter(x, y, z, label = labs, color='black', picker=True)