Selecting points on a curve

2,036 views
Skip to first unread message

Morgan Cherioux

unread,
Jun 25, 2014, 6:06:30 AM6/25/14
to pyqt...@googlegroups.com
Hi, I'm new to python/pyside/pyqtgraph and I'm kind of stuck in my program.

So, I have an numpy.ndarray representing 10000 values, and I plot it in a PlotWidget using the plot method.
The result is ok but now I want to allow the user to select points of the curve so I can save the X axis of the point and use it later on.

What I would like to do is creating a QPushButton which when clicked it waits for the user to select two points on the curve by left-clicking and then save the X axis. Seems pretty simple conceptually but I don't find the good way of doing it. 
I would be really pleased if you could give me an example or something, I'm also open to any suggestion that deviate from this use case.

Thanks,
Morgan

Morgan Cherioux

unread,
Jun 25, 2014, 9:49:43 AM6/25/14
to pyqt...@googlegroups.com
I'd like update my question as I was getting started on it.

In the end, I made a subclass of PlotWidget :

class PltWidget(pg.PlotWidget):

def __init__(self, parent=None):
    super(PltWidget, self).__init__(parent)
    self.selectionMode = False

def mousePressEvent(self, ev):
    if self.selectionMode:
        if ev.button() == QtCore.Qt.LeftButton:
            # How do I get the X axis ?
    else:
        super(PltWidget, self).mousePressEvent(ev)

Then I use it in my window, connecting the button signal with the slot changing the boolean of my PltWidget :

..... # Other attributes and connections of my Window
self.T0Button = QtGui.QPushButton()
self.graphicsLeft = PltWidget()
self.T0Button.clicked.connect(self.selectT0)

def selectT0(self):
    self.graphicsLeft.selectionMode = not(self.graphicsLeft.selectionMode)

However, I still need to know how do I get the X axis of the PlotWidget from where I clicked. If anyone using pyqtgraph know the answer, please let me know. Thanks.

Luke M

unread,
Jun 25, 2014, 1:12:04 PM6/25/14
to pyqt...@googlegroups.com

On Wednesday, June 25, 2014 9:49:43 AM UTC-4, Morgan Cherioux wrote:

However, I still need to know how do I get the X axis of the PlotWidget from where I clicked. If anyone using pyqtgraph know the answer, please let me know. Thanks.

 
I think what you're looking for is ev.pos().x().
 
The plot's ViewBox (PlotWidget.plotItem.vb) also has a mapToView method if you want to map it to your axes:

Morgan Cherioux

unread,
Jun 26, 2014, 5:55:08 AM6/26/14
to pyqt...@googlegroups.com
Yeah i knew for the ev.pos().x() but it wasn't the result I wanted since I needed the X axis on the plot. I finally discovered the mapToView methods but didn't really understood in the first place. 
Anyway, I still managed to do this and your answer made things more clear so thanks a lot.

Just on a side note, it seems like I have a problem in the way a inherit from PlotWidget. 

class PltWidget(pg.PlotWidget):
    """
    Subclass of PlotWidget
    """

    def __init__(self, parent=None):
        """
        Constructor of the widget
        """
        super(PltWidget, self).__init__(parent)
        self.selectionMode = False # Selection mode used to mark histo data
        self.viewBox = self.plotItem.getViewBox() # Get the ViewBox of the widget
        self.viewBox.setMouseMode(self.viewBox.RectMode) # Set mouse mode to rect for convenient zooming
        self.line1 = pg.InfiniteLine(angle=90, movable=False)
        self.line2 = pg.InfiniteLine(angle=90, movable=False)
        self.line1IsDefine = False
        self.line2IsDefine = False
        self.buffer = None

    def mousePressEvent(self, ev):
        """
        Override the method for selection mode
        """
        if self.selectionMode:
            if ev.button() == QtCore.Qt.LeftButton: # Left click to mark
                self.buffer = self.viewBox.mapSceneToView(ev.pos()).x() # get the X axis
                # plot a vertical line where the mark is
                if not self.line1IsDefine:
                    self.line1.setPos(self.buffer)
                    self.addItem(self.line1)
                    self.line1IsDefine = True
                    print self.line1.getPos()[0]
                elif not self.line2IsDefine:
                    self.line2.setPos(self.buffer)
                    self.addItem(self.line2)
                    self.line2IsDefine = True
                    print self.line2.getPos()[0]
                    self.selectionMode = False
                else:
                    super(PltWidget, self).mousePressEvent(ev)
        else:
            super(PltWidget, self).mousePressEvent(ev)

