Search code examples
pythonpython-3.xmatplotlibpyqtpyqt4

Python scatter updating using Qt4 - Qslider


I am trying to write a GUI in Python3 using PyQt4.

For data visualization, I need to isolate a specific point on a curve plotted by the function whole_plot(). To do so, I am currently using a slider that let the GUI user choose a point of interest. When the slider_value is changed, the point is selected and plotted by calling the function point_plot().

Regarding some previous answers, I am now trying to update my graph through matplotlib.animation (cf. post python matplotlib update scatter plot from a function). But for some reasons, I still got the wrong updating, can someone help me figure out what is the problem in my code?

import sys
import numpy as np
from PyQt4 import QtGui
from PyQt4 import QtCore
import matplotlib.pyplot as plt
import matplotlib.animation
#
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
#%%
# some Arbitrary data
nbr_points = 500
my_X_data = np.linspace(-10,10,nbr_points)
my_Y_data = my_X_data**3 + 100*np.cos(my_X_data*5)

class MyWidget(QtGui.QWidget):

    def __init__(self):
        super(MyWidget, self).__init__()
        self.initUI()

    def initUI(self):

        self.setGeometry(600,300,1000,600)

        grid = QtGui.QGridLayout()
        self.setLayout(grid)

        self.figure_1 = plt.figure(figsize=(15,5))
        self.canvas_1 = FigureCanvas(self.figure_1)

        self.toolbar = NavigationToolbar(self.canvas_1, self)
        grid.addWidget(self.canvas_1, 2,0,1,2)
        grid.addWidget(self.toolbar, 0,0,1,2)


        # Slider
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        self.slider.setMinimum(0) 
        self.slider.setMaximum(nbr_points-1) 
        self.slider.setTickInterval(1)
        self.slider.valueChanged.connect(self.point_plot)

        # Slider info box
        self.label = QtGui.QLabel(self)
        grid.addWidget(self.label,4,0)

        #  +1 / -1 buttons
        btn_plus_one = QtGui.QPushButton('+1', self)
        btn_plus_one.clicked.connect(self.value_plus_one)
        btn_minus_one = QtGui.QPushButton('-1', self)
        btn_minus_one.clicked.connect(self.value_minus_one)       

        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(btn_minus_one)
        hbox.addWidget(self.slider)
        hbox.addWidget(btn_plus_one)
        grid.addLayout(hbox, 3,0,1,3)


        self.whole_plot()
        self.point_plot()
        self.show()



    def whole_plot(self):
        ax1 = self.figure_1.add_subplot(111)
        ax1.clear()
        ax1.cla()
        #
        ax1.plot(my_X_data,my_Y_data,'b-')        
        #
        ax1.set_xlim([-10,10])  
        ax1.set_ylim([-1000,1000])  
        ax1.set_xlabel('X')             
        ax1.set_ylabel('Y')  
        #
        self.canvas_1.draw()


    def point_plot(self):      

        ax1 = self.figure_1.add_subplot(111)       
        #     
        X_point, Y_point = [],[]
        scat = ax1.scatter(X_point,Y_point, s=100,c='k')

        def animate(i):
            index_slider_value = self.slider.value()
            X_point = my_X_data[index_slider_value,]
            Y_point = my_Y_data[index_slider_value,]
            scat.set_offsets(np.c_[X_point,Y_point])  

        anim = matplotlib.animation.FuncAnimation(self.figure_1,animate, frames=my_X_data, interval=200, repeat=True) 
        self.canvas_1.draw()


    def value_plus_one(self):
        # slider +1
        if self.slider.value() < (my_X_data.shape[0]-1): 
            index_slider_value = self.slider.value() + 1
            self.slider.setValue(index_slider_value)


    def value_minus_one(self):
        # slider -1
        if self.slider.value() > 0:
            index_slider_value = self.slider.value() - 1
            self.slider.setValue(index_slider_value)



def main():
    app = QtGui.QApplication(sys.argv)
    MyWidget()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

Solution

  • You have to learn what is better to reuse than create, in this case you just need to create a scatter and update the data with set_offsets() when the value of the slider changes, so FuncAnimation is not necessary.

    import numpy as np
    from PyQt4 import QtCore, QtGui
    import matplotlib.pyplot as plt
    import matplotlib.animation
    #
    from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
    from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
    #%%
    # some Arbitrary data
    nbr_points = 500
    my_X_data = np.linspace(-10,10,nbr_points)
    my_Y_data = my_X_data**3 + 100*np.cos(my_X_data*5)
    
    class MyWidget(QtGui.QWidget):
        def __init__(self):
            super(MyWidget, self).__init__()
            self.initUI()
    
        def initUI(self):
            self.setGeometry(600,300,1000,600)
            self.figure_1 = plt.figure(figsize=(15,5))
            self.canvas = FigureCanvas(self.figure_1)
            self.toolbar = NavigationToolbar(self.canvas, self)
            # Slider
            self.slider = QtGui.QSlider(minimum=0, 
                maximum= nbr_points-1, 
                orientation=QtCore.Qt.Horizontal,
                tickInterval=1)
            self.slider.valueChanged.connect(self.on_valueChanged)
            # Slider info box
            self.label = QtGui.QLabel()
            #  +1 / -1 buttons
            btn_plus_one = QtGui.QPushButton('+1')
            btn_plus_one.clicked.connect(self.value_plus_one)
            btn_minus_one = QtGui.QPushButton('-1')
            btn_minus_one.clicked.connect(self.value_minus_one)       
    
            grid = QtGui.QGridLayout(self)
            grid.addWidget(self.canvas, 2, 0, 1, 2)
            grid.addWidget(self.toolbar, 0, 0, 1, 2)
            grid.addWidget(self.label, 4, 0)
            hbox = QtGui.QHBoxLayout()
            hbox.addWidget(btn_minus_one)
            hbox.addWidget(self.slider)
            hbox.addWidget(btn_plus_one)
            grid.addLayout(hbox, 3, 0, 1, 3)
            self.whole_plot()
    
        def whole_plot(self):
            ax = self.figure_1.add_subplot(111)
            ax.clear()
            ax.plot(my_X_data,my_Y_data,'b-')        
            ax.set_xlim([-10,10])  
            ax.set_ylim([-1000,1000])  
            ax.set_xlabel('X')             
            ax.set_ylabel('Y')  
            self.canvas.draw()
            X_point, Y_point = [],[]
            self.scat = ax.scatter(X_point, Y_point, s=100,c='k')
            # set initial
            self.on_valueChanged(self.slider.value())
    
        @QtCore.pyqtSlot(int)
        def on_valueChanged(self, value):
            X_point = my_X_data[value,]
            Y_point = my_Y_data[value,]
            self.scat.set_offsets(np.c_[X_point,Y_point])   
            self.canvas.draw()
    
        @QtCore.pyqtSlot()
        def value_plus_one(self):
            self.slider.setValue(self.slider.value() + 1)
    
        @QtCore.pyqtSlot()
        def value_minus_one(self):
            self.slider.setValue(self.slider.value() - 1)
    
    def main():
        import sys
        app = QtGui.QApplication(sys.argv)
        w = MyWidget()
        w.show()
        sys.exit(app.exec_())
    
    if __name__ == '__main__':
        main()
    

    On the other hand QSlider will not update the value if it is less than minimum or greater than maximum