Correct way to add/remove single points (getting RuntimeError due to deleting C / C++ object)

902 views
Skip to first unread message

Dan

unread,
Feb 18, 2017, 12:50:49 PM2/18/17
to pyqtgraph
I'm trying to put together a fairly simple app that allows the user to add and/or remove individual points on top of a data already plotted. 

A simplified version of the app is shown below. Basically individual points are plotted at the nearest point along the already-plotted curve when the user right clicks on the curve. If the user then double clicks (which I've achieved by just using a timer, since PlotDataItem.sigClicked doesn't provide information about the mouse click event - if there's a better way to do this I'd love to hear about that as well) on that point it is removed from the plot. 

The way I've gone about doing this, though, is causing me to have the following error: 

"RuntimeError: wrapped C/C++ object of type ScatterPlotItem has been deleted"

As a work around I'm keeping a list of the PlotDataItem objects, but obviously that's a pretty asinine solution. Is there a more correct way to go about achieving this? 


Code: 

import sys
import time
from PyQt4 import QtGui
import pyqtgraph as pg
import numpy as np


class ExPlot(QtGui.QWidget):
    def __init__(self):
        super().__init__()
        desktop = QtGui.QDesktopWidget()
        width = desktop.screenGeometry().width()
        ratio = width / 1920
        self.resize(1400*ratio, 800*ratio)
        
        self.layout=QtGui.QHBoxLayout(self)
        self.plotWidget = pg.GraphicsLayoutWidget(self)
        self.layout.addWidget(self.plotWidget)
        self.plot = self.plotWidget.addPlot(1,1, enableMenu=False)

        self.indexes = {}
        self.counter = 1
        self.time = 0
        x = np.arange(0, 10, 0.01)
        y = np.sin(x)

        plotData = self.plot.plot(x, y)
        self.plotWidget.scene().sigMouseClicked.connect(self.addPoint)
        
    def addPoint(self, event):
        if event.button()==2:
            items = self.plotWidget.scene().items(event.scenePos())
            plot = items[0]
            try:
                data = plot.getData()
                vb = items[1]
                index = int(vb.mapSceneToView(event.scenePos()).x() / 0.01)
                
                
                if index not in self.indexes.values():
                    x = data[0][index]
                    y = data[1][index]
                    point = self.plot.plot([x, ], [y, ], symbol='o',
                                           symbolSize=20, clickable=True,
                                           name=self.counter)
                    
                    point.sigClicked.connect(self.removePoint)
                    self.indexes[self.counter] = index
                    self.counter += 1
            
            #these are thrown if user clicks way to the left/right of the plotted data
            except (AttributeError, IndexError):
                pass
    
    def removePoint(self, item):
        tdiff = time.time() - self.time
        if tdiff < 1:
            del self.indexes[item.name()]
            self.plot.removeItem(item)
        
        self.time = time.time()
    
if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    ex = ExPlot()
    ex.show()
    sys.exit(app.exec_())
        

Dan

unread,
Feb 22, 2017, 11:14:19 AM2/22/17
to pyqtgraph
OK, in doing a bit more digging around I realized that this is the wrong way to do this anyway. I didn't know that PlotDataItem could be given an empty data set. Having learned that it makes more sense to just have an empty PlotDataItem that I can just update with user input. So the code below works now without having to add/delete plots, which is great. 

One remaining question, though. Is there any logic to the items returned by sigMousClicked? I know it's returning whatever is under the mouse click, but the contents seem to be somewhat unpredictable. Sometimes I get the PlotCurveItem first, followed by ScatterPlotItem, followed by ViewBox. Sometimes the PlotCurveItem and ScatterPlotItem are switched. And sometimes the ScatterPlotItem isn't present. Since I really only care about the ViewBox get to the mouse position I can mostly ignore the other things in there (and just make sure that the calculated index is valid). Is there a better way to go about doing this, though? 

import sys
import time
from PyQt5 import QtGui, QtWidgets
import pyqtgraph as pg
import numpy as np


class ExPlot(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        desktop = QtWidgets.QDesktopWidget()
        width = desktop.screenGeometry().width()
        ratio = width / 1920
        self.resize(1400*ratio, 800*ratio)
        
        self.layout=QtWidgets.QHBoxLayout(self)
        self.plotWidget = pg.GraphicsLayoutWidget(self)
        self.layout.addWidget(self.plotWidget)
        self.plot = self.plotWidget.addPlot(1,1, enableMenu=False)

        self.indexes = []
        self.counter = 1
        self.time = 0
        self.x = np.arange(0, 10, 0.01)
        self.y = np.sin(self.x)

        plotData = self.plot.plot(self.x, self.y)
        self.plotWidget.scene().sigMouseClicked.connect(self.getIndex)
        self.points_plot = self.plot.plot(x=[], y=[], pen=None, symbol='o')
        self.points_plot.sigPointsClicked.connect(self.removePoint)
        
    def getIndex(self, event):
        if event.button()==2:
            items = self.plotWidget.scene().items(event.scenePos())
            for item in items:
                if isinstance(item, pg.ViewBox):
                    index = int(item.mapSceneToView(event.scenePos()).x() / 0.01)
                    if 0 <= index < len(self.x):
                        self.addPoint(index)

    def addPoint(self, index):
        if index not in self.indexes:
            self.indexes.append(index)
            x = self.x[self.indexes]
            y = self.y[self.indexes]
            self.points_plot.setData(x, y, pen=None)

    def removePoint(self, item, points):
        tdiff = time.time() - self.time
        if tdiff < 1:
            point_x = points[0].pos()[0]
            ix = int(round(point_x/0.01))
            if ix in self.indexes:
                self.indexes.remove(ix)
                x = self.x[self.indexes]
                y = self.y[self.indexes]
                self.points_plot.setData(x, y, pen=None)
Reply all
Reply to author
Forward
0 new messages