Matthew Lawrence
Matthew Lawrence

Reputation: 51

Trying to get cursor with coordinate display within a pyqtgraph plotwidget in PyQt5

I'm trying to add a readout of the cursor position in a pqytplot plotwidget in PyQt5. I found this code which does what I want, but in a stand-alone window all within one program file:

import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore

#generate layout
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
label = pg.LabelItem(justify='right')
win.addItem(label)
p1 = win.addPlot(row=1, col=0)

data1 = [n**2 for n in range(100)]
p1.plot(data1, pen="r")

#cross hair
vLine = pg.InfiniteLine(angle=90, movable=False)
hLine = pg.InfiniteLine(angle=0, movable=False)
p1.addItem(vLine, ignoreBounds=True)
p1.addItem(hLine, ignoreBounds=True)


def mouseMoved(evt):
    pos = evt[0]  ## using signal proxy turns original arguments into a tuple
    if p1.sceneBoundingRect().contains(pos):
        mousePoint = p1.vb.mapSceneToView(pos)
        index = int(mousePoint.x())
        if index > 0 and index < len(data1):
            label.setText("<span style='font-size: 12pt'>x=%0.1f,   <span style='color: red'>y1=%0.1f</span>" % (mousePoint.x(), data1[index]))
        vLine.setPos(mousePoint.x())
        hLine.setPos(mousePoint.y())


proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)

## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()

The problem I'm running in to is figuring out how to implement something like this with my GUI - where I will have to pass reference to the plotwidget to the mouseMoved function. In the example above, the mousemoved function has access to hline, vline and p1, but in my code it won't - I need to be able to pass those through. But I have no idea how to do that.

I've tried to replicate this issue with the smallest amount of code possible. First here's a simple UI file for the GUI, called CursorLayout.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>1167</width>
    <height>443</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout_2">
    <item row="0" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_6">
      <item>
       <layout class="QHBoxLayout" name="horizontalLayout_16">
        <property name="sizeConstraint">
         <enum>QLayout::SetFixedSize</enum>
        </property>
        <item>
         <layout class="QVBoxLayout" name="verticalLayout">
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout_4">
            <item>
             <widget class="QPushButton" name="startbutton">
              <property name="sizePolicy">
               <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
                <horstretch>0</horstretch>
                <verstretch>0</verstretch>
               </sizepolicy>
              </property>
              <property name="text">
               <string>Plot</string>
              </property>
             </widget>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </item>
      <item>
       <layout class="QVBoxLayout" name="verticalLayout_5">
        <item>
         <widget class="PlotWidget" name="plotWidget" native="true">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="minimumSize">
           <size>
            <width>0</width>
            <height>300</height>
           </size>
          </property>
         </widget>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_3"/>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_17">
          <property name="spacing">
           <number>0</number>
          </property>
          <item>
           <widget class="QPushButton" name="exitbutton">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
              <horstretch>0</horstretch>
              <verstretch>0</verstretch>
             </sizepolicy>
            </property>
            <property name="text">
             <string>Exit</string>
            </property>
           </widget>
          </item>
         </layout>
        </item>
       </layout>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
 </widget>
 <customwidgets>
  <customwidget>
   <class>PlotWidget</class>
   <extends>QWidget</extends>
   <header location="global">pyqtgraph</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

The main program is this:

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow

from initGUI import connecttolayout, setinitialview


class UI(QMainWindow):
    def __init__(self):
        super(UI, self).__init__()
        uic.loadUi("CursorLayout.ui", self)  #load GUI layout file created with QtDesigner
        connecttolayout(self)  # connect code to elements in UI file
        setinitialview(self)  # set initial view (button/label visibility, default values, etc)
        self.show()


    def clickedstartButton(self):  #action if start button clicked
        self.plotWidget.clear()
        plotx = range(100)
        ploty = [number**2 for number in plotx]
        thisline = self.plotWidget.plot(plotx, ploty, pen='r')
        QApplication.processEvents()


    def clickedexitButton(self):
        self.close()


app=QApplication([])
UIWindow=UI()
app.exec()

with file containing code to set up the gui, initGUI.py (not necessarily how you would do this, but this is to mimic the file structure of my larger program):

from PyQt5.QtWidgets import QPushButton
import pyqtgraph as pg

def connecttolayout(self):  #connect GUI elements to elements in UI file
    self.startButton = self.findChild(QPushButton, "startbutton")
    self.exitButton = self.findChild(QPushButton, "exitbutton")
    self.startButton.clicked.connect(self.clickedstartButton)
    self.exitButton.clicked.connect(self.clickedexitButton)

def mouseMoved(evt):
    pos = evt[0]  ## using signal proxy turns original arguments into a tuple
    if self.plotWidget.sceneBoundingRect().contains(pos):
        mousePoint = self.plotWidget.vb.mapSceneToView(pos)
        index = int(mousePoint.x())
        #if index > 0 and index < len(data1):
        if index > 0 and index < self.MFmax:
            self.cursorlabel.setText("<span style='font-size: 12pt'>x=%0.1f,   <span style='color: red'>y=%0.1f</span>" % (
            mousePoint.x(), mousePoint.y()))
        self.vLine.setPos(mousePoint.x())
        self.hLine.setPos(mousePoint.y())


def setinitialview(self): #set initial view to pvst view and clear plot window
    #set plot initial configuration
    self.plotWidget.setBackground('w')
    self.plotWidget.setLabels(left=('Pressure', 'Torr'))
    self.plotWidget.setLabel('left',color='black',size=30)
    self.plotWidget.setLabels(bottom=('Time', 's'))
    self.plotWidget.setLabel('bottom',color='black',size=30)
    self.plotWidget.clear()

    # cross hair
    self.vLine = pg.InfiniteLine(angle=90, movable=False)
    self.hLine = pg.InfiniteLine(angle=0, movable=False)
    self.plotWidget.addItem(self.vLine, ignoreBounds=True)
    self.plotWidget.addItem(self.hLine, ignoreBounds=True)
    self.cursorlabel = pg.LabelItem(justify='right')
    proxy = pg.SignalProxy(self.plotWidget.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)

