pythonpyqt5candlestick-chartqchart

Dynamically updating a QChart


I have a class that plots a candlestick for given data. I am trying to dynamically update the plot as soon a new data is received. Data is received at irregular intervals.

What mechanism can I use in order to let the class know it's time to update the plot and act upon updating the plot with the newly received data point?

Class function append_data_and_plot() appends the data but the plot never gets updated. Could someone kindly shed light on what the issue is?

import sys
from PyQt5.QtChart import QCandlestickSeries, QChart, QChartView, QCandlestickSet
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.QtCore import Qt, QPointF
from PyQt5 import QtChart as qc
import time

class myCandlestick():
    def __init__(self, data):
        self.data = data
        self.app = QApplication(sys.argv)

        self.series = QCandlestickSeries()
        self.series.setDecreasingColor(Qt.red)
        self.series.setIncreasingColor(Qt.green)

        self.ma5 = qc.QLineSeries() 
        self.tm = []

        for num, o, h, l, c, m in self.data:
            self.series.append(QCandlestickSet(o, h, l, c))
            self.ma5.append(QPointF(num, m))
            self.tm.append(str(num))

        self.chart = QChart()

        self.chart.addSeries(self.series)  # candle
        self.chart.addSeries(self.ma5)  # ma5 line

        self.chart.createDefaultAxes()
        self.chart.legend().hide()

        self.chart.axisX(self.series).setCategories(self.tm)
        self.chart.axisX(self.ma5).setVisible(False)

        self.chartview = QChartView(self.chart)
        self.ui = QMainWindow()
        self.ui.setGeometry(50, 50, 500, 300)
        self.ui.setCentralWidget(self.chartview)
        self.ui.show()
        sys.exit(self.app.exec_())

    def append_data_and_plot(self, d):
        '''Append and update the plot'''
        num, o, h, l, c, m = d
        self.series.append(QCandlestickSet(o, h, l, c))
        self.ui.show()
        #sys.exit(self.app.exec_())


data = ((1, 7380, 7520, 7380, 7510, 7324),
        (2, 7520, 7580, 7410, 7440, 7372),
        (3, 7440, 7650, 7310, 7520, 7434),
        (4, 7450, 7640, 7450, 7550, 7480),
        (5, 7510, 7590, 7460, 7490, 7502),
        (6, 7500, 7590, 7480, 7560, 7512),
        (7, 7560, 7830, 7540, 7800, 7584))

m = myCandlestick(data)

# Data is received at irregular intervals
time.sleep(1)
m.append_data_and_plot((8, 7560, 7830, 7540, 7800, 7584))

# Data is received at irregular intervals
time.sleep(0.1)
m.append_data_and_plot((9, 7450, 7640, 7450, 7550, 7480))

# Data is received at irregular intervals
time.sleep(2.5)
m.append_data_and_plot((10, 7380, 7520, 7380, 7510, 7324))

Solution

  • Qt uses an event loop to handle the events and tasks of the gui, and that is the code "app.exec_ ()" so all the code after that line will not be executed, and if it is added to that it is used sys.exit () will make the application terminate when the Qt eventloop ends.

    The solution is to use the eventloop to update the gui, for example using a QTimer:

    import sys
    from PyQt5.QtChart import QCandlestickSeries, QChart, QChartView, QCandlestickSet
    from PyQt5.QtWidgets import QApplication, QMainWindow
    from PyQt5.QtCore import Qt, QPointF, QObject, pyqtSignal, QTimer
    from PyQt5 import QtChart as qc
    
    import random
    
    
    class MainWindow(QMainWindow):
        def __init__(self, data, parent=None):
            super().__init__(parent)
            self.data = data
    
            self.series = QCandlestickSeries()
            self.series.setDecreasingColor(Qt.red)
            self.series.setIncreasingColor(Qt.green)
    
            self.ma5 = qc.QLineSeries()
            self.tm = []
    
            self.chart = QChart()
    
            self.chart.addSeries(self.series)  # candle
            self.chart.addSeries(self.ma5)  # ma5 line
    
            self.chart.createDefaultAxes()
            self.chart.legend().hide()
    
            self.chart.axisX(self.series).setCategories(self.tm)
            self.chart.axisX(self.ma5).setVisible(False)
    
            self.chartview = QChartView(self.chart)
            self.setGeometry(50, 50, 500, 300)
            self.setCentralWidget(self.chartview)
    
        def append_data_and_plot(self, d):
            """Append and update the plot"""
            num, o, h, l, c, m = d
    
            ax1 = self.chart.axisX(self.ma5)
            ay1 = self.chart.axisY(self.ma5)
    
            xmin = xmax = num
            ymin = ymax = m
    
            step = 10
            offset = 100
    
            for p in self.ma5.pointsVector()[-step:]:
                xmin = min(p.x(), xmin)
                xmax = max(p.x(), xmax)
    
                ymin = min(p.y(), ymin) - offset
                ymax = max(p.y(), ymax) + offset
    
            xmin = max(0, xmax - step)
    
            ax1.setMin(xmin)
            ax1.setMax(xmax)
            ay1.setMin(ymin)
            ay1.setMax(ymax)
    
            self.ma5.append(QPointF(num, m))
            self.tm.append(str(num))
    
            self.series.append(QCandlestickSet(o, h, l, c))
            ax2 = self.chart.axisX(self.series)
            ax2.setCategories(self.tm)
            ax2.setMin(str(xmin))
            ax2.setMax(str(xmax))
    
            ay2 = self.chart.axisY(self.series)
            ay2.setMin(ymin)
            ay2.setMax(ymax)
    
    
    def create_data():
    
    
        i = 1
        while True:
            i += 1
            yield (
                i,
                random.randint(7000, 8000),
                random.randint(7000, 8000),
                random.randint(7000, 8000),
                random.randint(7000, 8000),
                random.randint(7000, 8000),
            )
    
    
    class Producer(QObject):
        dataChanged = pyqtSignal(list)
    
        def __init__(self, parent=None):
            super().__init__(parent)
            self.iter = create_data()
            QTimer.singleShot(random.randint(0, 1500), self.send_data)
    
        def send_data(self):
            d = list(next(self.iter))
            self.dataChanged.emit(d)
            QTimer.singleShot(random.randint(0, 1500), self.send_data)
    
    
    def main():
        app = QApplication(sys.argv)
    
        data = ((1, 7380, 7520, 7380, 7510, 7324),)
        w = MainWindow(data)
        w.show()
    
        p = Producer()
        p.dataChanged.connect(w.append_data_and_plot)
    
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()