如何更新pyqtgraph中的绘图?

Posted

技术标签:

【中文标题】如何更新pyqtgraph中的绘图?【英文标题】:How to update a plot in pyqtgraph? 【发布时间】:2021-03-31 08:53:56 【问题描述】:

我正在尝试创建一个使用 PyQt5 和 pyqtgraph 的用户界面。我做了两个复选框,每当我选择它们时,我想绘制代码中可用的两个数据集之一,并且每当我取消选择一个按钮时,我希望它清除相应的曲线。有两个带有文本A1A2 的复选框,每个复选框都绘制一组数据。

我有两个问题:

1- 如果我选择A1,它会绘制与A1 关联的数据,只要我不选择A2,通过取消选择A1,我可以清除与A1 关联的数据。 但是,如果我选中 A1 框,然后选中 A2 框,则取消选择 A1 不会清除关联的绘图。在这种情况下,如果我选择绘制随机数据,而不是像sin 这样的确定性曲线,我会看到通过选择任一按钮都会添加新数据,但无法将其删除。

2- 真正的应用程序有 96 个按钮,每个按钮都应该与一个数据集相关联。我认为我编写代码的方式效率低下,因为我需要为一个按钮和数据集复制相同的代码 96 次。有没有办法将我在下面介绍的玩具代码推广到任意数量的复选框?或者,为每个按钮使用/复制几乎相同的代码是通常且正确的方法吗?

代码是:

from PyQt5 import QtWidgets, uic, QtGui
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
import sys
import string
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore

app = QtWidgets.QApplication(sys.argv)

x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2

#This function is called whenever the state of checkboxes changes
def todo():
    if cbx1.isChecked():
        global curve1
        curve1 = plot.plot(x, y1, pen = 'r')
    else:
        try:
            plot.removeItem(curve1)
        except NameError:
            pass
    if cbx2.isChecked():
        global curve2
        curve2 = plot.plot(x, y2, pen = 'y')
    else:
        try:
            plot.removeItem(curve2)
        except NameError:
            pass  
#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()

#Checkboxes named A1 and A2
cbx1 = QtWidgets.QCheckBox()
cbx1.setText('A1')
cbx1.stateChanged.connect(todo)

cbx2 = QtWidgets.QCheckBox()
cbx2.setText('A2')
cbx2.stateChanged.connect(todo)

#Making a pyqtgraph plot widget
plot = pg.PlotWidget()

#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)

#Adding the widgets to the layout
layout.addWidget(cbx1, 0,0)
layout.addWidget(cbx2, 0, 1)
layout.addWidget(plot, 1,0, 3,1)

widget_holder.adjustSize()
widget_holder.show()

sys.exit(app.exec_())


    

【问题讨论】:

96 个按钮?这对于 UI 来说已经很多了。 @JHBonarius 在这个特定的 UI 中,我试图绘制一些细胞的生长曲线。生长测定通常在 96 孔板中进行,能够检查单个生长曲线会很好。在稍后阶段,我将尝试添加可以通过选择一组按钮上方的区域来选择一组按钮的功能,以使绘图更容易。总的来说,我想知道我实现代码的方式是否有效。由于我没有制作 GUI 的经验,我不确定为每个按钮使用几乎相同的代码是否正确。 【参考方案1】:

下面是我做的一个很好的例子。 可以重复使用做更多的绘图而不增加代码,只需要改变self.num的值并使用函数add_data(x,y,ind)添加对应的数据,其中xy是数据的值,@987654330 @ 是框的索引(从0n-1)。

import sys
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)
        self.central_layout = QtGui.QVBoxLayout()
        self.plot_boxes_layout = QtGui.QHBoxLayout()
        self.boxes_layout = QtGui.QVBoxLayout()
        self.setLayout(self.central_layout)
        
        # Lets create some widgets inside
        self.label = QtGui.QLabel('Plots and Checkbox bellow:')
        
        # Here is the plot widget from pyqtgraph
        self.plot_widget = pg.PlotWidget()
        
        # Now the Check Boxes (lets make 3 of them)
        self.num = 6
        self.check_boxes = [QtGui.QCheckBox(f"Box i+1") for i in range(self.num)]
        
        # Here will be the data of the plot
        self.plot_data = [None for _ in range(self.num)]
        
        # Now we build the entire GUI
        self.central_layout.addWidget(self.label)
        self.central_layout.addLayout(self.plot_boxes_layout)
        self.plot_boxes_layout.addWidget(self.plot_widget)
        self.plot_boxes_layout.addLayout(self.boxes_layout)
        for i in range(self.num):
            self.boxes_layout.addWidget(self.check_boxes[i])
            # This will conect each box to the same action
            self.check_boxes[i].stateChanged.connect(self.box_changed)
            
        # For optimization let's create a list with the states of the boxes
        self.state = [False for _ in range(self.num)]
        
        # Make a list to save the data of each box
        self.box_data = [[[0], [0]] for _ in range(self.num)] 
        x = np.linspace(0, 3.14, 100)
        self.add_data(x, np.sin(x), 0)
        self.add_data(x, np.cos(x), 1)
        self.add_data(x, np.sin(x)+np.cos(x), 2)
        self.add_data(x, np.sin(x)**2, 3)
        self.add_data(x, np.cos(x)**2, 4)
        self.add_data(x, x*0.2, 5)
        

    def add_data(self, x, y, ind):
        self.box_data[ind] = [x, y]
        if self.plot_data[ind] is not None:
            self.plot_data[ind].setData(x, y)

    def box_changed(self):
        for i in range(self.num):
            if self.check_boxes[i].isChecked() != self.state[i]:
                self.state[i] = self.check_boxes[i].isChecked()
                if self.state[i]:
                    if self.plot_data[i] is not None:
                        self.plot_widget.addItem(self.plot_data[i])
                    else:
                        self.plot_data[i] = self.plot_widget.plot(*self.box_data[i])
                else:
                    self.plot_widget.removeItem(self.plot_data[i])
                break
        
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

