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

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.

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

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

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