With this code, it works exactly the way I want but it seems like there is an issue with the mouseReleaseEvent : When I'm in selectionMode, if I left-click, it does the good job showing the line and printing the X-axis. However it then adds this error in the console : 
Traceback (most recent call last):
  File "C:\Anaconda\lib\site-packages\pyqtgraph\GraphicsScene\GraphicsScene.py", line 212, in mouseReleaseEvent
    if self.sendClickEvent(cev[0]):
IndexError: list index out of range

The error doesn't freeze the program or something like that so it remains functional but it seems like the mouseReleaseEvent and mouseMoveEvent are getting issues in my subclass. Any thoughts about that ?

Luke M

unread,
Jun 26, 2014, 7:15:04 AM6/26/14
to pyqt...@googlegroups.com
Hmm, Yes I get the same error when I override mousePressEvent in PlotWidget. I took a closer look at my application and here's how I'm doing something similar: Sublcass ViewBox and override it's mousePressEvent. Then PlotWidget has a viewBox argument:
 
class CustomViewBox(pg.ViewBox):
    def mousePressEvent(self, ev):
        if ev.button() == QtCore.Qt.LeftButton:
            print('x = {}'.format(self.mapToView(ev.pos()).x()))
        else:
            super(CustomViewBox, self).mousePressEvent(ev)

p = pg.PlotWidget(viewBox=CustomViewBox())

I think I got the idea for this somewhere else on the forum so I don't want to take credit, but it works for me without the error you're seeing. Maybe the other Luke can explain the difference.

Morgan Cherioux

unread,
Jun 26, 2014, 8:07:00 AM6/26/14
to pyqt...@googlegroups.com
Indeed, it worked for me as well. Not really sure what's happening but I'm glad you came up with an answer =) The code is a little less clean since a still had to subclass PlotWidget to promote it properly with a CustomViewBox in QtDesigner :

class PltWidget(pg.PlotWidget):
    """
    Subclass of PlotWidget
    """

    def __init__(self, parent=None):
        """
        Constructor of the widget
        """
        super(PltWidget, self).__init__(parent, viewBox=CustomViewBox())

class CustomViewBox(pg.ViewBox):
    """
    Subclass of ViewBox
    """
    # My previous code for PltWidget


Also, I had to change "mapSceneToView" to "mapToView" like you did, I don't really now how those functions work but it seems fine.

Luke Campagnola

unread,
Jun 26, 2014, 8:14:53 AM6/26/14
to pyqt...@googlegroups.com
On Thu, Jun 26, 2014 at 5:55 AM, Morgan Cherioux <cheriou...@gmail.com> wrote:
Yeah i knew for the ev.pos().x() but it wasn't the result I wanted since I needed the X axis on the plot. I finally discovered the mapToView methods but didn't really understood in the first place. 
Anyway, I still managed to do this and your answer made things more clear so thanks a lot.

Just on a side note, it seems like I have a problem in the way a inherit from PlotWidget. 

class PltWidget(pg.PlotWidget):
    """
    Subclass of PlotWidget
    """

    def __init__(self, parent=None):
        """
        Constructor of the widget
        """
        super(PltWidget, self).__init__(parent)
        self.selectionMode = False # Selection mode used to mark histo data
        self.viewBox = self.plotItem.getViewBox() # Get the ViewBox of the widget
        self.viewBox.setMouseMode(self.viewBox.RectMode) # Set mouse mode to rect for convenient zooming
        self.line1 = pg.InfiniteLine(angle=90, movable=False)
        self.line2 = pg.InfiniteLine(angle=90, movable=False)
        self.line1IsDefine = False
        self.line2IsDefine = False
        self.buffer = None

    def mousePressEvent(self, ev):
        """
        Override the method for selection mode
        """
[snip]
With this code, it works exactly the way I want but it seems like there is an issue with the mouseReleaseEvent : When I'm in selectionMode, if I left-click, it does the good job showing the line and printing the X-axis. However it then adds this error in the console : 
Traceback (most recent call last):
  File "C:\Anaconda\lib\site-packages\pyqtgraph\GraphicsScene\GraphicsScene.py", line 212, in mouseReleaseEvent
    if self.sendClickEvent(cev[0]):
IndexError: list index out of range


A general rule in Qt is that if you override one mouse event handler, you must override all of them. The error appears because you have caught a press event, but the release event is sent on to the GraphicsScene. When it receives the release, it tries to process a click, but raises an error because it never received the original press event. So in this case, you should override at least mouseMoveEvent and mouseReleaseEvent as well.

Morgan Cherioux

unread,
Jun 26, 2014, 8:41:45 AM6/26/14
to pyqt...@googlegroups.com
In fact, I already tried to override them but the issue was still there =/ Maybe I was doing it wrong but anyway the other solution works perfectly fine.

Luke Campagnola

