I am streaming TimeSeries that I want to chart efficiently (20+ chart live on a small computer). I have tried PyQtChart and pyqtgraph on PyQt5, but with both libs, I am ending up redrawing the whole chart for each data that I receive, which doesn't feel optimal. I settled for PyQtChart because it was handling better DatetimeSeries, but happy to be proven wrong (and share the pyqtgraph code, just didn't want to make the post too big).
Bellow is my working code with PyQtChart using random datas, so that you can run it:
import sys
from random import randint
from typing import Union
from PyQt5.QtChart import (QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis)
from PyQt5.QtCore import Qt, QDateTime, QTimer
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import (QWidget, QGridLayout)
class Window(QWidget):
def __init__(self, window_name: str = 'Ticker'):
QWidget.__init__(self)
# GUI
self.setGeometry(200, 200, 600, 400)
self.window_name: str = window_name
self.setWindowTitle(self.window_name)
layout = QGridLayout(self)
# change the color of the window
self.setStyleSheet('background-color:black')
# Series
self.high_dataset = QLineSeries()
self.low_dataset = QLineSeries()
self.mid_dataset = QLineSeries()
self.low_of_day: Union[float, None] = 5
self.high_of_day: Union[float, None] = 15
# Y Axis
self.time_axis_y = QValueAxis()
self.time_axis_y.setLabelFormat("%.2f")
self.time_axis_y.setTitleText("Price")
# X Axis
self.time_axis_x = QDateTimeAxis()
self.time_axis_x.setFormat("hh:mm:ss")
self.time_axis_x.setTitleText("Datetime")
# Events
self.qt_timer = QTimer()
# QChart
self.chart = QChart()
self.chart.addSeries(self.mid_dataset)
self.chart.addSeries(self.high_dataset)
self.chart.addSeries(self.low_dataset)
self.chart.setTitle("Barchart Percent Example")
self.chart.setTheme(QChart.ChartThemeDark)
# https://linuxtut.com/fr/35fb93c7ca35f9665d9f/
self.chart.legend().setVisible(True)
self.chart.legend().setAlignment(Qt.AlignBottom)
self.chartview = QChartView(self.chart)
# using -1 to span through all rows available in the window
layout.addWidget(self.chartview, 2, 0, -1, 3)
self.chartview.setChart(self.chart)
def set_yaxis(self):
# Y Axis Settings
self.time_axis_y.setRange(int(self.low_of_day * .9), int(self.high_of_day * 1.1))
self.chart.addAxis(self.time_axis_y, Qt.AlignLeft)
self.mid_dataset.attachAxis(self.time_axis_y)
self.high_dataset.attachAxis(self.time_axis_y)
self.low_dataset.attachAxis(self.time_axis_y)
def set_xaxis(self):
# X Axis Settings
self.chart.removeAxis(self.time_axis_x)
self.time_axis_x = QDateTimeAxis()
self.time_axis_x.setFormat("hh:mm:ss")
self.time_axis_x.setTitleText("Datetime")
self.chart.addAxis(self.time_axis_x, Qt.AlignBottom)
self.mid_dataset.attachAxis(self.time_axis_x)
self.high_dataset.attachAxis(self.time_axis_x)
self.low_dataset.attachAxis(self.time_axis_x)
def start_app(self):
self.qt_timer.timeout.connect(self.retrieveStream, )
time_to_wait: int = 500 # milliseconds
self.qt_timer.start(time_to_wait)
def retrieveStream(self):
date_px = QDateTime()
date_px = date_px.currentDateTime().toMSecsSinceEpoch()
print(date_px)
mid_px = randint(int((self.low_of_day + 2) * 100), int((self.high_of_day - 2) * 100)) / 100
self.mid_dataset.append(date_px, mid_px)
self.low_dataset.append(date_px, self.low_of_day)
self.high_dataset.append(date_px, self.high_of_day)
print(f"epoch: {date_px}, mid: {mid_px:.2f}")
self.update()
def update(self):
print("updating chart")
self.chart.removeSeries(self.mid_dataset)
self.chart.removeSeries(self.low_dataset)
self.chart.removeSeries(self.high_dataset)
self.chart.addSeries(self.mid_dataset)
self.chart.addSeries(self.high_dataset)
self.chart.addSeries(self.low_dataset)
self.set_yaxis()
self.set_xaxis()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
window.start_app()
sys.exit(app.exec_())
Biggest worries with this code are:
- the 'update' method which basically re-draw every element of the chart=>I would have prefered a deque, refresh/update/refire type of solution
- QLineSeries do not seem to have a maxLen like deque collection, so I could end up with loads of data (ideally looking to run more than three QLineSeries)
Besides that, I would be grateful to receive any insigths about how to optimize this code. I am new to Qt/Asyncio/Threading and really keen to learn.
Best
EDIT chart now updating without redrawing everything Let me know if there is a better way, or code needing improvement as i am new to Qt.
Thanks to answer bellow (@domarm) I corrected the way I was updating chart and link bellow made me aware I needed to set a min max for axis at each refresh so that data are within scope.
import sys
from datetime import datetime
from random import randint
from typing import Union, Optional
from PyQt5.QtChart import (QChart, QChartView, QLineSeries, QDateTimeAxis, QValueAxis)
from PyQt5.QtCore import (Qt, QDateTime, QTimer, QPointF)
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (QWidget, QGridLayout, QLabel, QApplication)
# https://doc.qt.io/qt-5/qtcharts-modeldata-example.html
class Window(QWidget):
running = False
def __init__(self, window_name: str = 'Chart',
chart_title: Optional[str] = None,
geometry_ratio: int = 2,
histo_tick_size: int = 200):
QWidget.__init__(self)
# GUI
self.window_wideness: int = 300
self.histo_tick_size: int = histo_tick_size
self.setGeometry(200,
200,
int(self.window_wideness * geometry_ratio),
self.window_wideness
)
self.window_name: str = window_name
self.setWindowTitle(self.window_name)
self.label_color: str = 'grey'
self.text_color: str = 'white'
# Layout
layout = QGridLayout(self)
# Gui components
bold_font = QFont()
bold_font.setBold(True)
self.label_last_px = QLabel('-', self)
self.label_last_px.setFont(bold_font)
self.label_last_px.setStyleSheet("QLabel { color : blue; }")
layout.addWidget(self.label_last_px)
# change the color of the window
self.setStyleSheet('background-color:black')
# QChart
self.chart = QChart()
if chart_title:
self.chart.setTitle(chart_title)
# Series
self.high_dataset = QLineSeries(self.chart)
self.high_dataset.setName("High")
self.low_dataset = QLineSeries(self.chart)
self.low_dataset.setName("Low")
self.mid_dataset = QLineSeries(self.chart)
self.mid_dataset.setName("Mid")
self.low_of_day: Union[float, None] = 5
self.high_of_day: Union[float, None] = 15
self.last_data_point: dict = {"last_date": None, "mid_px": None, "low_px": None, "high_px": None}
# Y Axis
self.time_axis_y = QValueAxis()
self.time_axis_y.setLabelFormat("%.2f")
self.time_axis_y.setTitleText("Price")
# X Axis
self.time_axis_x = QDateTimeAxis()
self.time_axis_x.setTitleText("Datetime")
# Events
self.qt_timer = QTimer()
self.chart.setTheme(QChart.ChartThemeDark)
self.chart.addSeries(self.mid_dataset)
self.chart.addSeries(self.low_dataset)
self.chart.addSeries(self.high_dataset)
# https://linuxtut.com/fr/35fb93c7ca35f9665d9f/
self.chart.legend().setVisible(True)
# self.chart.legend().setAlignment(Qt.AlignBottom)
self.chartview = QChartView(self.chart)
# self.chartview.chart().setAxisX(self.axisX, self.mid_dataset)
# using -1 to span through all rows available in the window
layout.addWidget(self.chartview, 2, 0, -1, 3)
self.chartview.setChart(self.chart)
def set_yaxis(self):
# Y Axis Settings
self.time_axis_y.setRange(int(self.low_of_day * .9), int(self.high_of_day * 1.1))
self.chart.addAxis(self.time_axis_y, Qt.AlignLeft)
self.mid_dataset.attachAxis(self.time_axis_y)
self.high_dataset.attachAxis(self.time_axis_y)
self.low_dataset.attachAxis(self.time_axis_y)
def set_xaxis(self):
# X Axis Settings
self.chart.removeAxis(self.time_axis_x)
# X Axis
self.time_axis_x = QDateTimeAxis()
self.time_axis_x.setFormat("hh:mm:ss")
self.time_axis_x.setTitleText("Datetime")
point_first: QPointF = self.mid_dataset.at(0)
point_last: QPointF = self.mid_dataset.at(len(self.mid_dataset) - 1)
# needs to be updated each time for chart to render
# https://stackoverflow.com/questions/57079698/qdatetimeaxis-series-are-not-displayed
self.time_axis_x.setMin(QDateTime().fromMSecsSinceEpoch(point_first.x()).addSecs(0))
self.time_axis_x.setMax(QDateTime().fromMSecsSinceEpoch(point_last.x()).addSecs(0))
self.chart.addAxis(self.time_axis_x, Qt.AlignBottom)
self.mid_dataset.attachAxis(self.time_axis_x)
self.high_dataset.attachAxis(self.time_axis_x)
self.low_dataset.attachAxis(self.time_axis_x)
def _update_label_last_px(self):
last_point: QPointF = self.mid_dataset.at(self.mid_dataset.count() - 1)
last_date: datetime = datetime.fromtimestamp(last_point.x() / 1000)
last_price = last_point.y()
self.label_last_px.setText(f"Date time: {last_date.strftime('%d-%m-%y %H:%M %S')} "
f"Price: {last_price:.2f}")
def start_app(self):
"""Start Thread generator"""
# This method is supposed to stream data but not the issue, problem is that chart is not updating
self.qt_timer.timeout.connect(self.update, )
time_to_wait: int = 250 # milliseconds
self.qt_timer.start(time_to_wait)
def update(self):
""" Update chart and Label with the latest data in Series"""
print("updating chart")
self._update_label_last_px()
# date_px = QDateTime()
# self.last_data_point['last_date'] = date_px.currentDateTime().toMSecsSinceEpoch()
date_px = datetime.now().timestamp() * 1000
self.last_data_point['last_date'] = date_px
# Make up a price
self.last_data_point['mid_px'] = randint(int((self.low_of_day + 2) * 100),
int((self.high_of_day - 2) * 100)) / 100
self.last_data_point['low_date'] = self.low_of_day
self.last_data_point['high_date'] = self.high_of_day
print(self.last_data_point)
# Feed datasets and simulate deque
# https://www.qtcentre.org/threads/67774-Dynamically-updating-QChart
if self.mid_dataset.count() > self.histo_tick_size:
self.mid_dataset.remove(0)
self.low_dataset.remove(0)
self.high_dataset.remove(0)
self.mid_dataset.append(self.last_data_point['last_date'], self.last_data_point['mid_px'])
self.low_dataset.append(self.last_data_point['last_date'], self.last_data_point['low_date'])
self.high_dataset.append(self.last_data_point['last_date'], self.last_data_point['high_date'])
self.set_xaxis()
self.set_yaxis()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
window.start_app()
sys.exit(app.exec())