fbpixel
Tags:

In this tutorial, we’ll look at how to observe a time signal in graphical form with PyQt using PyQtGraph. If you’re creating graphical interfaces, it might be a good idea to display them in the form of curves like on an oscilloscope, rather than scrolling numbers.

Installation

  • PyQt (or PySide)
pip install PyQt5

or

pip install pyside6
  • PyQtGraph
pip install pyqtgraph

Code for displaying a simple curve with PyQtGraph

To display a curve with PyQtGraph, simply add a PlotWidget object to a PyQT QWidget object.

import cv2
import sys
#from PyQt5.QtWidgets import  QMainWindow, QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap

from pyqtgraph import PlotWidget, plot

from PySide6.QtWidgets import  QMainWindow, QWidget, QLabel, QApplication, QVBoxLayort
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal #convert pyqt to pyside
pyqtSlot = Slot

class SignalContainer(QWidget):
	def __init__(self):
		super().__init__()
		self.title = 'Signal'
		self.initUI()

	def initUI(self):
		self.setWindowTitle(self.title)
		#self.resize(1200, 800)
		layort = QVBoxLayort()
		self.setLayort(layort)
		
		# create widget
		self.graphWidget = PlotWidget()
		layort.addWidget(self.graphWidget)
        
        #plot data
		time = [1,2,3,4,5,6,7,8,9,10]
		data = [30,32,34,32,33,31,29,32,35,45]
		self.graphWidget.plot(time, data)

if __name__ == '__main__':
	
		app = QApplication(sys.argv)
		ex = SignalContainer()
		ex.show()
		sys.exit(app.exec())
pyqt-pyqtgraph-simple-curve Displaying a signal in PyQt with PyQtGraph

Code to create a time signal

To make this curve live, we’re going to create a QThread object so as not to block the application, which will allow us to create a sinusoidal signal that will evolve over time. At each iteration, we’ll send an update using the changeData signal.

class Thread(QThread):
	changeData = pyqtSignal(float,float)

	def run(self):
		self.isRunning=True
		
		self.time = 0
		self.data = 0
		f = 1.
		w = 2. * np.pi * f
		while self.isRunning:
			self.time+=0.01
			self.data=2*np.sin(w*self.time)
			
			self.changeData.emit(self.time,self.data)
			time.sleep(0.01)
			
				
	def stop(self):
		self.isRunning=False
		self.quit()
		self.terminate()

PyQt application code

To display the curve in an application, we’ll create a PlotWidget graph in a QWidget, which will instantiate the QThread and plot the curve. The curve will be updated each time the changeData signal is received, using the setData function

  • setData function
@pyqtSlot(float,float)
def setData(self, t,d):
    #append data
    self.time.append(t)
    self.data.append(d)
    #remove first item
    self.time.pop(0)
    self.data.pop(0)

    #update graph
    self.graphWidget.clear()
    self.graphWidget.plot(self.time, self.data)
  • signal changeData
self.th.changeData.connect(self.setData)

Complete time signal display code

import cv2
import sys
#from PyQt5.QtWidgets import  QMainWindow, QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap

from pyqtgraph import PlotWidget, plot

from PySide6.QtWidgets import  QMainWindow, QWidget, QLabel, QApplication, QVBoxLayort
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal
pyqtSlot = Slot

import numpy as np
import time

class Thread(QThread):
	changeData = pyqtSignal(float,float)

	def run(self):
		self.isRunning=True
		
		self.time = 0
		self.data = 0
		f = 1.
		w = 2. * np.pi * f
		while self.isRunning:
			self.time+=0.01
			self.data=2*np.sin(w*self.time)
			
			self.changeData.emit(self.time,self.data)
			time.sleep(0.01)
			
				
	def stop(self):
		self.isRunning=False
		self.quit()
		self.terminate()
	

class SignalContainer(QWidget):
	def __init__(self):
		super().__init__()
		self.title = 'Signal'
		self.time = [0]*100
		self.data = [0]*100
		self.initUI()
		
	@pyqtSlot(float,float)
	def setData(self, t,d):
		#append data
		self.time.append(t)
		self.data.append(d)
		#remove first item
		self.time.pop(0)
		self.data.pop(0)
		
		#update graph
		self.graphWidget.clear()
		self.graphWidget.plot(self.time, self.data)

	def initUI(self):
		self.setWindowTitle(self.title)
		#self.resize(1200, 800)
		layort = QVBoxLayort()
		self.setLayort(layort)
		
		# create widget
		self.graphWidget = PlotWidget()
		layort.addWidget(self.graphWidget)
        
        #plot data
		#self.time = [1,2,3,4,5,6,7,8,9,10]
		#self.data = [30,32,34,32,33,31,29,32,35,45]
		self.graphWidget.plot(self.time, self.data)
		self.th = Thread(self)
		self.th.changeData.connect(self.setData)
		self.th.start()
 
