0

I am trying to plot live data using PyQtgraph, which i started to learn. I read a lot of posts here, such as:

Fast plotting using pyqtgraph

and searched for the docs in the internet. The problem that i`m having is that the plot is very slow while drawing the data. This is because, when the data comes from a serial port, I append it to a list, and then I take it from that list and draw it. So, the plot keeps drawing even after I shut down the device where the data is coming from.

This is my code:

class LivePlot(QtGui.QApplication):
  def __init__(self, *args, **kwargs):
    super(MyApplication, self).__init__(*args, **kwargs)
    self.t = QTime()
    self.t.start()

    self.data = deque()
    self.plot = self.win.addPlot(title='Timed data') 
    self.curve = self.plot.plot()

    print "Opening port"
    self.raw=serial.Serial("com4",115200)
    print "Port is open"

    self.tmr = QTimer()
    self.tmr.timeout.connect(self.update)
    self.tmr.start(100)

    self.cnt = 0

  def update(self):
    line = self.raw.read()
    ardString = map(ord, line)
    for number in ardString:
        numb = float(number/77.57)
        self.cnt += 1
        x = self.cnt/2

        self.data.append({'x': x , 'y': numb})  
        x = [item['x'] for item in self.data]
        y = [item['y'] for item in self.data]
        self.curve.setData(x=x, y=y)

So, how can I draw the data without append it to a list? I am new with this library so i am confused. Hope you can help me.

---- EDIT ----

I've tried this modification in the code:

class LivePlot(QtGui.QApplication):
  def __init__(self, *args, **kwargs):
    super(MyApplication, self).__init__(*args, **kwargs)
   self.cnt = 0
 #Added this new lists
   self.xData = []
   self.yData = []

  def update(self):
    line = self.raw.read()
    ardString = map(ord, line)
    for number in ardString:
        numb = float(number/77.57)
        self.cnt += 1
        x = self.cnt/2

        self.data.append({'x': x , 'y': numb}) 
        self.xData.append(x)
        self.yData.append(numb) 

    self.curve.setData(x=self.xData, y=self.yData)

But i am having the same problem.

Community
  • 1
  • 1
Pablo Flores
  • 667
  • 1
  • 13
  • 33

2 Answers2

1

As mentioned in my comments, setData() repaints the whole series. It's very inefficient if you're just modifying / adding a small portion at a time.

My solution is to keep creating and adding new PlotDataItems (or BarGraphItems, ...) to the series, and to set the PlotWidget to repaint only the added / removed item.

The default setting of QGraphicsView's viewportUpdateMode is QGraphicsView::MinimalViewportUpdate but the QGraphicsScene spends a lot of time trying to determine the minimal update. To disable this set the viewportUpdateMode() of the PlotWidget to QGraphicsView::BoundingRectViewportUpdate .

plot = pg.PlotWidget()
plot.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)

Turning off item indexing is advantageous for dynamic scenes such as realtime charts. By default a BSP tree is used for indexing. Adding and removing items, as well as all QGraphicsScene's location algorithms, have logarithmic time complexity. With no indexing, adding and removing items runs in constant time, item lookup in linear time. Because the tree depth is being optimized as items are added and removed, in scenes with many items and in dynamic scenes, maintaining the index may outweight the speed advantages of a logarithmic lookup.

For dynamic scenes, or scenes with many animated items, the index bookkeeping can outweight the fast lookup speeds.

Qt5 QGraphicsScene::itemIndexMethod

plot.scene().setItemIndexMethod(QGraphicsScene.NoIndex)

Alternatively you can calculate the optimal tree depth beforehand if you know the constraints of the plot (max number of items) and set it fixed:

# scene - total length of the scene in your units (seconds, ...)
# segment - minimal length of a single segment

depth = math.ceil(math.log(scene / segment, 2) + 1)
plot.scene().setBspTreeDepth(depth)

Qt5 QGraphicsScene::bspTreeDepth

Setting the QGraphicsView.DontAdjustForAntialiasing optimization flag and turning off antialiasing altogether also helps. Setting this flag speeds up the painting / repainting.

plot.setAntialiasing(False)
plot.setOptimizationFlag(QGraphicsView.DontAdjustForAntialiasing)

After each new added item, explicitly processing all pending Qt events might also help:

QtGui.QApplication.processEvents()

Another improvement can be achieved by disabling widget updates and repainting except on selected occasions, such as when a new item is added, or when the window is resized.

The reason for this is that if a user clicks away from a window, paint() is being called on PlotWidgets by the window, which triggers repainting of all PlotDataItems. The same when the user clicks back into the window. Most of the time it's completely unnecessary, and freezes the application temporarily if you have a lot of PlotDataItems in the scene.

However method setUpdatesEnabled() cannot be used, because setUpdatesEnabled(True) automatically calls update() on the whole widget which triggers paint() on all items, which is exactly what we want to avoid.

isShown = False

def setUpdatesDisabled(self, widget, value):
    try:
        widget.setAttribute(Qt.WA_UpdatesDisabled, value)
        widget.setAttribute(Qt.WA_ForceUpdatesDisabled, value)
    except AttributeError:
        pass
    for child in widget.children():
        self.setUpdatesDisabled(child, value)

def myWorkMethod(self):
    self.setUpdatesDisabled(self.plot, False)

    ...

    QtGui.QApplication.processEvents()
    self.setUpdatesDisabled(self.plot, True)

def changeEvent(self, event):
    if event.type() == QEvent.ActivationChange:
        if self.isShown:
            self.setUpdatesDisabled(self.plot, False)
        else:
            self.isShown = True

def resizeEvent(self, event):
    self.plot.setUpdatesEnabled(True)
  
    

Probably an even faster, but less flexible solution would be to simply draw it as an QImage and don't use PyQtGraph at all.

mhrvth
  • 87
  • 1
  • 1
  • 7
  • Thanks for your comprehensive answer. It gives a lot of good ideas. Personally I wouldn't disable widget updates and auto-repainting because there are some occasions where you can't avoid repainting. For instance when resizing the window, like you said, but also when zooming an panning. If the application then still freezes for a few seconds in those occasions, I don't consider it a full solution. But perhaps that's just me. Also, how do you handle gaps between the line segments of the added items? Do you plot duplicate points at the boundaries? – titusjan Mar 22 '21 at 08:47
0

For each number you read from the device you will recreate the x and y arrays from scratch. That means, that if you have 1000 data points you will walk a 1000 times through a list of 1000 elements, which amounts to a million steps. As you can imagine, this doesn't scale well. Your execution time grows with a factor of n^2 where n is the number of points you measure. Google 'computational complexity' or 'big O notation' and read about it.

Appending to a Python list (or deque) is fast, the time it takes does not depend on the number of elements in the list (note that this is not True for appending to a Numpy array). If you create self.xData and self.yData lists and then append your data to those list, you can get rid of the inner loops:

def update(self):
    line = self.raw.read()
    ardString = map(ord, line)
    for number in ardString:
        numb = float(number/77.57)
        self.cnt += 1
        x = self.cnt/2

        # Appending goes in constant time 
        self.data.append({'x': x , 'y': numb})  
        self.xData.append(x)    # self.xData should be a list or deque
        self.yData.append(numb) # self.yData should be a list or deque

    # Drawing the plot is now only performed once per update
    self.curve.setData(x=self.xData, y=self.yData)

Also, redrawing the plot, which happens in setData, is an expensive operation and you do this after reading each data point. A better solution would be to simply update the plot once per update. See the example above. This is still ten times a second (at least, if your plotting can keep up with the data).

As a last remark, communication with hardware is a tricky subject, your first attempt will undoubtedly be unstable and full of bugs. If you are doing this as a hobby project then it's a great opportunity to learn. If you are doing this in a professional setting then try if you can find a mentor.

ruancomelli
  • 610
  • 8
  • 17
titusjan
  • 5,376
  • 2
  • 24
  • 43
  • Sorry for late reply, and thank you for your answer. In the line `self.yData.append(x)`, there should be "numb" instead of an "x" right? And in `self.curve.setData(x=x, y=y)`, should i replace x and y for self.xData and self.yDAta? – Pablo Flores May 09 '16 at 22:03
  • I have modified the code with your answer, but it still does not works. I mean that i am still having the same problem. Maybe i did something wrong. Hope you can help me. P.S.: Thank you for your links, i learned something new – Pablo Flores May 10 '16 at 12:28
  • After 10 seconds, i've got approximately 562 items in each list. I print the length of each list: `len(xData)` and `len(yData)` and also i used QTime() – Pablo Flores May 11 '16 at 02:04
  • @mhrvth,I don't think its possible. Note that it doesn't erase your whole plot but "only" all your data points. Things like axis and labels are retained. Disabling auto scale can significantly improve performance. Also plotting lines with a lineWidth of 1 is a lot faster than thicker lines. – titusjan Mar 16 '21 at 01:31
  • @titusjan I guess the only solution then is to just keep creating and adding new PlotDataItems for every new data point – mhrvth Mar 16 '21 at 07:58
  • @mhrvth do you mean that you modified the code of the `PlotDataItem` in PyQtGraph? Would you mind sharing how you did this? I think other people might be interested since this question has currently more than a thousand views. If it doesn't fit in the comments just make a new answer. – titusjan Mar 18 '21 at 13:50
  • @titusjan I've added a complete tested answer – mhrvth Mar 22 '21 at 03:29