unread,
Jun 26, 2014, 8:47:00 AM6/26/14
to pyqt...@googlegroups.com
This is tricky because the mouse events delivered to Widget.mousePressEvent will report their position in the pixel coordinate system of the widget. You need to know how to map from this to the coordinate system used by your data (the internal coordinate system of the ViewBox). There are several coordinate systems in between that must be mapped across:

  PlotWidget (pixels) -> GraphicsScene -> PlotItem -> ViewBox -> ViewBox_internal -> Data Items

There are many different mapTo / mapFrom methods available to you, and each transforms from one coordinate system to another.
The easiest way is to use ViewBox.mapDeviceToView, which does the entire mapping in one step ("device" refers to the widget). If you didn't happen to come across that method, though, there are other ways. Another possibility is to first map the position into scene coordinates (PlotWidget.mapToScene), and from there you can map to any other item (QGraphicsItem.mapFromScene). Examples:

    # pos is a QPoint in widget coordinates.
    # example 1:
    data_pos = plotWidget.plotItem.vb.mapDeviceToView(pos)

    # example 2:
    scene_pos = plotWidget.mapToScene(pos)
    data_pos = plotCurve.mapFromScene(scene_pos)

And here's a more complete example:

    import pyqtgraph as pg
    pg.mkQApp()

    class W(pg.PlotWidget):
    def __init__(self):
        pg.PlotWidget.__init__(self)
        # disable auto-range
        self.setRange(xRange=[0,10], yRange=[0,10])
    def mousePressEvent(self, ev):
        pos = self.plotItem.vb.mapDeviceToView(pg.QtCore.QPointF(ev.pos())
        self.addLine(x=pos.x())
        self.addLine(y=pos.y())
    def mouseMoveEvent(self, ev):
        pass
    def mouseReleaseEvent(self, ev):
        pass

    w = W()
    w.show()

Note the conversion from QPoint (integer) to QPointF (float) before mapping.






Morgan Cherioux

unread,
Jun 26, 2014, 9:46:51 AM6/26/14
to pyqt...@googlegroups.com
Ok, thanks for clearing this up. So I think the "mapToView" that I use currently does the same thing as "mapDeviceToView" ? (because it works exactly the way I expected)

Luke Campagnola

unread,
Jun 26, 2014, 10:02:12 AM6/26/14
to pyqt...@googlegroups.com
On Thu, Jun 26, 2014 at 9:46 AM, Morgan Cherioux <cheriou...@gmail.com> wrote:
Ok, thanks for clearing this up. So I think the "mapToView" that I use currently does the same thing as "mapDeviceToView" ? (because it works exactly the way I expected)

No--ViewBox.mapToView only maps across one hop in the list I gave above (ViewBox -> ViewBox_internal). You can try it in the example I provided and see that the x-value of the lines drawn is offset from the click position. I'm surprised it works in your case, but there are some situations when this could happen by chance (if the ViewBox coordinate system happens to be exactly the same as the Widget coordinate system).

Luke Campagnola

unread,
Jun 26, 2014, 10:03:48 AM6/26/14
to pyqt...@googlegroups.com
On Thu, Jun 26, 2014 at 8:41 AM, Morgan Cherioux <cheriou...@gmail.com> wrote:
In fact, I already tried to override them but the issue was still there =/ Maybe I was doing it wrong but anyway the other solution works perfectly fine.

I assume you were checking for self.selectionMode in the other event handlers, but mousePressEvent sets that to False..? You would need to have mouseReleaseEvent set the flag False instead.

Luke M

unread,
Jun 26, 2014, 10:27:32 AM6/26/14
to pyqt...@googlegroups.com
Interesting, I have been using mapToView because it was working fine for me and I didn't know about what you explained above. In the example I posted above, if I also print self.mapDeviceToView, it seems off, where mapToView seems correct. What am I missing?

Luke Campagnola

unread,
Jun 26, 2014, 10:37:00 AM6/26/14
to pyqt...@googlegroups.com
Ah, sorry I missed something important--in your example, you are catching the mouse events from ViewBox.mousePressEvent. These events already have their position in the coordinate system of the ViewBox, so mapToView is correct in that case. For the original example with PlotWidget.mousePressEvent, the events use the widget coordinate system and thus we need mapDeviceToView.

Morgan Cherioux

unread,
Jun 26, 2014, 11:06:29 AM6/26/14
to pyqt...@googlegroups.com
That explain it indeed. When I used the PlotWidget.mousePressEvent, it worked with mapSceneToView, I don't know if it's far from mapDeviceToView but anyway, subclassing ViewBox seems to be the most stable solution for me.
Thanks again for your answers ;)
Reply all
Reply to author
Forward
0 new messages