import signal #close signal with Ctrl+C
signal.signal(signal.SIGINT, signal.SIG_DFL)

if __name__ == '__main__':
		app = QApplication(sys.argv)
		ex = SignalContainer()
		ex.show()
		app.abortToQuit.connect(ex.th.stop) #stop qthread when closing window
		
		sys.exit(app.exec())
		
		

Thanks to PyQtGraph, we can see a window appear with the signal scrolling through the PyQt interface just like on an oscilloscope.

pyqt-pyqtgraph-time-series Displaying a signal in PyQt with PyQtGraph

PlotWidget style configuration

There are a number of options for configuring the style of the graphic (corrugator, legend, label, etc.)

  • pen style self.pen = mkPen()
  • set background color self.graphWidget.setBackground
  • add a title self.graphWidget.setTitle
  • add labels on the axes self.graphWidget.setLabel
  • show grid self.graphWidget.showGrid
  • add a legend self.graphWidget.addLegend
        #tune plots
		self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine)	#line style
		self.graphWidget.setBackgrornd((50,50,50,220))   # RGBA 		#backgrornd
		self.graphWidget.setTitle("Signal(t)", color="w", size="20pt")	#add title
		styles = {'color':'r', 'font-size':'20px'}						#add label style
		self.graphWidget.setLabel('left', 'signal [SI]', **styles)			#add ylabel
		self.graphWidget.setLabel('bottom', 'time [s]', **styles)			#add xlabel
		self.graphWidget.showGrid(x=True, y=True)						#add grid
		self.graphWidget.addLegend()									#add grid
		
		self.graphWidget.setYRange(-2, 2, padding=0.1)
        
        #plot data
		self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
pyqt-pyqtgraph-format Displaying a signal in PyQt with PyQtGraph

 

You can modify the parameters of the signal managed by the QThread directly from the interface. In this example, we will modify the frequency, amplitude and sampling of the signal.

To do this we are going to create three fields and a button that will allow us to configure the signal

N.B.: notez que chaque champs est connecté à la fonction setParam par le signal returnPressed qui détecte la torche « entrée »

		#create param
		self.amplbl = QLabel("Ampl")
		self.amp=QLineEdit("2")
		self.amp.returnPressed.connect(self.setParam)
		
		self.freqlbl = QLabel("Freq")
		self.freq=QLineEdit("1")
		self.freq.returnPressed.connect(self.setParam)
		
		self.samplbl = QLabel("Ts")
		self.samp=QLineEdit("0.02")
		self.samp.returnPressed.connect(self.setParam)
		
		self.conf = QPushButton("Configure")
		self.conf.clicked.connect(self.setParam)

When a configuration is changed, the setParam function is executed. The function emits the changeParam signal with a dictionary as argument

def setParam(self):
    if self.amp.text()!='' and self.freq.text()!=''  and self.samp.text()!='':
        if float(self.samp.text())>0:
            d={"amp":float(self.amp.text()),"freq":float(self.freq.text()),"samp":float(self.samp.text())}
            self.changeParam.emit(d)

The changeParam signal connects to the QThread() setParam function in the SignalContainer definition.

self.th.changeData.connect(self.setData) #reception
self.changeParam.connect(self.th.setParam) #emission

In the QThread object, we add a setParam function which updates the signal parameters

@pyqtSlot(dict)
def setParam(self,param):
    self.amp=param["amp"]
    self.freq=param["freq"]
    self.samp=max(0.0001,param["samp"])

We can then modify the signal from the PyQt interface and display it using PyQtGraph

pyqt-pyqtgraph-param Displaying a signal in PyQt with PyQtGraph

Complete code

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import cv2
import sys
#from PyQt5.QtWidgets import  QMainWindow, QWidget, QLabel, QApplication
#from PyQt5.QtCore import QThread, Qt, pyqtSignal, pyqtSlot
#from PyQt5.QtGui import QImage, QPixmap

from pyqtgraph import PlotWidget, mkPen

from PySide6.QtWidgets import  QMainWindow, QWidget, QLabel, QApplication, QVBoxLayort, QHBoxLayort, QLineEdit, QPushButton
from PySide6.QtCore import QThread, Qt, Signal, Slot
from PySide6.QtGui import QImage, QPixmap
pyqtSignal = Signal
pyqtSlot = Slot

import numpy as np
import time