I'm actually surprised my attempt doesn't cause an error - pressing the plot button does create a plot, but it definitely doesn't create the cursor in the graph in the GUI.

How do I get the necessary info passed to the mouseMoved function?

Upvotes: 2

Views: 3009

Answers (2)

A_d_r_i
A_d_r_i

Reputation: 21

Performance of mx calculation in mouseMoved can be largly improved in order to get a faster response of the cursor:

def mouseMoved(self, evt):
        pos = evt
        if self.plotWidget.sceneBoundingRect().contains(pos):
            mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
            mx = abs(np.ones(len(self.plotx))*mousePoint.x() - self.plotx)
            index = mx.argmin()
            if index >= 0 and index < len(self.plotx):
                self.cursorlabel.setHtml(
                    "<span style='font-size: 12pt'>x={:0.1f}, \
                     <span style='color: red'>y={:0.1f}</span>".format(
                     self.plotx[index], self.ploty[index])
                     )
            self.vLine.setPos(self.plotx[index])
            self.hLine.setPos(self.ploty[index])

Upvotes: 2

Alejandro Condori
Alejandro Condori

Reputation: 864

There are a few little errors that will make your program fail:

  • The mouseMoved() function has to be inside your widget class because it needs the evt argument, which is generated in the widget.

  • The self.MFmax variable/constant is not created anywhere

  • In this line:

    mousePoint = self.plotWidget.vb.mapSceneToView(pos)
    

    The PlotWidget object doesn't have the vb attribute. It is a PlotItem's attribute, then you should change that line to this:

    mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
    
  • Pyqtgraph recommends here to use TextItem instead of LabelItem, to display text inside a scaled view, because of its scaling size.

Now, with that said and reorganizing your code to be more legible, here is my solution to your code (you only need the UI file and this script):

import sys
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, uic

ui_file = uic.loadUiType("CursorLayout.ui")[0]

class UI(QtGui.QMainWindow, ui_file):
    def __init__(self):
        ## Inherit the QMainWindow and ui_file classes
        QtGui.QMainWindow.__init__(self)
        ui_file.__init__(self)
        self.setupUi(self)
        ## Create aditional widgets
        self.plot_item = self.plotWidget.plot()
        self.vLine = pg.InfiniteLine(angle=90, movable=False)
        self.hLine = pg.InfiniteLine(angle=0, movable=False)
        self.cursorlabel = pg.TextItem(anchor=(-1,10))
        ## Build the rest of the GUI
        self.format_plot()
        ## data
        self.plotx = range(100)
        self.ploty = [number**2 for number in self.plotx]        
        ## Connect signals to actions
        self.startbutton.clicked.connect(self.clickedstartButton)
        self.exitbutton.clicked.connect(self.clickedexitButton)
        self.plotWidget.scene().sigMouseMoved.connect(self.mouseMoved)
    
    ## OVERWRITE the mouseMoved action:
    def mouseMoved(self, evt):
        pos = evt
        if self.plotWidget.sceneBoundingRect().contains(pos):
            mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
            index = int(mousePoint.x())
            if index > 0 and index < len(self.plotx):
            # if index > 0 and index < self.MFmax:
                self.cursorlabel.setHtml(
                    "<span style='font-size: 12pt'>x={:0.1f}, \
                     <span style='color: red'>y={:0.1f}</span>".format(
                mousePoint.x(), mousePoint.y()))
            self.vLine.setPos(mousePoint.x())
            self.hLine.setPos(mousePoint.y())
  
    def clickedstartButton(self):  #action if start button clicked
        self.plot_item.setData(self.plotx, self.ploty, pen='r')
        self.plotWidget.addItem(self.cursorlabel)

    def clickedexitButton(self):
        self.close()
    
    def format_plot(self):
        self.plotWidget.setBackground('w')
        self.plotWidget.setLabels(left=('Pressure', 'Torr'))
        self.plotWidget.setLabel('left',color='black',size=30)
        self.plotWidget.setLabels(bottom=('Time', 's'))
        self.plotWidget.setLabel('bottom',color='black',size=30)
        self.plotWidget.addItem(self.vLine, ignoreBounds=True)
        self.plotWidget.addItem(self.hLine, ignoreBounds=True)
        
    
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = UI()
    window.show()
    sys.exit(app.exec_())

The code above will make the "crosshair" (the hline and vline) to follow your mouse and displaying the coordinates of that position, like this:

enter image description here

If you want the "crosshair" to track the points in the curve based on the x-axis position of your cursor, you can change the mouseMoved() function to this:

def mouseMoved(self, evt):
        pos = evt
        if self.plotWidget.sceneBoundingRect().contains(pos):
            mousePoint = self.plotWidget.plotItem.vb.mapSceneToView(pos)
            mx = np.array([abs(i-mousePoint.x()) for i in self.plotx])
            index = mx.argmin()
            if index >= 0 and index < len(self.plotx):
                self.cursorlabel.setHtml(
                    "<span style='font-size: 12pt'>x={:0.1f}, \
                     <span style='color: red'>y={:0.1f}</span>".format(
                     self.plotx[index], self.ploty[index])
                     )
            self.vLine.setPos(self.plotx[index])
            self.hLine.setPos(self.ploty[index])

And this will be the result:

enter image description here

Upvotes: 5

Related Questions