Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

LineSeries with QAbstractListModel's roles



  • Hello, I've been searching for a way to use the QML Charts with a class derived from QAbstractListModel. It's a fairly trivial implementation of QAbstractListModel with user-defined roles. I've successfully been able to use QML's ListView by specifying custom delegates that use the named roles, but I would like a similar feature within QML Charts. The closest I've seen is the use of VXYModelMapper. However, that deals directly with columns of data, and not roles. I would prefer to not have to change my model from a QAbstractListModel to a QAbstractTableModel.

    Would I need to reimplement my own model mapper? Is there an existing one? Is there a better way to do all of this?

    Here's my trival implementation of QAbstractListModel:
    TrivialListModel.h:

    struct ModelElement {
        QDateTime receivedTime;
        int var1;
        double var2;
    };
    
    class TrivialListModel : public QAbstractListModel
    {
        Q_OBJECT
    public:
        enum Roles {
            ReceivedTime = Qt::UserRole + 1,
            Var1,
            Var2
        };
        Q_ENUM(Roles)
    
        explicit TrivialListModel (QObject *parent = nullptr);
    
        // Basic functionality:
        QModelIndex index(int row, int column, const QModelIndex& parent) const override;
        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
        virtual QHash<int, QByteArray> roleNames() const override;
    
        // Custom functionality
        void addData(const ModelElement& element);
    
    private:
        QList<ModelElement> m_data;
    };
    

    TrivialListModel.cpp:

    TrivialListModel::TrivialListModel(QObject *parent)
        : QAbstractListModel(parent)
    {
    }
    
    QModelIndex TrivialListModel::index(int row, int column, const QModelIndex& parent) const
    {
        if(parent.isValid()) {
            return {};
        }
    
        return createIndex(row, column);
    }
    
    int TrivialListModel::rowCount(const QModelIndex &parent) const
    {
        if (parent.isValid())
            return 0;
    
        return m_data.size();
    }
    
    QVariant TrivialListModel::data(const QModelIndex &index, int role) const
    {
        if (!index.isValid())
            return QVariant();
    
        if (index.row() < 0 || index.row() >= m_data.size())
            return QVariant();
    
        const auto modelElement = m_data.at(index.row());
    
        switch(role) {
            case ReceivedTime: {
                return modelElement.receivedTime;
            }
            case Var1: {
                return modelElement.var1;
            }
            case Var2: {
                return modelElement.var2;
            }
        }
    
        return {};
    }
    
    QHash<int, QByteArray> TrivialListModel::roleNames() const
    {
        return {
            {ReceivedTime, "receivedTime"},
            {Var1, "var1"},
            {Var2, "var2"}
        }
    }
    
    void TrivialListModel::addData(const ModelElement& element)
    {
        beginInsertRows(QModelIndex(), m_data.size(), m_data.size());
    
        m_data.append(element);
    
        endInsertRows();
    }
    

    Desired QML (or something similar):

    ChartView {
        anchors.fill: parent
        antialiasing: true
        legend.visible: true
    
        SplineSeries {
            VXYModelMapper { // Custom ModelWrapper?
                model: listModel // Assume this was already put into the root context
                xColumn: "receivedTime"
                yColumn: "var1"
            }
        }
    }
    

    Any thoughts on this would be greatly appreciated. Thanks!



  • I am not sure of exactly what you need, but would a DelegateModel help with this? I am not sure if the DelegateModel can create the objects SplineSeries requires though.



  • @fcarney I don't think a DelegateModel would be appropriate for what I'm looking for. Essentially I'm just looking for a way to map chart series to a model's roles and not columns.



  • I don't think SplineSeries takes a model. You are right DelegateModel maps a model to a model. You want something that can read a model and instance objects in the SplineSeries:

    import QtQuick 2.12
    import QtQuick.Controls 2.12
    import QtQuick.Window 2.12
    import QtCharts 2.15
    import QtQml.Models 2.12
    
    ApplicationWindow {
        visible: true
        width: 400
        height: 900
        title: qsTr("Dynamic Charts")
    
        ListModel {
            id: listModel
    
            ListElement {xv:0; yv:0}
            ListElement {xv:0.25; yv:0.10}
            ListElement {xv:0.25; yv:0.25}
            ListElement {xv:0.50; yv:0.50}
        }
    
        Column {
            width: parent.width
            ChartView {
                width: parent.width
                height: 300
                antialiasing: true
                legend.visible: false
    
                Instantiator {
                    model: listModel
                    onObjectAdded: splineseries.insert(index, object.x, object.y)
                    onObjectRemoved: splineseries.remove(index)
                    QtObject{
                        property real x: xv
                        property real y: yv
                    }
                }
    
                SplineSeries {
                    id: splineseries
                }
            }
        }
    
        //Component.onCompleted: console.log(listModel.count)
    }
    

    I tried a Repeater inside the SplineSeries, but it would not create items for whatever reason. Unsure why. I don't think the SplineSeries liked the Repeater.



  • @fcarney I did get your example code to work, but expanding that to my problem seems a bit unfeasible. I provided my "trivial" example, but in the real world I have almost 100 roles. It seems that I'd have to create a property in the QtObject that was in your Instantiator for each one of my roles.

    I'm really just looking for something that works somewhat like the example I provided originally, because there's actually a 3d charting QML object that does do it via roles, exactly the way I want, except in 3d and not 2d:

    Scatter3D {
        anchors.fill: parent
                
        Scatter3DSeries {
            ItemModelScatterDataProxy {
                itemModel: itemModel
                        
                xPosRole: "receivedTime" // Item roles from the original code I provided
                yPosRole: "val1"
                zPosRole: "val2"
            }
        }
    }
    

    I'd really like essentially what is above, but in 2d chart form. I'm not sure why QML provides these objects for their 3d charting library, but not the 2d one.


  • Qt Champions 2018

    You correctly identified the problem that VXYModelMapper works with model with columns and your model has multiple roles instead of columns.

    If you don't want to modify your model to also add columns, you could write a proxy model transposing roles to columns as a middle man. For inspiration, the reverse has been done there : https://github.com/KDAB/KDToolBox/tree/master/qt/model_view/sortProxyModel



  • @GrecKo Thanks GrecKo for the inspiration for that. I created my own QIdentityProxyModel (wasn't sure if I should use that one, or QAbstractProxyModel) as follows:

    class TrivialTableProxyModel: public QIdentityProxyModel
    {
        Q_OBJECT
        QML_ELEMENT
    public:
        TrivialTableProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {};
    
        virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override {
            if(!index.isValid()) {
                return {};
            }
            
            if(role != Qt::DisplayRole) {
                return {};
            }
            qDebug() << "Proxy:" << index << ", Role:" << role;
            
            return sourceModel()->data(createIndex(index.row(), 0), index.column() + Qt::UserRole + 1); // Adding Qt::UserRole+1 since that's where my roles start
        }
    

    And in QML I implemented it as follows:

    ChartView {
        anchors.fill: parent
        antialiasing: true
        legend.visible: true
        
        ScatterSeries {
            name: "Data"
            axisX: DateTimeAxis { }
            axisY: ValueAxis { }
    
            VXYModelMapper {
                xColumn: 0 // Received time role/column
                yColumn: 1 // Var1 role/column
                model: trivialTableProxyModel // Assume this was inserted into the root context
            }
        }
    }
    

    It seems like it's much closer (I even got it working with manually setting the VXYModelMapper's rowCount), but when I leave it at the default -1, it seems to endlessly call the data function. I even went through the effort of turning my original model into a QAbstractTableModel, but with the same result. Here's an output of the print statements from the proxy's data function and the model's data function:

    Proxy: QModelIndex(1,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(1,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    Proxy: QModelIndex(1,1,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(1,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    Proxy: QModelIndex(2,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(2,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    Proxy: QModelIndex(2,1,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(2,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    ...
    Proxy: QModelIndex(12065,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(12065,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    Proxy: QModelIndex(12065,1,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(12065,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    Proxy: QModelIndex(12066,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(12066,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    Proxy: QModelIndex(12066,1,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 0
    Model: QModelIndex(12066,0,0x0,HousekeepingTableProxyModel(0x399856f580)), Role: 1
    

    It may be hard to decipher, but the translation is working from roles/columns. I'm just now not sure why VXYModelMapper endlessly calls the data functions with ever increasing row counts. Is this a bug, or did I implement my model incorrectly?



  • @Tyrannic said in LineSeries with QAbstractListModel's roles:

    ScatterSeries

    I don't see a dataProxy in the ScatterSeries docs like I see in the Scatter3DSeries. I don't see any docs that a ScatterSeries takes a model. How is this supposed to work?



  • Anyway, I may have a way to do go wide on your data. But I am unsure if this fits your data. I don't have a good feel for what is in your model:

    import QtQuick 2.12
    import QtQuick.Controls 2.12
    import QtQuick.Window 2.12
    import QtCharts 2.15
    import QtQml.Models 2.12
    
    ApplicationWindow {
        visible: true
        width: 400
        height: 900
        title: qsTr("Dynamic Charts")
    
        ListModel {
            id: listModel
    
            ListElement {xv1:0.00; yv1:0.00; xv2:0.10; yv2:0.10; xv3:0.25; yv3:0.10; xv4:0.10; yv4:0.25}
            ListElement {xv1:0.50; yv1:0.50; xv2:0.75; yv2:0.50; xv3:0.50; yv3:0.75; xv4:1.00; yv4:1.00}
        }
    
        Column {
            width: parent.width
            ChartView {
                width: parent.width
                height: 300
                antialiasing: true
                legend.visible: false
    
                Instantiator {
                    model: listModel
                    onObjectAdded: {
                        for(var subindex=0; subindex<8; ++subindex){
                            console.log(index*8+subindex)
                            splineseries.insert(index*8+subindex, object.points[subindex].x, object.points[subindex].y)
                        }
                    }
                    onObjectRemoved: {
                        for(var subindex=0; subindex<8; ++subindex){
                            splineseries.remove(index*8+subindex)
                        }
                    }
                    delegate: QtObject{
                        property var points: {
                            var arr = []
    
                            arr.push(Qt.point(xv1, yv1))
                            arr.push(Qt.point(xv2, yv2))
                            arr.push(Qt.point(xv3, yv3))
                            arr.push(Qt.point(xv4, yv4))
    
                            return arr
                        }
                    }
                }
    
                SplineSeries {
                    id: splineseries
                }
            }
        }
    }
    


  • @fcarney Well, that was somewhat the point, was that it was frustrating that the Scatter3dSeries could take a roles as demonstrated by my example code, but not the ScatterSeries. Either way, I'm one step closer to having a solution thanks to @GrecKo, but as illustrated by my last post, VXYModelMapper seems to be misbehaving when rowCount is -1. That or I messed up the Proxy and/or model.



  • For checking your model put this in your models constructor:

    new QAbstractItemModelTester(this, QAbstractItemModelTester::FailureReportingMode::Warning, this);
    

    This will print out issues it finds. You can look at the source code to that class to see exactly what it is testing in the Qt source.



  • @fcarney I put the QAbstractItemModelTester in both my model, and my proxy model, and... no errors. I also swapped it to FailureReportingMode::Fatal to double check, and nothing. The endless calls to data from VXYModelMapper continue.