Search code examples
pythonpython-3.xplotpyqtpyqtgraph

pyqtgraph scrolling plots: plot in chunks, show only latest 10s samples in current window


I have trouble using pygtgraph scrolling plots

Expected Results

The expected results are quite similar to the pyqtgraph-examples-scrolling plots-plot5

X-values are times, which can be generated by a simple function. Y-Values are random values.

Each 10 seconds samples as one chunk and each plot can have max. 30 seconds samples, which means 3 chunks. The current plot window only shows the latest 10 seconds samples

For example, now there are total 60 seconds samples:

  • Data between 50s-60s will be viewed in the current window
  • Data between 30s-50s could be viewed by using the mouse to drag the x-axis backward
  • Data between 0-30s will not be displayed

My Code

My current code is below, it can only show latest 30s data.

import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import random


win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('Scrolling Plots')

p1 = win.addPlot()
p1.setYRange(0,10)

xVal = [0]
yVal = [0]

def genTime():  # used to generate time
    t = 0
    while True:
        t += np.random.random_sample()
        yield t
        t = np.ceil(t)

xTime = genTime() 

#=====================================================

viewSize = 10   # current window show only latest 10s data
plotSize = 30   # plot 30s data -> 3 chunk
lstCurves = []  # List for Curves

def update():
    global p1, xVal, yVal, lstCurves

    #for c in lstCurves:
    #    c.setPos(xVal[-1], 0)

    i = np.ceil(xVal[-1]) % viewSize  # e.g. when time is 9.2s -> one 10s view size is full, append to curves list as one chunk
    if i == 0:
        curve = p1.plot()
        lstCurves.append(curve)
        xValLast = xVal[-1]
        yValLast = yVal[-1]

        xVal = [xValLast]
        yVal = [yValLast]

        while len(lstCurves) > 3:  # max 3 chunk (30 s)
            p1.removeItem(lstCurves.pop(0))  # remove the oldest 10s
        
    else:
        curve = lstCurves[-1]    # latest 10s curve
        
    xVal.append(next(xTime))
    yVal.append(random.randint(0,9))
    curve.setData(xVal, yVal)
    print(len(lstCurves))

    
#======================================================

timer = pg.QtCore.QTimer()
timer.timeout.connect(update)
timer.start(1000)


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

Problem

I have tried using curve.setPos(xx, 0), It looks like the whole curve is moving along the x-axis, but the mapping relationship between X-value and Y-value is broken

I have also tried using setXRange() to dynamically change x-axis display-range in update() func. But in this case, I can't use the mouse to drag the x-axis back to view the old data any more.

My English is not good, I hope you can understand my question. Any suggestions would be sincerely appreciated!


Solution

  • Problem

    The reasons your code don't do what you want are:

    • When you drag to view the other chunks, you disable the automatic auto-range of the plot, after that, you will have to manually drag the plot every time you want to see the new data. Also, by default, the auto-range of the plot will cover all the data that you are plotting.
    • When you use the setRange() method inside the update function, it will force that range every time you add another value to the data. Then the drag will not work as you want

    What can you do

    Well, from my perspective using the mouse drag to visualize the other data is not very convenient, I suggest to use an external widget to control the range of the data you want to view, like, a slider, scroll bar, spin box, ... A QScrollBar can do the job and it will look esthetic in a GUI.

    Before my Alternative Solution, I have a suggestion for you:

    • Use objects to create your widget, generate a class and use the variables as attributes, with this you avoid the use of the keyword global, and you could reuse the widget for other purposes.

    Alternative Solution

    Try this:

    import sys
    import random
    import numpy as np
    import pyqtgraph as pg
    from pyqtgraph.Qt import QtCore, QtGui
    
    class MyApp(QtGui.QWidget):
        def __init__(self):
            QtGui.QWidget.__init__(self)
            ## Creating the Widgets and Layouts
            self.plot_widget = pg.PlotWidget()
            self.layout = QtGui.QVBoxLayout()
            self.sbutton = QtGui.QPushButton("Start / Continue")
            self.ebutton = QtGui.QPushButton("Stop")
            self.timer = pg.QtCore.QTimer()
            self.scroll = QtGui.QScrollBar(QtCore.Qt.Horizontal)
            ## Creating the variables and constants
            self.data = [[0], [random.randint(0,9)]]  ## [xVal, yVal] to create less variables
            self.plot_item = self.plot_widget.plot(*self.data)
            self.plot_widget.setYRange(0, 10)
            self.xTime = self.genTime()
            self.vsize = 10
            self.psize = 30
            ## Building the Widget
            self.setLayout(self.layout)
            self.layout.addWidget(self.sbutton)
            self.layout.addWidget(self.ebutton)
            self.layout.addWidget(self.plot_widget)
            self.layout.addWidget(self.scroll)
            ## Changing some properties of the widgets
            self.plot_widget.setMouseEnabled(x=False, y=False)
            self.ebutton.setEnabled(False)
            self.scroll.setEnabled(False)
            self.scroll.setMaximum(self.psize-self.vsize)
            self.scroll.setValue(self.psize-self.vsize)
            ## Coneccting the signals
            self.sbutton.clicked.connect(self.start)
            self.ebutton.clicked.connect(self.stop)
            self.timer.timeout.connect(self.update)
            self.scroll.valueChanged.connect(self.upd_scroll)
    
        def genTime(self):  # used to generate time
            t = 0
            while True:
                t += np.random.random_sample()
                yield t
                t = np.ceil(t)
    
        def upd_scroll(self):
            val = self.scroll.value()
            xmax = np.ceil(self.data[0][-1+self.vsize-self.psize+val])-1
            xmin = xmax-self.vsize
            self.plot_widget.setXRange(xmin, xmax)
    
        def update(self):
            num = len(self.data[0])
            if num <= self.psize:
                self.plot_item.setData(*self.data)
            else:
                self.plot_item.setData(
                    self.data[0][-self.psize:],
                    self.data[1][-self.psize:]
                )
    
            if num == self.vsize:
                self.scroll.setEnabled(True)
            self.data[0].append(next(self.xTime))
            self.data[1].append(random.randint(0,9))
            if num > self.vsize :
                self.upd_scroll()
         
        def start(self):
            self.sbutton.setEnabled(False)
            self.ebutton.setEnabled(True)
            self.timer.start(100)
    
        def stop(self):
            self.sbutton.setEnabled(True)
            self.ebutton.setEnabled(False)
            self.timer.stop()
            self.upd_scroll()
            
        def closeEvent(self, event):
            self.timer.stop()
            event.accept()
    
    if __name__ == "__main__":
        app = QtGui.QApplication(sys.argv)
        window = MyApp()
        window.show()
        sys.exit(app.exec_())
    

    It may look like this:

    enter image description here