Search code examples
pythonqtablewidgetpyside2qtcharts

How to plot data from QTableWidget into QChart


I'm working on data analysis software, which takes data from remote database and puts it into QTableWidget. How could I effectively get these data from table and put them into QChart?

I've seen that if I had been using QTableView, it could have been done with models, but as I understand it, using QTableView would be far more complicated for my scenario.

from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtCharts import *
import sys
import random


class DateTimeDelegate(QStyledItemDelegate):
    def initStyleOption(self, option, index):
        super(DateTimeDelegate, self).initStyleOption(option, index)
        value = index.data()
        option.text = 
QDateTime.fromMSecsSinceEpoch(value).toString("dd.MM.yyyy")


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setGeometry(0, 0, 1280, 400)
        self.chart_table()

        self.populate()
    def chart_table(self):
        self.table = QTableWidget(0, 2)
        delegate = DateTimeDelegate(self.table)
        self.table.setItemDelegateForColumn(0, delegate)
        chart = QtCharts.QChart()
        self.chartView = QtCharts.QChartView(chart)
        self.chartView.setFixedSize(600, 430)

        splitter = QSplitter(self)
        splitter.addWidget(self.table)
        splitter.addWidget(self.chartView)

        self.setCentralWidget(splitter)

        series = QtCharts.QLineSeries(name='Odoslané')
        mapper = QtCharts.QVXYModelMapper(xColumn=0, yColumn=2)
        mapper.setModel(self.table.model())
        mapper.setSeries(series)
        chart.addSeries(mapper.series())

        self.axis_X = QtCharts.QDateTimeAxis()
        self.axis_X.setFormat("MMM yyyy")
        self.axis_Y = QtCharts.QValueAxis()

        chart.setAxisX(self.axis_X, series)
        chart.setAxisY(self.axis_Y, series)
        self.axis_Y.setRange(0, 0)
        self.axis_Y.setLabelFormat('%.0f')
        self.axis_X.setRange(QDate(2017, 10, 1), QDate.currentDate())

        chart.setTitle('Chart')

    def addRow(self, dt, value):
        self.table.insertRow(0)

        for col, v in enumerate((dt.toMSecsSinceEpoch(), value)):
            it = QTableWidgetItem()
            it.setData(Qt.DisplayRole, dt.toMSecsSinceEpoch())
            self.table.setItem(0, 0, it)
        t_m, t_M = self.axis_X.min(), self.axis_X.max()
        t_m = min(t_m, dt)
        t_M = max(t_M, dt)

        m, M = self.axis_Y.min(), self.axis_Y.max()
        m = min(m, value)
        M = max(M, value)

In this method I simulate filling table with data as I get them from database.

def populate(self):
    for i in range(4):
        count=random.randint(1,40)
        value_str = QDate.currentDate().addDays(count).toString('dd.MM.yyyy')

        dt = QDateTime.fromString(value_str, "dd.MM.yyyy")

        sent = QTableWidgetItem(str(count))
        value = int(sent.text())

        self.addRow(dt, value)
        self.table.setItem(0, 1, sent)

And App running function -

def main():
    app = QApplication(sys.argv)
    gui = MainWindow()
    gui.show()
    sys.exit(app.exec_())

main()