请注意,在PlotWidget 内部,我使用plot() 方法添加绘图,它返回一个PlotDataItem,该PlotDataItem 保存在之前创建的名为self.plot_data 的列表中。 有了这个,您可以轻松地将其从Plot Widget 中删除并重新添加。此外,如果您的目标是更复杂的程序,例如,您可以在运行时更改每个框的数据,如果您在PlotDataItem 上使用setData() 方法,则绘图将更新而不会出现重大问题

正如我一开始所说的,这应该适用于很多复选框,因为当复选框被选中/未选中时调用的函数,首先将每个框的实际状态与前一个进行比较(存储在@ 987654341@) 并且只对与该特定框相对应的图进行更改。这样,您可以避免为每个复选框执行一个功能,并在每次选中/取消选中一个框时重新绘制所有 de 框(如 user8408080 所做的)。我并不是说它不好,但是如果你增加复选框的数量和/或数据的复杂性,重新绘制所有数据的工作量会急剧增加。

唯一的问题是当窗口太小而无法支持大量复选框(例如 96 个)时,您将不得不在另一个小部件而不是布局中组织复选框。

现在是上面代码的一些截图:

然后将self.num 的值更改为6 并向其中添加一些随机数据:

self.add_data(x, np.sin(x)**2, 3)
self.add_data(x, np.cos(x)**2, 4)
self.add_data(x, x*0.2, 5)

【讨论】:

【参考方案2】:

在下文中,我采用更暴力的方法,同时假设绘制所有曲线所需的时间可以忽略不计:

import numpy as np
import sys
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtWidgets

app = QtWidgets.QApplication(sys.argv)

x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2

curves = [y1, y2]
pens = ["r", "y"]

#This function is called whenever the state of checkboxes changes
def plot_curves(state):
    plot.clear()
    for checkbox, curve, pen in zip(checkboxes, curves, pens):
        if checkbox.isChecked():
            plot.plot(x, curve, pen=pen)

#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()

#Making a pyqtgraph plot widget
plot = pg.PlotWidget()

#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)

checkboxes = [QtWidgets.QCheckBox() for i in range(2)]
for i, checkbox in enumerate(checkboxes):
    checkbox.setText(f"Ai+1")
    checkbox.stateChanged.connect(plot_curves)
    layout.addWidget(checkbox, 0, i)

#Adding the widgets to the layout
layout.addWidget(plot, 1, 0, len(checkboxes), 0)

widget_holder.adjustSize()
widget_holder.show()

sys.exit(app.exec_())

现在您有了一个复选框列表,索引为 0 的复选框对应于索引为 0 的 curves-list 中的数据。我每次都绘制所有曲线,这会产生更易读的代码。但是,如果这确实会影响性能,则需要稍微复杂一些。

我还尝试添加另一条曲线,效果似乎非常好:

【讨论】:

【参考方案3】:

