QML LineSeries with c++ Vector Data



  • Good morning,

    I'm looking to develop a basic real-time LineSeries plot, using the method/implementation defined in the Qt documentation:

    The LineSeries points are added as such:

    XYPoint { x: 0; y: 0 }
    

    I have a c++ class which reads data from a variety of sources, and appends them to a Vector:

    std::vector< int > arr;
    
    arr.push_back(1);
    arr.push_back(2);
    arr.push_back(3);
    //etc.
    

    How can I plot arr data in the QML LineSeries? I'm completely new to plotting with Qt/QML, so I'm not sure if this is even an optimal approach. Performance is the primary consideration in this case, as I'll be deploying on an embedded device with limited processing capabilities.

    Thanks!



  • Just thinking out loud, could I perhaps use a Repeater in my QML document, to add XYPoints for each index of the c++ Vector? Pseudo-code below.

     LineSeries {
            name: "LineSeries"
            Repeater {
                model: MyCppVector.size
                XYPoint { x: MyCppVector.array[index]; y: index }
            }
        }
    


  • I too use std containers and have my own QML / C++ solution. I may not have the answer but I have mine.
    I use a pointer to the QML LineSeries and do the data gathering, processing and LineSeries updates in C++

    In QML I set my axis:

    ChartView {
    DateTimeAxis {
                   id: valueAxisX
                   format: "m"
                   reverse: true;
               }
    ValueAxis {
    id: valueAxisY
    min: 0
    max: 100.0
    tickCount: 5
    /reverse: true;
    }
    LineSeries { ....
    

    and if you want performance - you got it - just specify in your series (mine are LineSeries):
    useOpenGL: true (see https://doc.qt.io/qt-5/qabstractseries.html#useOpenGL-prop) - there are some functionality sacrifices using this - but I need speed.

    As far as the data goes, you just need to change that std vec into a QVector<QPointF> and call the QXYSeries replace() https://doc.qt.io/qt-5/qxyseries.html#replace-5 - building your data up first and using replace is fast. I'd recommend not using anything like add - they fire way too much off.

    I use a ptr to the QML object gathered in advance to get access to the series I want.



  • @6thC Thank you very much, you've introduced a number of new (to me) elements in your approach. I don't suppose you have an example of how you would set up the LineSeries in c++ and then point to the LineSeries in QML? Similarly, how exactly is the OpenGL declaration made in c++? I understand the use of QXYSeries.replace().



  • @jars121
    QML XYSeries gives you
    QML AbstractSeries
    which exposes: https://doc.qt.io/qt-5/qml-qtcharts-abstractseries.html#useOpenGL-prop

    useOpenGL : bool
    

    But you can also in C++ use the QAbstractSeries : useOpenGL https://doc.qt.io/qt-5/qabstractseries.html#useOpenGL-prop with the pointer of your series as such:

    pSeries->setUseOpenGL(true );
    // or disable
    pSeries->setUseOpenGL(false);
    

    I group up my series into charts both the LineSeries and the ChartView through a slot. I use the ChartView to set value dynamic ranges series data.

    I've exposed this ChartPointers instance to the QML Engine as: ChartPointersCpp

    context->setContextProperty(QLatin1Literal("ChartPointersCpp"), &coreEngineInstance->Charts );
    

    Called this instance's addSeries with the chart and series QML instances as such:

    // QML
    ChartView {
                id: chart;
    ...
    // I've just lazily hardcoded the series instance - I'm sure dynamic is fine too
    LineSeries {id: series1; ...
    LineSeries {id: series2; ...
    LineSeries {id: series3; ...
    ...
    

    I have an event trigger (onMouseclick: on a (custom legend) item to set the series visible: true/false

    function processSeries(series){
    visible = !visible; // toggle
    ... if visible 
    ChartPointersCpp.addSeries(chart, series);
    ... else not visible
    ChartPointersCpp.removeSeries(series); // we already know the chartview ptr from c++ now
    

    I receives this call as:

    ChartPointers::addSeries(QObject* chartView, QLineSeries *pSeries);
    

    Once you have pSeries ... QAbstractSeries (I have a QLineSeries hardcoded) is all yours. Just be careful for nullptrs and the rest as I still let QML be it's parent etc, my c++ doesn't assume there's a valid ptr, if it's invalid it will remove it from the memory container as it was nullptr

    I use the chartView to access chart properties declared by me around dynamic ranges:

    qint64 beginMs = pChartView->property("beginMs").toLongLong();
    

    Think that should be enough to have you having some fun...



  • @6thC Thanks again, that's given me plenty to work with.

    I'm doing some reading at the moment, and have found that I can't use QtCharts with QGuiApplication, and have to instead use QApplication? I've tried swapping QGuiApplication in my .pro and .cpp files (QT += widgets and #include <QApplication> respectively), which prevents me from using the various QQml classes I'm already using (QQmlApplicationEngine, QQmlEngine, etc.)?

    I must be missing some quite elementary; how can I use both QApplication for QtCharts and QGuiApplication for all my QML-based classes at the same time?

    EDIT: It looks like I wasn't declaring QT += widgets early enough; I've now got a static LineSeries populated within my QML view. Now I'll start working on having c++ build the LineSeries and update/replace the plot in real-time.



  • Sweet, yes, with Charts it's Widget based. My project file has this entry:

    QT += qml         quick         widgets         quickcontrols2         charts
    

    You should be good to go already on the charts end then! Just update the data. When the points have been replaced using:

    void QXYSeries::replace(QVector<QPointF> points)
    

    it Emits

     QXYSeries::pointsReplaced()
    

    You may need this somewhere - if it's working already I'd call it a win!:

    QT_CHARTS_USE_NAMESPACE
    

    I found most of this from pulling apart and playing with:
    Qml Oscilloscope

    Take a look inside if you are curious. I modified this in a copy to use many more points.
    It was 10 * series, each with 100, 000 points updating with the same random data found in the Oscilloscope example app - all at rates every 16ms!

    And I just have a crappy Intel HD Graphics 530! It pulls ~45 to 60 fps with this going on. Anyhow, good luck, let me know.



  • @6thC Thanks again for your help!

    I've poured over the linked QML Oscilloscope example, and am afraid I've confused myself even further. I've just come across to c++ from Python (PyQt and PySide), so the declarations, use of header files, contextProperties, etc. are thoroughly confusing me at this point.

    Just so I'm clear, do you update the QPointF vector/array from c++? The QML Oscilloscope example has a Timer function in the .qml ChartView file which polls c++ for data, which isn't the approach I'd like to use. As (I believe you've described), I'd like for c++ to collect the data and push it to QML (using the replace() function as you've described).

    I've included a detailed breakdown of where I'm up to below (irrelevant sections omitted for simplicity).

    Data.cpp:

    #include "data.h"
    #include <QtCharts/QXYSeries"
    
    QT_CHARTS_USE_NAMESPACE
    
    Q_DECLARE_METATYPE(QAbstractSeries *)
    Q_DECLARE_METATYPE(QAbstractAxis *)
    
    Data::Data() :
        m_Value(0), m_Index(-1)
        {
            qRegisterMetaType<QAbstractSeries*>();
            qRegisterMetaType<QAbstractAxis*>();
    
            this->m_Timer = new QTimer(this);
            this->m_Timer->setInterval(200);
            connect(this->m_Timer, &QTimer::timeout, this, &Data::Timeout);
            this->m_Timer->start();
        }
    
        void Data::dataUpdate(QAbstractSeries *series)
        {
            if (series) {
                QXYSeries *xySeries = static_cast<QXYSeries *>(series);
                m_Index++;
                if (m_Index > m_Data.count() - 1)
                    m_Index = 0;
    
                QVector<QPointF> points = m_Data.at(m_Index);
                xySeries->replace(points);
            }
        }
    
        void Data::generateData(int value1, int value2)
        {
            //Receive the latest data values and append them to m_Data?
            //Once the vector/array has reached a certain size (e.g. 10),
            //Append new values to the end and remove initial values so the
            //Data set 'scrolls' with a fixed size
        }
    
        void DataDemo::Timeout()
        {
            int HIGH = 10;
            int LOW = 0;
            this->m_Value = rand() % (HIGH - LOW + 1) + LOW;
            emit ValueChanged();
            generateData(m_Value);
        }
    

    DataDemo.h:

    #ifndef DATADEMO_H
    #define DATADEMO_H
    
    #include <QObject>
    #include <QTimer>
    #include <QtCore/QObject>
    #include <QtCharts/QAbstractSeries>
    
    QT_BEGIN_NAMESPACE
    class QQuickView;
    QT_END_NAMESPACE
    
    QT_CHARTS_USE_NAMESPACE
    
    class Data : public QObjects
    {
        Q_OBJECT
    public:
        Data();
        Q_PROPERTY(int value READ value NOTIFY valueChanged)
        int value(){return this->m_Value;}
    
        //The below was taken from the QML Oscilloscope example
        //I don't know if it's needed in my case or what it's for
        explicit Data(QQuickView *appViewer, QObject *parent = 0);
    
    signals:
        void valueChanged();
    
    private slots:
        void Timeout();
    
    public slots:
        void dataUpdate(QAbstractSeries *series);
        void generateData(int value1, int value2);
    
    private:
        int         m_Value;
        QTimer    * m_Timer;
    
        QQuickView *m_appViewer;
        QList<QVector<QPointF> > m_Data;
        int         m_index;
    };
    #endif // DATADEMO_H
    

    main.cpp:

    //#include a whole host of items here
    
    using namespace std;
    
    int main(int argc, char *argv[])
    {
        QApplication app(argc, argv);
        QQmlApplicationEngine engine;
    
        engine.rootContext()->setContextProperty(QStringLiteral("Data"), new Data());
    
        engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
        if (engine.rootObjects().isEmpty())
            return -1;
    
        return app.exec();
    }
    

    main.qml:

    ChartView {
        id: chartView
        property bool openGL: true
        property bool openGLSupported: true
        antialiasing: true
        onOpenGLChanged: {
            if (openGLSupported) {
                series("signal 1").useOpenGL = openGL;
            }
        }
        Component.onCompleted: {
            if (!series("signal 1").useOpenGL) {
                openGLSupported = false
                openGL = false
            }
        }
        //A couple of ValueAxis elements here
    
        LineSeries {
            id: lineSeries1
            name: "signal 1"
            //If I use the below line I get OpenGLFrameBuffer issues for some reason
            useOpenGL: chartView.openGL
        }
    
        //I don't know if I want to use this Timer function (straight from the Oscilloscope demo)
        //I would rather the data update be pushed from c++ than requested from QML
        Timer {
            id: refreshTimer
            interval: 200
            running: true
            repeat: true
            onTriggered: {
                Data.dataUpdate(chartView.series(0));
            }
        }
    }
    

    I've cut a considerable amount out of my actual code, and have renamed almost everything to make it easier to follow, so if something doesn't make sense it's because I've done so incorrectly for this post. The application compiles and runs without error (provide the openGL line mentioned above is commented out), but the plot remains empty (the axes, grid, etc. are shown).

    Where am I going wrong?



  • Sorry to have confused you - they've only made that data class and timer so there's a constant supply of new data to show charts off - yes, you do use your data and with signals and slots - only replace the series points when you wish/require.

    I won't be near a machine for another 12 hours or so but I'll take a look at what you have and see if I can help you out then.

    Oh and sorry, I must have sent you the wrong example app, the one I had was a pure widget app I abused to make myself, it had a timer and everything but in c++ side.



  • @6thC That's quite alright! I'm thoroughly confused almost all the time at the moment as I embark on this c++ adventure; your input has been one of the few things I've managed to delve into and understand at least at a high level.

    If you don't mind have a flick through whenever you get a chance I'd really appreciate it! I'll continue reading for the time being, and will made another attempt tomorrow. Thanks again!



  • Bit of a sidetrack, but maybe useful if you follow me this way later on... I've just been testing closing and destroy()ing charts from the QML side. I've come across a nice memory access violation with my design. I should have seen it coming - using pointers to objects that weren't mine.

    I think my design has it's risks, I have mitigated it by calling my remove method on close just before I call destroy.

    Realizing this and solving that also exposed a risk in my own c++ design where I would deadlock (certainly) when I go multi-thread - and could hit currently via using a QML created signal.
    I was emit'ing inside c++ but that emit was inside iterations of a container member protected by a mutex / std::lock_guard. The slot connected to that emit'd signal sent it's own signal which a different slot but in the original class used a lock over that same member container.

    Anyhow. I'm learning as I go - I'm kind of apologizing if my way isn't the ideal way - it is my way and my way is working for me currently. Whether it's the most efficient way... I always like to listen to others with experience.



  • To fix what you have - as long as in c++ somewhere you use charts you have:

    #include <QtCharts>
    // I think that macro earlier just does the namespacing, I just removed the macro in my own application for this myself...
    using namespace QtCharts; 
    

    Really, the only changes you need to make is in your ChartView (QML).

    • give it dimensions somehow
      height & width / anchors.fill: parent; / etc
    • give it Axes ( axis(s) )
    DateTimeAxis {
        id: valueAxisX
        format: "s"
        reverse: true;
    }
    ValueAxis {
        id: valueAxisY // vertical axis
        min: 0
        max: 100.0
        tickCount: 5
        //reverse: true;
    }
    
    • in your LineSeries - assign it those axes:
    axisX: valueAxisX; axisY : valueAxisY ;
    


  • As far as OpenGL issues go... are you setting anything OpenGL manually? Setting the QSurfaceFormat yourself?

    Cause I had support raise this: https://bugreports.qt.io/browse/QTBUG-63807 - should be fixed now in 5.9.3

    Let me know - hopefully now you can ditch all timer references and get to pumping that chart series with data.



  • I'm afraid I'm getting progressively more lost the longer I attempt this. I'm reading through the QML Oscilloscope example for the 10th time, and I just can't understand how to apply that to my function. I.e. pushing data to the chart rather than QML requesting data.

    I'll continue reading and attempting, and will hopefully post back here with some updates/revised code of the coming couple of days.



  • Oh hey. Didn't see you were still stuck.

    So if you've QML declared your (LineSeries, ScatterSeries, and SplineSeries) - in C++ you'd have a slot (or Q_INVOKABLE) receive the object as a QXYSeries* ptr.

    Once you have access to a QXYSeries object: (you have a pointer to your series and you know which c++ std::vector matches what series etc) the only work you have to do is reformat your std::vector into a QVector<QPointF> before the replace() call of the QLineSeries*

    The LineSeries will do the work to repaint.
    I guess it signals: void QXYSeries::pointsReplaced() which kicks of some ChartView dirty or repaint request. Anyhow, it's free to us.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.