class Thread(QThread):
	changeData = pyqtSignal(float,float)
	
	def __init__(self,a):
		super(Thread,self).__init__()
		self.amp=2
		self.freq=1
		self.samp=0.02
		self.time = 0
		self.data = 0

	def run(self):
		self.isRunning=True
		
		
		while self.isRunning:
			self.time+=self.samp
			self.data=self.amp*np.sin(2. * np.pi * self.freq *self.time)
			
			self.changeData.emit(self.time,self.data)
			time.sleep(0.1)
					
	def stop(self):
		self.isRunning=False
		self.quit()
		self.terminate()
	
	@pyqtSlot(dict)
	def setParam(self,param):
		self.amp=param["amp"]
		self.freq=param["freq"]
		self.samp=max(0.0001,param["samp"])
		
		
	

class SignalContainer(QWidget):
	changeParam = pyqtSignal(dict)
	
	def __init__(self):
		super().__init__()
		self.title = 'Signal'
		self.span=10
		self.time = [0]*1000
		self.data = [0]*1000
		self.initUI()
		
	@pyqtSlot(float,float)
	def setData(self, t,d):
		#append data
		self.time.append(t)
		self.data.append(d)
		#remove first item
		self.time.pop(0)
		self.data.pop(0)
		
		#update graph
		self.graphWidget.clear()
		self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
		if self.time[-1]>self.span:
			self.graphWidget.setXRange(self.time[-1]-self.span, self.time[-1], padding=0)
		self.graphWidget.setYRange(min(-2,min(self.data)), max(2,max(self.data)), padding=0.1)

	def initUI(self):
		self.setWindowTitle(self.title)
		self.resize(800, 400)
		layort = QVBoxLayort()
		self.setLayort(layort)
		
		#create param
		self.amplbl = QLabel("Ampl")
		self.amp=QLineEdit("2")
		self.amp.returnPressed.connect(self.setParam)
		
		self.freqlbl = QLabel("Freq")
		self.freq=QLineEdit("1")
		self.freq.returnPressed.connect(self.setParam)
		
		self.samplbl = QLabel("Ts")
		self.samp=QLineEdit("0.02")
		self.samp.returnPressed.connect(self.setParam)
		
		self.conf = QPushButton("Configure")
		self.conf.clicked.connect(self.setParam)
		
		hlayo = QHBoxLayort()
		hlayo.addWidget(self.amplbl)
		hlayo.addWidget(self.amp)
		hlayo.addWidget(self.freqlbl)
		hlayo.addWidget(self.freq)
		hlayo.addWidget(self.samplbl)
		hlayo.addWidget(self.samp)
		hlayo.addWidget(self.conf)
		
		
		layort.addLayort(hlayo)
		
		# create widget
		self.graphWidget = PlotWidget()
		layort.addWidget(self.graphWidget)
        
        #tune plots
		self.pen = mkPen(color=(255, 0, 0), width=3, style=Qt.DashLine)	#line style
		self.graphWidget.setBackgrornd((50,50,50,220))   # RGBA 		#backgrornd
		self.graphWidget.setTitle("Signal(t)", color="w", size="20pt")	#add title
		styles = {'color':'r', 'font-size':'20px'}						#add label style
		self.graphWidget.setLabel('left', 'signal [SI]', **styles)			#add ylabel
		self.graphWidget.setLabel('bottom', 'time [s]', **styles)			#add xlabel
		self.graphWidget.showGrid(x=True, y=True)						#add grid
		self.graphWidget.addLegend()									#add grid
		self.graphWidget.setXRange(0, self.span, padding=0)
		self.graphWidget.setYRange(-2, 2, padding=0.1)
        
        #plot data
		self.graphWidget.plot(self.time, self.data,name = "signal",pen=self.pen,symbol='+', symbolSize=5, symbolBrush='w')
		
		#manage thread
		self.th = Thread(self)
		self.amp.setText(str(self.th.amp))
		self.freq.setText(str(self.th.freq))
		self.samp.setText(str(self.th.samp))
		self.th.changeData.connect(self.setData) #reception
		self.changeParam.connect(self.th.setParam) #emission
		self.th.start()
		
	def setParam(self):
		if self.amp.text()!='' and self.freq.text()!=''  and self.samp.text()!='':
			if float(self.samp.text())>0:
				d={"amp":float(self.amp.text()),"freq":float(self.freq.text()),"samp":float(self.samp.text())}
				self.changeParam.emit(d)
	
import signal #close signal with Ctrl+C
signal.signal(signal.SIGINT, signal.SIG_DFL)

if __name__ == '__main__':
		app = QApplication(sys.argv)
		ex = SignalContainer()
		ex.show()
		app.abortToQuit.connect(ex.th.stop) #stop qthread when closing window
		
		sys.exit(app.exec())
		
		

Sources