我在您的代码中发现了问题。让我们看看你的代码做了什么:

    当您将第一个绘图添加到小部件(A1A2)时,您将获得 PlotDataItem 并将其存储在 curve1curve2 中。假设您首先检查A1,然后您的todo 函数首先检查复选框1 是否已选中,因此绘制数据并将其存储在curve1 中,然后相同的函数检查复选框2。复选框2 未选中所以该函数执行else 语句,该语句从绘图小部件中删除curve2,此变量不存在,因此可能会引发错误,但是,您使用try 语句并且永远不会引发错误。

    现在,您选中 A2 框,您的函数首先检查复选框 1,它被选中,因此该函数将再次添加相同的绘图,但作为另一个 PlotDataItem,并将其存储在 curve1。到目前为止,您有两个相同数据的PlotDataItem(这意味着两个图),但只有最后一个存储在curve1 中。该函数的下一件事是检查复选框 2,它被选中,因此它将绘制第二个数据并将其 PlotDataItem 保存在 curve2

    所以,当您现在取消选中复选框 1 时,您的函数首先会检查复选框 1(如果它是重复的,很抱歉),它未被选中,因此该函数将删除存储在 curve1 中的 PlotDataItem 并且它会执行此操作,但是请记住,您有两个相同数据的图,因此对于我们(观众)来说,图不会消失。这就是问题所在,但还不止于此,该函数现在检查复选框 2,它已被选中,因此该函数将添加第二个数据的另一个 PlotDataItem 并将其存储在 curve2 中。我们将再次遇到与第一个数据相同的问题。

通过这个分析,我还学到了一些东西,如果你“覆盖”存储它的变量,PlotDataItem 不会消失,当它从PlotWidget 中删除时也不会消失。考虑到这一点,我对之前答案的代码进行了一些更改,因为每次我们选中之前选中但未选中的框时,旧代码都会创建另一个项目。现在,如果创建了项目,我的函数将再次添加它,而不是创建另一个。

我有一些建议:

尝试使用对象,生成您自己的小部件类。您可以避免调用全局变量,将它们作为类的属性传递。 (和我之前的回答一样)

如果您想按原样维护您的代码(不使用类),为了使其正常工作,您可以添加另外两个带有复选框“状态”的变量,因此当您首先调用您的函数时,它会检查如果状态没有改变并忽略该复选框。另外,检查PlotDataItem之前是否生成过,然后再添加一次,避免生成更多的项目。

您的目标是使用一堆框或按钮来执行此操作,尝试对所有这些框或按钮仅使用一个变量:例如,包含所有框/按钮(对象)的列表。然后你可以通过索引来管理它们中的任何一个。此外,您可以对该变量进行循环,以将内部的对象连接到同一个函数。

my_buttons = [ QtGui.QPushButton() for _ in range(number_of_buttons) ]
my_boxes= [ QtGui.QCheckBox() for _ in range(number_of_boxes) ]
my_boxes[0].setText('Box 1 Here')
my_boxes[2].setChecked(True)
for i in range(number_of_boxes):
    my_boxes[i].stateChanged.connect(some_function)

列出对象还可以帮助您轻松地自动命名:

my_boxes= [ QtGui.QCheckBox(f"Box number i+1") for i in range(number_of_boxes) ]
my_boxes= [ QtGui.QCheckBox(str(i+1)) for i in range(number_of_boxes) ]
my_boxes= [ QtGui.QCheckBox('Box :d'.format(i+1)) for i in range(number_of_boxes) ]

最后,这里是你的代码,做了一些小改动以使其工作:

from PyQt5 import QtWidgets, uic, QtGui
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
import sys
import string
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore

app = QtWidgets.QApplication(sys.argv)

x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2

#This function is called whenever the state of checkboxes changes
def todo():
    global b1st, b2st, curve1, curve2
    if cbx1.isChecked() != b1st:
        b1st = cbx1.isChecked()
        if cbx1.isChecked():
            if curve1 is None:
                curve1 = plot.plot(x, y1, pen = 'r')
            else:
                plot.addItem(curve1)
        else:
            plot.removeItem(curve1)

    if cbx2.isChecked() != b2st:
        b2st = cbx2.isChecked()
        if cbx2.isChecked():
            if curve2 is None:
                curve2 = plot.plot(x, y2, pen = 'y')
            else:
                plot.addItem(curve2)
        else:
            plot.removeItem(curve2)

#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()

#Checkboxes named A1 and A2
cbx1 = QtWidgets.QCheckBox()
cbx1.setText('A1')
cbx1.stateChanged.connect(todo)
b1st = False
curve1 = None

cbx2 = QtWidgets.QCheckBox()
cbx2.setText('A2')
cbx2.stateChanged.connect(todo)
b2st = False
curve2 = None

#Making a pyqtgraph plot widget
plot = pg.PlotWidget()

#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)

#Adding the widgets to the layout
layout.addWidget(cbx1, 0,0)
layout.addWidget(cbx2, 0, 1)
layout.addWidget(plot, 1,0, 3,1)

widget_holder.adjustSize()
widget_holder.show()

sys.exit(app.exec_())

【讨论】:

以上是关于如何更新pyqtgraph中的绘图?的主要内容,如果未能解决你的问题,请参考以下文章

Pyqtgraph 绘图很慢

如何在pyqtgraph中缩放绘图

pyqtgraph 绘图小部件未更新

为实时数据绘图实现 pyqtgraph

pyQtgraph 绘图延迟串行绘图

pyqtgraph:为绘图中的线条添加图例