Как обновить график в pyqtgraph?

Я пытаюсь создать пользовательский интерфейс, используя PyQt5 и pyqtgraph. Я сделал два флажка, и всякий раз, когда я их выбираю, я хочу построить один из двух наборов данных, доступных в коде, и всякий раз, когда я отменяю выбор кнопки, я хочу, чтобы она очищала соответствующую кривую. Есть два флажка с текстами A1 и A2, и каждый из них отображает один набор данных.

У меня есть две проблемы:

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


    

person MOON    schedule 21.12.2020    source источник
comment
96 кнопок? Это много для пользовательского интерфейса.   -  person JHBonarius    schedule 23.12.2020
comment
@JHBonarius В этом конкретном пользовательском интерфейсе я пытаюсь построить кривые роста некоторых клеток. Анализ роста обычно проводится в 96-луночных планшетах, и было бы неплохо иметь возможность проверять отдельные кривые роста. На более поздних этапах я попытаюсь добавить функциональность, которая могла бы выбирать группу кнопок, выбирая область над ними, чтобы упростить построение графика. В целом я хочу знать, эффективен ли способ, которым я реализую код, или нет. Поскольку у меня нет опыта создания GUI, я не уверен, что правильно использовать почти одинаковый код для каждой кнопки.   -  person MOON    schedule 23.12.2020


Ответы (3)


Ниже приведен пример, который я сделал, который отлично работает. Его можно повторно использовать для создания большего количества графиков без увеличения кода, просто изменив значение self.num и добавив соответствующие данные с помощью функции add_data(x,y,ind), где x и y — значения данных, а ind — индекс поля (из 0 до n-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_())

Обратите внимание, что внутри de PlotWidget я добавляю график с помощью метода plot(), он возвращает PlotDataItem, который сохраняется в списке, созданном до вызова self.plot_data. При этом вы можете легко удалить его из Plot Widget и добавить снова. Кроме того, если вы стремитесь к более сложной программе, например, такой, в которой вы можете изменять данные каждого блока во время выполнения, график будет обновляться без серьезных проблем, если вы используете метод setData() на PlotDataItem

Как я сказал в начале, это должно нормально работать с большим количеством флажков, потому что функция, которая вызывается, когда флажок отмечен/не отмечен, сначала сравнивает фактическое состояние каждого флажка с предыдущим (хранящимся в self.state) и только внесите изменения на графике, соответствующие этому конкретному блоку. При этом вы избегаете выполнения одной функции для каждого флажка и перерисовки всех полей каждый раз, когда вы устанавливаете/снимаете флажок (например, 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)

введите здесь описание изображения

person Alejandro Condori    schedule 23.12.2020

Далее я использую более грубый подход, предполагая, что построение всех кривых занимает незначительное количество времени:

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"A{i+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 соответствует данным в curves-списке с индексом 0. Я каждый раз строю все кривые, что дает немного более читаемый код. Однако, если это влияет на производительность, это должно быть немного сложнее.

Я также попытался добавить еще одну кривую, и, кажется, все работает отлично:

Изображение с тремя кривыми

person user8408080    schedule 23.12.2020

Я нашел проблему в вашем коде. Давайте посмотрим, что делает ваш код:

  1. Когда вы добавляете первый график в виджет (либо A1, либо A2), вы получаете PlotDataItem и сохраняете его в curve1 или curve2. Предположим, вы сначала проверяете A1, затем ваша функция todo сначала проверяет, что флажок 1 отмечен, поэтому нанесите данные и сохраните их в curve1, затем та же функция проверяет флажок 2. Флажок 2 не установлен, поэтому функция выполняет оператор else , который удаляет curve2 из виджета графика, этой переменной не существует, поэтому она может вызвать ошибку, однако вы используете оператор try, и ошибка никогда не возникает.

  2. Теперь вы устанавливаете флажок A2, ваша функция сначала проверяет флажок 1, он отмечен, поэтому функция снова добавит тот же график, но как еще один PlotDataItem, и сохранит его в curve1. До сих пор у вас было два PlotDataItem одних и тех же данных (это означает два графика), но только последний хранится в curve1. Следующее, что делает функция, это проверяет флажок 2, он отмечен, поэтому он будет отображать вторые данные и сохранять их PlotDataItem в curve2.

  3. Итак, когда вы теперь снимаете флажок 1, ваша функция сначала проверяет флажок 1 (извините, если он повторяется), он не отмечен, поэтому функция удалит PlotDataItem, хранящийся в curve1, и сделает это, но помните, что у вас есть два графика те же данные, поэтому для нас (зрителей) сюжет не исчезает. В этом проблема, но она на этом не заканчивается, теперь функция проверяет флажок 2, он отмечен, поэтому функция добавит еще PlotDataItem вторых данных и сохранит их в curve2. У нас снова будет та же проблема, что и с первыми данными.

Благодаря этому анализу я также кое-что узнал: PlotDataItem не исчезает, если вы перезаписываете переменную, в которой он хранится, и не исчезает, когда он удаляется из PlotWidget. Учитывая это, я внес некоторые изменения в код своего предыдущего ответа, потому что старый код будет создавать новый элемент каждый раз, когда мы устанавливаем флажок, который был установлен ранее и не отмечен. Теперь, если элемент создан, моя функция добавит его снова, а не создаст еще один.

У меня есть несколько предложений:

  • Попробуйте использовать объекты, создайте свой собственный класс виджетов. Вы можете не вызывать глобальные переменные, передавая их как атрибуты класса. (как и мой предыдущий ответ)

  • Если вы хотите сохранить свой код как есть (без использования классов), чтобы он работал, вы можете добавить еще две переменные с состоянием ваших флажков, поэтому, когда вы сначала вызываете свою функцию, она проверяет, не произошло ли состояние. t изменить и игнорировать этот флажок. Кроме того, проверьте, был ли 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_())
person Alejandro Condori    schedule 24.12.2020