2 votes

Changer la couleur des points de données lors de la sélection et les supprimer en appuyant sur une touche dans un nuage de points 3d de matplotlib

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

1voto

Freya W Points 372

OK, j'ai au moins une solution partielle pour vous, sans la sélection du rectangle, mais vous pouvez sélectionner plusieurs points et les supprimer avec un seul key_event.

Pour changer la couleur, vous devez modifier graph._facecolor3d l'indice était dans ce rapport de bogue sur set_facecolor pas de réglage _facecolor3d .

Il peut également s'avérer utile de réécrire votre fonction en tant que classe afin de se débarrasser de tous les éléments nécessaires. global variables.

Ma solution comporte des parties qui ne sont pas vraiment jolies, je dois redessiner la figure après avoir supprimé des points de données, je n'ai pas réussi à faire fonctionner la suppression et la mise à jour. Voir aussi (voir EDIT 2 ci-dessous). Je n'ai pas encore implémenté ce qui se passe si le dernier point de données est supprimé.

La raison pour laquelle votre fonction on_key(event) ne fonctionnait pas était facile : vous avez oublié de le connecter.

Il s'agit donc d'une solution qui devrait satisfaire les objectifs (i) et (ii) :

import matplotlib.pyplot as plt, numpy as np
from mpl_toolkits.mplot3d import proj3d

class Class3DDataVisualizer:    
    def __init__(self, X, ids, subindus, drawNew = True):

        self.X = X;
        self.ids = ids
        self.subindus = subindus

        self.disconnect = False
        self.ind = []
        self.label = None

        if drawNew:        
            self.fig = plt.figure(figsize = (7,5))
        else:
            self.fig.delaxes(self.ax)
        self.ax = self.fig.add_subplot(111, projection = '3d')
        self.graph  = self.ax.scatter(self.X[:, 0], self.X[:, 1], self.X[:, 2], depthshade = False, picker = True, facecolors=np.repeat([[0,0,1,1]],X.shape[0], axis=0) )         
        if drawNew and not self.disconnect:
            self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.onMouseMotion(event))  # on mouse motion    
            self.fig.canvas.mpl_connect('pick_event', lambda event: self.onpick(event))
            self.fig.canvas.mpl_connect('key_press_event', lambda event: self.on_key(event))

        self.fig.tight_layout()
        self.fig.show()

    def distance(self, 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 = self.ax.transData.transform((x2, y2))

        return np.sqrt ((x3 - event.x)**2 + (y3 - event.y)**2)

    def calcClosestDatapoint(self, 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 = [self.distance (self.X[i, 0:3], event) for i in range(self.X.shape[0])]
        return np.argmin(distances)

    def annotatePlot(self, index):
        """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 self.label is not None:
            self.label.remove()
        # Get data point from array of points X, at position index
        x2, y2, _ = proj3d.proj_transform(self.X[index, 0], self.X[index, 1], self.X[index, 2], self.ax.get_proj())
        self.label = plt.annotate( self.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'))
        self.fig.canvas.draw()

    def onMouseMotion(self, event):
        """Event that is triggered when mouse is moved. Shows text annotation over data point closest to mouse."""
        closestIndex = self.calcClosestDatapoint(event)
        self.annotatePlot (closestIndex) 

    def on_key(self, 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':
            if self.ind:
                self.X = np.delete(self.X, self.ind, axis=0)
                self.ids = np.delete(ids, self.ind, axis=0)
                self.__init__(self.X, self.ids, self.subindus, False)
            else:
                print('nothing selected')

    def onpick(self, event):
        self.ind.append(event.ind)
        self.graph._facecolor3d[event.ind] = [1,0,0,1]

#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], [3,  8, -8.12277299e+00]])
#Annotations
ids = ['a', 'b', 'c', 'd']

subindustries =  'example'

Class3DDataVisualizer(Y, ids, subindustries)

Pour mettre en œuvre votre sélection rectangulaire, vous devriez remplacer ce qui se passe actuellement pendant le glissement (rotation du tracé 3D) ou une solution plus simple serait de définir votre rectangle avec deux clics consécutifs.

Utilisez ensuite le proj3d.proj_transform pour trouver les données qui se trouvent à l'intérieur de ce rectangle, trouver l'index de ces données et les recolorer en utilisant la fonction self.graph._facecolor3d[idx] fonction et remplissage self.ind avec ces indices, après quoi le fait d'appuyer sur delete se chargera d'effacer toutes les données qui sont spécifiées par self.ind .

EDIT : J'ai ajouté deux lignes dans le __init__ qui suppriment l'ax/subplot avant d'en ajouter un nouveau après la suppression des points de données. J'ai remarqué que les interactions entre les tracés devenaient lentes après la suppression de quelques points de données, car la figure se contentait de tracer sur chaque sous-plot.

EDIT 2 : J'ai découvert comment vous pouvez modifier vos données au lieu de redessiner tout le tracé, comme mentionné dans cette réponse vous devrez modifier _offsets3d qui renvoient étrangement un tuple pour x y y mais un tableau pour z .

Vous pouvez le modifier en utilisant

(x,y,z) = self.graph._offsets3d # or event.artist._offsets3d
xNew = x[:int(idx)] + x[int(idx)+1:]
yNew = y[:int(idx)] + y[int(idx)+1:]
z = np.delete(z, int(idx))
self.graph._offsets3d = (xNew,yNew,z) # or event.artist._offsets3d

Mais vous rencontrerez alors un problème lors de la suppression de plusieurs points de données dans une boucle, car les indices que vous avez stockés auparavant ne seront plus applicables après la première boucle, vous devrez mettre à jour le fichier _facecolor3d J'ai donc choisi de garder la réponse telle quelle, car redessiner le graphique avec les nouvelles données semble plus simple et plus propre que cela.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X