Измените цвет точек данных при выделении и удалите их нажатием клавиши на графике рассеяния Matplotlib 3D - PullRequest
0 голосов
/ 16 января 2019

У меня есть трехмерный график рассеяния в matplotlib, и я настроил аннотации, вдохновленные ответами здесь , особенно Доном Кристобальем .

У меня настроен базовый код захвата событий, но после нескольких дней попыток мне так и не удалось достичь своих целей. Это:

(i) Изменить цвет точки (точки) при выборе левой кнопкой мыши с синего на, например, темно-синий / зеленый.

(ii) Удалить любую выбранную точку, выбранную в (i) после нажатия клавиши «Удалить», включая любые аннотации

(iii) Выберите несколько точек в (i), используя прямоугольник выбора , и удалите, используя клавишу «delete»

Я испробовал много подходов, включая анимацию графика для обновления на основе изменений данных, манипулирование параметрами исполнителя, изменение точек данных, например, с помощью. xs, ys, zs = graph._offsets3d (что, похоже, не задокументировано), но безрезультатно.

Я попытался в функции onpick (событие):

(i) Взаимодействовать с точками через event.ind для изменения цвета с помощью event.artist.set_face_colour ()

(ii) Удаление точек с использованием обоих artist.remove ()

(iii) Удаление точек с использованием xs, ys, zs = graph._offsets3d, удаление соответствующей точки по индексу (event.ind [0]) из xs, ys и zs, а затем сброс точек графа с помощью graph._offsets3d = xs_new, ys_new, zs_new

(iv) Перерисовка диаграммы или только соответствующих разделов диаграммы (моргание?)

безуспешно!

Мой текущий код примерно такой, как показано ниже. На самом деле у меня есть несколько сотен очков, а не 3 в упрощенном примере ниже. Я бы хотел, чтобы график обновлялся плавно, если это возможно, хотя просто получить что-то полезное было бы здорово. Большая часть кода для этого, вероятно, должна находиться в 'onpick', так как это функция, которая имеет дело с выбором событий (см. обработчик событий ). Я закомментировал некоторые из моих попыток кода, которые, я надеюсь, могут быть полезны. Функция 'forceUpdate' предназначена для обновления графического объекта по триггеру события, но я не уверен, что в данный момент он что-то делает. функция on_key(event) также в настоящее время, похоже, не работает: предположительно, должна быть настройка для определения точек, например, для удаления. все художники, у которых цвет лица был изменен по умолчанию (например, удалите все точки, которые имеют темно-синий / зеленый, а не голубой).

Любая помощь очень ценится.

Код (ниже) вызывается с помощью:

visualize3DData (Y, ids, subindustry)

Некоторые примеры данных приведены ниже:

#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'

Мой текущий код здесь:

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

1 Ответ

0 голосов
/ 23 января 2019

ОК, у меня есть хотя бы частичное решение для вас, без выбора прямоугольника, но вы можете выбрать несколько точек и удалить их одним key_event.

Чтобы изменить цвет, вам нужно изменить graph._facecolor3d, подсказка была в этом отчете об ошибке о set_facecolor не установке _facecolor3d.

Также может быть хорошей идеей переписать вашу функцию как класс, чтобы избавиться отлюбые необходимые global переменные.

В моем решении есть части, которые не совсем красивые, мне нужно перерисовать фигуру после удаления точек данных, я не смог получить удаление и обновление для работы.Также (см. РЕДАКТИРОВАТЬ 2 ниже).Я еще не реализовал, что произойдет, если будет удалена последняя точка данных.

Причина, по которой ваша функция on_key(event) не сработала, была простой: вы забыли подключить ее.

Так вотявляется решением, которое должно удовлетворять целям (i) и (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)

Для реализации прямоугольного выделения вам придется переопределить то, что происходит в настоящее время при перетаскивании (поворот трехмерного графика), или более простое решениеопределить прямоугольник двумя последовательными щелчками.

Затем используйте proj3d.proj_transform, чтобы найти, какие данные находятся внутри этого прямоугольника, найти индекс указанных данных и перекрасить его с помощью функции self.graph._facecolor3d[idx] и заполнить * 1027.* с этими индексами, после чего нажатие на клавишу удалит все данные, которые определены как self.ind.

РЕДАКТИРОВАТЬ: Я добавил две строки в __init__, которые удаляюттопор / субплот перед добавлением нового после удаления точек данных.Я заметил, что взаимодействие графиков становилось медленным после того, как несколько точек данных были удалены, поскольку фигура просто строила графики для каждого субплота.

РЕДАКТИРОВАТЬ 2: Я узнал, как вы можете изменить свои данные вместоперерисовывая весь график, как упомянуто в этом ответе , вам придется изменить _offsets3d, который странным образом возвращает кортеж для x и y, но массив для z.

Вы можете изменить его, используя

(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

Но тогда вы столкнетесь с проблемой удаления нескольких точек данных в цикле, потому что индексы, которые вы сохранили ранее, не будут применимы после первого цикла,вам придется обновить _facecolor3d, список меток ... поэтому я решил оставить ответ как есть, потому что перерисовка графика с новыми данными кажется проще и чище.

...