Solution

  • The easiest way to show the data of a QTableWidget in a QChartView is to use a QVXYModelMapper that relates the model of the QTableWidget with a QLineSerie. But for this the data stored in the QTableWidget should not be a string but an integer so you should not convert the QDateTime to string using toString(), but to an integer using toMSecsSinceEpoch(), and to show it as datetime in the QTableWidget a delegate should be used.

    In the following example the addRow method allows to add a (QDateTime, value) to a row, this recalculates the ranges of each axis.

    import random
    from functools import partial
    from PySide2 import QtCore, QtGui, QtWidgets
    from PySide2.QtCharts import QtCharts
    
    
    class DateTimeDelegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super(DateTimeDelegate, self).initStyleOption(option, index)
            value = index.data()
            option.text = QtCore.QDateTime.fromMSecsSinceEpoch(value).toString("dd.MM.yyyy")
    
    
    class MainWindow(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
    
            self.m_tablewidget = QtWidgets.QTableWidget(0, 2)
            delegate = DateTimeDelegate(self.m_tablewidget)
            self.m_tablewidget.setItemDelegateForColumn(0, delegate)
            self.m_chartview = QtCharts.QChartView()
            self.m_chartview.chart().setTheme(QtCharts.QChart.ChartThemeQt)
            self.m_chartview.setMinimumWidth(400)
    
            self.m_series = QtCharts.QLineSeries(name="Time-Value")
            self.m_mapper = QtCharts.QVXYModelMapper(self, xColumn=0, yColumn=1)
            self.m_mapper.setModel(self.m_tablewidget.model())
            self.m_mapper.setSeries(self.m_series)
    
            self.m_chartview.chart().addSeries(self.m_mapper.series())
    
            splitter = QtWidgets.QSplitter(self)
            splitter.addWidget(self.m_tablewidget)
            splitter.addWidget(self.m_chartview)
            self.setCentralWidget(splitter)
    
            self.m_time_axis = QtCharts.QDateTimeAxis()
            self.m_time_axis.setFormat("dd.MM.yyyy")
            self.m_value_axis = QtCharts.QValueAxis()
    
            self.m_chartview.chart().setAxisX(self.m_time_axis, self.m_series)
            self.m_chartview.chart().setAxisY(self.m_value_axis, self.m_series)
    
            self.m_value_axis.setRange(0, 0)
            self.m_time_axis.setRange(
                QtCore.QDateTime.currentDateTime(),
                QtCore.QDateTime.currentDateTime().addDays(1),
            )
    
        def addRow(self, dt, value):
            row = self.m_tablewidget.rowCount()
            self.m_tablewidget.insertRow(row)
            for col, v in enumerate((dt.toMSecsSinceEpoch(), value)):
                it = QtWidgets.QTableWidgetItem()
                it.setData(QtCore.Qt.DisplayRole, v)
                self.m_tablewidget.setItem(row, col, it)
            t_m, t_M = self.m_time_axis.min(), self.m_time_axis.max()
            t_m = min(t_m, dt)
            t_M = max(t_M, dt)
    
            m, M = self.m_value_axis.min(), self.m_value_axis.max()
            m = min(m, value)
            M = max(M, value)
    
            self.m_time_axis.setRange(t_m, t_M)
            self.m_value_axis.setRange(m, M)
    
    
    counter = 0
    
    
    def onTimeout(w):
        # Emulate the data
        global counter
        dt = QtCore.QDateTime.currentDateTime().addDays(counter)
        value = random.uniform(-100, 100)
        w.addRow(dt, value)
    
        counter += 1
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
    
        w = MainWindow()
        w.resize(640, 480)
        w.show()
    
        wrapper = partial(onTimeout, w)
        timer = QtCore.QTimer(timeout=wrapper, interval=1000)
        timer.start()
    
        sys.exit(app.exec_())
    

    enter image description here


    Update:

    You do not have to create any QTableWidget in the populate method. I have corrected your logic so that it is added to the top of the QTableWidget, also I have corrected the calculation of the range.

    import sys
    import random
    from PySide2.QtCore import *
    from PySide2.QtWidgets import *
    from PySide2.QtCharts import QtCharts
    
    
    class DateTimeDelegate(QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super(DateTimeDelegate, self).initStyleOption(option, index)
            value = index.data()
            option.text = QDateTime.fromMSecsSinceEpoch(value).toString("dd.MM.yyyy")
    
    
    class MainWindow(QMainWindow):
        def __init__(self, parent=None):
            super(MainWindow, self).__init__(parent)
    
            self.setGeometry(0, 0, 1280, 400)
            self.chart_table()
    
            self.populate()
    
        def chart_table(self):
            self.table = QTableWidget(0, 2)
            delegate = DateTimeDelegate(self.table)
            self.table.setItemDelegateForColumn(0, delegate)
            chart = QtCharts.QChart()
            self.chartView = QtCharts.QChartView(chart)
            self.chartView.setFixedSize(600, 430)
    
            splitter = QSplitter(self)
            splitter.addWidget(self.table)
            splitter.addWidget(self.chartView)
            self.setCentralWidget(splitter)
    
            series = QtCharts.QLineSeries(name="Odoslané")
            mapper = QtCharts.QVXYModelMapper(self, xColumn=0, yColumn=1)
            mapper.setModel(self.table.model())
            mapper.setSeries(series)
            chart.addSeries(mapper.series())
    
            self.axis_X = QtCharts.QDateTimeAxis()
            self.axis_X.setFormat("MMM yyyy")
            self.axis_Y = QtCharts.QValueAxis()
    
            chart.setAxisX(self.axis_X, series)
            chart.setAxisY(self.axis_Y, series)
            self.axis_Y.setRange(0, 0)
            self.axis_Y.setLabelFormat("%.0f")
            chart.setTitle("Chart")
    
        def addRow(self, dt, value):
            self.table.insertRow(0)
            for col, v in enumerate((dt.toMSecsSinceEpoch(), value)):
                it = QTableWidgetItem()
                it.setData(Qt.DisplayRole, v)
                self.table.setItem(0, col, it)
    
            if self.table.rowCount() == 1:
                self.axis_X.setRange(dt, dt.addDays(1))
                self.axis_Y.setRange(v, v)
    
            else:
                t_m, t_M = self.axis_X.min(), self.axis_X.max()
                t_m = min(t_m, dt)
                t_M = max(t_M, dt)
    
                m, M = self.axis_Y.min(), self.axis_Y.max()
                m = min(m, value)
                M = max(M, value)
    
                self.axis_X.setRange(t_m, t_M)
                self.axis_Y.setRange(m, M)
    
        def populate(self):
            for i in range(100):
                # simulate filling table with data as I get them from database.
                value = random.uniform(1, 40)
                fake_dt_str = QDate.currentDate().addDays(i).toString("dd.MM.yyyy")
                fake_value_str = str(random.uniform(0, 2))
    
                # Convert simulated data
                dt = QDateTime.fromString(fake_dt_str, "dd.MM.yyyy")
                value = float(fake_value_str)
                self.addRow(dt, value)
    
    def main():
        app = QApplication(sys.argv)
        gui = MainWindow()
        gui.show()
        sys.exit(app.exec_())
    
    
    main()