Search code examples
pythonpandaspyqtpyqt5qabstracttablemodel

tableview not updated after sort on tablemodel


The idea is to display a dataframe via the PyQt5 MV programming idiom and perform some basic sorting and filtering operations on the presented dataframe.

The displaying part all went fine however now I am stuck on the sorting part of the tool. Print statement showed me the dataframe it self was sorted, it is the view that is not updated. So now to the code:

import sys
import operator
tmp = [('23-02-1978', '19:03:13', 'eh', None, 'even more some data'),
       ('23-02-1978', '19:01:45',  'ss', 'some data ', 'even more some data'),
       ('23-02-1978', '19:02:55',  'he', 'some data ', 'even more some data')]    


tmp1 = [('23-02-1978', '19:02:33',  'eh', 'some data ', '666', 'even more some data'),
        ('23-02-1978', '19:03:22',  'ss', 'some data ', '777', 'even more some data'),
        ('23-02-1978', '19:01:45',  'he', 'some data ', '888', 'even more some data')]  


from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QAction,
                             QGroupBox, QCheckBox, QTableView, QTableWidgetItem, 
                             QTabWidget, QGridLayout,QLineEdit, QFormLayout, 
                             QVBoxLayout, QHBoxLayout, QLabel, QDialog, QHeaderView)

from PyQt5.QtGui import QIcon, QFont 
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QAbstractTableModel, QVariant, QModelIndex, QSortFilterProxyModel


from pandas import DataFrame


class DataFrameModel(QAbstractTableModel): 
    def __init__(self): 
        """ datain: a list of lists
            headerdata: a list of strings
        """
        super(DataFrameModel, self).__init__()
        self._df = DataFrame()

    def setDataFrame(self, df):
        self._df = df;

    def signalUpdate(self):
        ''' tell viewers to update their data (this is full update, not
        efficient)'''
        self.layoutChanged.emit()


    #------------- table display functions -----------------
    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return QVariant()

        if orientation == Qt.Horizontal:
            try:
                return self._df.columns.tolist()[section]
            except (IndexError, ):
                return QVariant()
        elif orientation == Qt.Vertical:
            try:
                # return self.df.index.tolist()
                return self._df.index.tolist()[section]
            except (IndexError, ):
                return QVariant()

    def data(self, index, role=Qt.DisplayRole):
        if role != Qt.DisplayRole:
            return QVariant()

        if not index.isValid():
            return QVariant()

        return QVariant(str(self._df.ix[index.row(), index.column()]))

    def flags(self, index):
            flags = super(DataFrameModel, self).flags(index)
            return flags

    def setData(self, index, value, role):
        row = self._df.index[index.row()]
        col = self._df.columns[index.column()]
        if hasattr(value, 'toPyObject'):
            # PyQt4 gets a QVariant
            value = value.toPyObject()
        else:
            # PySide gets an unicode
            dtype = self._df[col].dtype
            if dtype != object:
                value = None if value == '' else dtype.type(value)
        self._df.set_value(row, col, value)
        return True

    def rowCount(self, parent=QModelIndex()): 
        return len(self._df.index)

    def columnCount(self, parent=QModelIndex()): 
        return len(self._df.columns)

    def sort(self, column, order=Qt.AscendingOrder):
        """Sort table by given column number.
        """
        print('sort clicked col {} order {}'.format(column, order))
        self.layoutAboutToBeChanged.emit()
        print(self._df.columns[column])
        self._df.sort_values('time', ascending=order == Qt.AscendingOrder, inplace=True)
        print(self._df)
        self.layoutChanged.emit()


class DataFrameWidget(QWidget):
    ''' a simple widget for using DataFrames in a gui '''
    def __init__(self, dataFrame, parent=None):
        super(DataFrameWidget, self).__init__(parent)

        self.dataModel = DataFrameModel()
        # Set DataFrame
        self.dataTable = QTableView()
#        self.proxy = QSortFilterProxyModel()
#        self.proxy.setSourceModel(self.dataModel)
        self.dataTable.setModel(self.dataModel)

        self.dataTable.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

        self.setDataFrame(dataFrame)
        self.dataTable.setSortingEnabled(True)
        self.dataTable.sortByColumn(0,0)


        layout = QVBoxLayout()
        layout.addWidget(self.dataTable)
        self.setLayout(layout)

    def setDataFrame(self, dataFrame):
        self.dataModel.setDataFrame(dataFrame)
        self.dataModel.signalUpdate()


def testDf():
    ''' creates test dataframe '''
#    data = {'int': [1, 2, 3], 'float': [1.5, 2.5, 3.5],
#            'string': ['a', 'b', 'c'], 'nan': [np.nan, np.nan, np.nan]}

#    data = [(1, 1.5, 'a', np.nan),
#            (2, 2.5, 'b', np.nan),
#            (3, 3.5, 'c', np.nan)]



    return DataFrame(tmp, columns=['date', 'time', 'string', 'nan', 'bla'])

class Form(QDialog):
    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        df = testDf()  # make up some data
        widget = DataFrameWidget(df)

        layout = QVBoxLayout()
        layout.addWidget(widget)
        self.setLayout(layout)

if __name__ == '__main__':

    app = QApplication(sys.argv)
    form = Form()
    form.show()
    exit(app.exec_())

Using the SortFilterProxy worked for this example, but was very slow on larger dataframes.

The above code expample did work, read sorted, for not-dataframe data. Creating a model/view with just the list of tuples worked fine.

The advices I found were mainly in two direction: remember to signal the view or use sortfilterproxy. I remembered and tried but not succeeded so far. Seems to be related to the usage of a dataframe. All advice is welcome. Thanks in advance.


Solution

  • In the next part I show the result of your impression, in it we see that it is reordered, but also the index is reordered, and this causes that when it is not updated the change.

    sort clicked col 0 order 1
    date
             date      time string         nan                  bla
    0  23-02-1978  19:03:13     eh        None  even more some data
    2  23-02-1978  19:02:55     he  some data   even more some data
    1  23-02-1978  19:01:45     ss  some data   even more some data
    sort clicked col 0 order 0
    date
             date      time string         nan                  bla
    1  23-02-1978  19:01:45     ss  some data   even more some data
    2  23-02-1978  19:02:55     he  some data   even more some data
    0  23-02-1978  19:03:13     eh        None  even more some data
    

    To update the data you must reset the indexes with reset_index().

    def sort(self, column, order):
        """Sort table by given column number.
        """
        print('sort clicked col {} order {}'.format(column, order))
        self.layoutAboutToBeChanged.emit()
        print(self._df.columns[column])
        self._df.sort_values('time', ascending=order == Qt.AscendingOrder, inplace=True)
        self._df.reset_index(inplace=True, drop=True) # <-- this is the change
        print(self._df)
        self.layoutChanged.emit()