J'ai un nuage de points en 3 dimensions dans matplotlib, et j'ai mis en place des annotations, inspirées par des réponses aquí notamment celle de Don Cristobal .
J'ai mis en place un code de capture d'événements de base, mais après plusieurs jours d'essais, je n'ai toujours pas réussi à atteindre mes objectifs. Ceux-ci sont les suivants :
(i) Changez la couleur d'un point sélectionné avec le bouton gauche de la souris du bleu au bleu foncé/vert par exemple.
(ii) Supprimer tout point sélectionné au point (i) après avoir appuyé sur la touche "supprimer", y compris les annotations.
(iii) Sélectionnez plusieurs points dans (i) en utilisant un rectangle de sélection et supprimer en utilisant la touche 'delete'.
J'ai essayé de nombreuses approches, y compris l'animation du graphique pour qu'il se mette à jour en fonction des changements de données, la manipulation des paramètres de l'artiste, le changement des points de données via, par exemple, l'utilisation de l'outil de gestion des données. xs, ys, zs = graph._offsets3d
(qui ne semble pas être documenté), mais en vain.
J'ai essayé, dans la fonction onpick(event), de :
(i) Interagir avec les points via event.ind pour changer de couleur en utilisant event.artist.set_face_colour()
(ii) Supprimez les points en utilisant à la fois artist.remove()
(iii) Supprimer des points en utilisant xs, ys, zs = graph._offsets3d , en supprimant le point pertinent par indice (event.ind[0]) de xs, ys et zs, puis en réinitialisant les points du graphique via graph._offsets3d = xs_new, ys_new, zs_new
(iv) Redessiner le graphique, ou les sections pertinentes du graphique seulement (blitting ?)
sans succès !
Mon code actuel est à peu près le suivant. En fait, j'ai plusieurs centaines de points, et non les 3 de l'exemple simplifié ci-dessous. J'aimerais que le graphique se mette à jour de manière fluide si possible, bien que le fait d'obtenir quelque chose d'utilisable soit déjà une bonne chose. La plus grande partie du code pour faire cela devrait probablement résider dans 'onpick', car c'est la fonction qui traite les événements de sélection (cf. gestionnaire d'événement ). J'ai conservé certaines de mes tentatives de code, commentées, qui, je l'espère, pourront être utiles. La fonction 'forceUpdate' est censée mettre à jour l'objet graphique lors d'un déclenchement d'événement, mais je ne suis pas convaincu qu'elle fasse quoi que ce soit actuellement. function on_key(event)
ne semble pas non plus fonctionner actuellement : il doit probablement y avoir un paramètre pour déterminer les points à supprimer, par exemple tous les artistes qui ont une couleur de visage qui a été modifiée par rapport à la valeur par défaut (par exemple, supprimer tous les points qui ont une couleur bleu foncé/vert plutôt que bleu clair).
Toute aide est la bienvenue.
Le code (ci-dessous) est appelé avec :
visualize3DData (Y, ids, subindustry)
Quelques exemples de points de données sont présentés ci-dessous :
#Datapoints
Y = np.array([[ 4.82250000e+01, 1.20276889e-03, 9.14501289e-01], [ 6.17564688e+01, 5.95020883e-02, -1.56770827e+00], [ 4.55139000e+01, 9.13454423e-02, -8.12277299e+00]])
#Annotations
ids = ['a', 'b', 'c']
subindustry = 'example'
Mon code actuel est ici :
import matplotlib.pyplot as plt, numpy as np
from mpl_toolkits.mplot3d import proj3d
def visualize3DData (X, ids, subindus):
"""Visualize data in 3d plot with popover next to mouse position.
Args:
X (np.array) - array of points, of shape (numPoints, 3)
Returns:
None
"""
fig = plt.figure(figsize = (16,10))
ax = fig.add_subplot(111, projection = '3d')
graph = ax.scatter(X[:, 0], X[:, 1], X[:, 2], depthshade = False, picker = True)
def distance(point, event):
"""Return distance between mouse position and given data point
Args:
point (np.array): np.array of shape (3,), with x,y,z in data coords
event (MouseEvent): mouse event (which contains mouse position in .x and .xdata)
Returns:
distance (np.float64): distance (in screen coords) between mouse pos and data point
"""
assert point.shape == (3,), "distance: point.shape is wrong: %s, must be (3,)" % point.shape
# Project 3d data space to 2d data space
x2, y2, _ = proj3d.proj_transform(point[0], point[1], point[2], plt.gca().get_proj())
# Convert 2d data space to 2d screen space
x3, y3 = ax.transData.transform((x2, y2))
return np.sqrt ((x3 - event.x)**2 + (y3 - event.y)**2)
def calcClosestDatapoint(X, event):
""""Calculate which data point is closest to the mouse position.
Args:
X (np.array) - array of points, of shape (numPoints, 3)
event (MouseEvent) - mouse event (containing mouse position)
Returns:
smallestIndex (int) - the index (into the array of points X) of the element closest to the mouse position
"""
distances = [distance (X[i, 0:3], event) for i in range(X.shape[0])]
return np.argmin(distances)
def annotatePlot(X, index, ids):
"""Create popover label in 3d chart
Args:
X (np.array) - array of points, of shape (numPoints, 3)
index (int) - index (into points array X) of item which should be printed
Returns:
None
"""
# If we have previously displayed another label, remove it first
if hasattr(annotatePlot, 'label'):
annotatePlot.label.remove()
# Get data point from array of points X, at position index
x2, y2, _ = proj3d.proj_transform(X[index, 0], X[index, 1], X[index, 2], ax.get_proj())
annotatePlot.label = plt.annotate( ids[index],
xy = (x2, y2), xytext = (-20, 20), textcoords = 'offset points', ha = 'right', va = 'bottom',
bbox = dict(boxstyle = 'round,pad=0.5', fc = 'yellow', alpha = 0.5),
arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
fig.canvas.draw()
def onMouseMotion(event):
"""Event that is triggered when mouse is moved. Shows text annotation over data point closest to mouse."""
closestIndex = calcClosestDatapoint(X, event)
annotatePlot (X, closestIndex, ids)
def onclick(event):
print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
('double' if event.dblclick else 'single', event.button,
event.x, event.y, event.xdata, event.ydata))
def on_key(event):
"""
Function to be bound to the key press event
If the key pressed is delete and there is a picked object,
remove that object from the canvas
"""
if event.key == u'delete':
ax = plt.gca()
if ax.picked_object:
ax.picked_object.remove()
ax.picked_object = None
ax.figure.canvas.draw()
def onpick(event):
xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata
artist = event.artist
# print(dir(event.mouseevent))
ind = event.ind
# print('Artist picked:', event.artist)
# # print('{} vertices picked'.format(len(ind)))
print('ind', ind)
# # print('Pick between vertices {} and {}'.format(min(ind), max(ind) + 1))
# print('x, y of mouse: {:.2f},{:.2f}'.format(xmouse, ymouse))
# # print('Data point:', x[ind[0]], y[ind[0]])
#
# # remove = [artist for artist in pickable_artists if artist.contains(event)[0]]
# remove = [artist for artist in X if artist.contains(event)[0]]
#
# if not remove:
# # add a pt
# x, y = ax.transData.inverted().transform_point([event.x, event.y])
# pt, = ax.plot(x, y, 'o', picker=5)
# pickable_artists.append(pt)
# else:
# for artist in remove:
# artist.remove()
# plt.draw()
# plt.draw_idle()
xs, ys, zs = graph._offsets3d
print(xs[ind[0]])
print(ys[ind[0]])
print(zs[ind[0]])
print(dir(artist))
# xs[ind[0]] = 0.5
# ys[ind[0]] = 0.5
# zs[ind[0]] = 0.5
# graph._offsets3d = (xs, ys, zs)
# print(artist.get_facecolor())
# artist.set_facecolor('red')
graph._facecolors[ind, :] = (1, 0, 0, 1)
plt.draw()
def forceUpdate(event):
global graph
graph.changed()
fig.canvas.mpl_connect('motion_notify_event', onMouseMotion) # on mouse motion
fig.canvas.mpl_connect('button_press_event', onclick)
fig.canvas.mpl_connect('pick_event', onpick)
fig.canvas.mpl_connect('draw_event', forceUpdate)
plt.tight_layout()
plt.show()