[QCharts] How to improve the performance of a line chart with a range-changing x-axis?
Unsolved
QML and Qt Quick
-
I need to display about 10 lines in one chart. Data comes about every 50ms. The x-axis is time, shared by all data. I've followed the oscilloscope example and found the chart very performant with OpenGL enabled. But after some experiments, I find that if the x-axis' range changes when new data is appended, the performance drops a lot. Is there a way to improve the performance of a line chart with a range-changing x-axis?
The following is an example project based on the oscilloscope example. After pressing the Start button, the line series' data will change every 50ms. On my Windows 10 laptop, the CPU usage increases from 0.8% to 9%, and the power consumption changes from low to high when the fixXAxis box is unchecked.
# CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(chart LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Charts Core Gui Qml Quick) qt_standard_project_setup() qt_add_executable(${PROJECT_NAME} main.cpp ) target_link_libraries(${PROJECT_NAME} PRIVATE Qt::Charts Qt::Core Qt::Gui Qt::Qml Qt::Quick ) set(resources_resource_files "qml/Main.qml" "qml/Chart.qml" ) qt6_add_resources(${PROJECT_NAME} "resources" PREFIX "/" FILES ${resources_resource_files} )
// main.cpp #include <QApplication> #include <QDir> #include <QLineSeries> #include <QQmlContext> #include <QQmlEngine> #include <QQuickView> #include <QTimer> #include <QValueAxis> #include <memory> #include <vector> #include <assert.h> Q_DECLARE_METATYPE(QAbstractSeries *) struct Backend : QObject { Q_OBJECT Q_PROPERTY(bool fixXAxis MEMBER fixXAxis NOTIFY fixXAxisChanged) // 2 LineSeries in qml ChartView std::vector<QXYSeries *> series; // common xAxis shared by all series QValueAxis *xAxis = nullptr; std::unique_ptr<QTimer> timer; bool fixXAxis = false; // use a fix-length list if fixXAxis==true static constexpr qsizetype listLen = 101; double tick = 0; signals: void fixXAxisChanged(bool); public: Backend() { qRegisterMetaType<QAbstractSeries *>(); connect(this, &Backend::fixXAxisChanged, [this](bool newFixXAxis){ if(newFixXAxis){ if(xAxis) xAxis->setRange(0, 100); }else{ tick = listLen; } }); } private: void updateSeries() { assert(series.size() == 2); if (fixXAxis) { regenerateData(tick); } else { series[0]->append(QPointF{tick, sin(tick / 5.0)}); series[1]->append(QPointF{tick, cos(tick / 5.0)}); xAxis->setRange(tick - 90, tick + 10); // performance } tick++; } void regenerateData(double startPoint) { QList<QPointF> data0; QList<QPointF> data1; data0.reserve(listLen); data1.reserve(listLen); for (double x = 0; x < listLen; ++x) { data0.push_back(QPointF{x, sin((startPoint + x) / 5.0)}); data1.push_back(QPointF{x, cos((startPoint + x) / 5.0)}); } series[0]->replace(data0); series[1]->replace(data1); } public slots: void registerSeries(QAbstractSeries *series) { auto xySeries = static_cast<QXYSeries *>(series); this->series.push_back(xySeries); // all registered line series share a common xAxis if (xAxis == nullptr) xAxis = static_cast<QValueAxis *>(xySeries->attachedAxes()[0]); } void startTimer() { timer = std::make_unique<QTimer>(); connect(timer.get(), &QTimer::timeout, [this] { updateSeries(); }); timer->start(50); } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); // Set QSG_RHI_BACKEND=opengl environment variable to force the OpenGL backend to be used const bool openGLSupported = QQuickWindow::graphicsApi() == QSGRendererInterface::OpenGLRhi; assert(openGLSupported); Backend backend; QQuickView viewer; #ifdef Q_OS_WIN QString extraImportPath(QStringLiteral("%1/../../../../%2")); #else QString extraImportPath(QStringLiteral("%1/../../../%2")); #endif viewer.engine()->addImportPath(extraImportPath.arg( QGuiApplication::applicationDirPath(), QString::fromLatin1("qml"))); QObject::connect(viewer.engine(), &QQmlEngine::quit, &viewer, &QWindow::close); viewer.rootContext()->setContextProperty("backend", &backend); viewer.setSource(QUrl("qrc:/qml/Main.qml")); viewer.setResizeMode(QQuickView::SizeRootObjectToView); viewer.show(); return app.exec(); } #include "main.moc"
// qml/Main.qml import QtQuick import QtQuick.Controls Item{ id: main width: 600 height: 400 Button { id: btn text: "Start" onClicked: chart.registerAndStart() } CheckBox { id: ckBox text: "FixXAxis" checked: backend.fixXAxis anchors.top: btn.bottom onClicked: backend.fixXAxis = !backend.fixXAxis } Text { text: "opengl: "+ (chart.openGL ? "y" : "n") anchors.top: ckBox.bottom } Chart { id: chart anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right anchors.left: ckBox.right height: main.height } }
// qml/Chart.qml import QtQuick import QtCharts ChartView { id: chartView property bool openGL: true animationOptions: ChartView.NoAnimation theme: ChartView.ChartThemeDark ValueAxis { id: axisY1 min: -2 max: 4 } ValueAxis { id: axisY2 min: -4 max: 2 } ValueAxis { id: axisX min: 0 max: 100 } LineSeries { id: lineSeries1 name: "signal 1" axisX: axisX axisY: axisY1 useOpenGL: chartView.openGL } LineSeries { id: lineSeries2 name: "signal 2" axisX: axisX axisYRight: axisY2 useOpenGL: chartView.openGL } function registerAndStart(){ backend.registerSeries(chartView.series(0)) backend.registerSeries(chartView.series(1)) backend.startTimer() } }